mirror of https://github.com/vapor/docs.git
Compare commits
34 Commits
036d65cdb2
...
9dede3eb21
| Author | SHA1 | Date |
|---|---|---|
|
|
9dede3eb21 | |
|
|
16ae086917 | |
|
|
f2000c5544 | |
|
|
12d9bd6b52 | |
|
|
359f89b2b8 | |
|
|
4c8ff6f0b0 | |
|
|
053a46f953 | |
|
|
9dc39172b7 | |
|
|
151df13e6b | |
|
|
89abfe99ea | |
|
|
4e6c15f3c5 | |
|
|
a3501f9221 | |
|
|
40b992beab | |
|
|
6af2df1fba | |
|
|
1661adfc5c | |
|
|
f37592046a | |
|
|
3029c8eab1 | |
|
|
7eaebe178a | |
|
|
42dae97fe6 | |
|
|
2305ddc577 | |
|
|
3a5cb02e93 | |
|
|
b828c9f568 | |
|
|
16021e2d82 | |
|
|
b7a745481b | |
|
|
961c886b28 | |
|
|
27446a3441 | |
|
|
9bc3acd019 | |
|
|
6faa8ea64d | |
|
|
7cdd9dbaaa | |
|
|
fc2115b126 | |
|
|
e96ea0759c | |
|
|
406686a471 | |
|
|
a1a9a7a62c | |
|
|
eeb0a3837f |
|
|
@ -0,0 +1,148 @@
|
|||
# APNS
|
||||
|
||||
VaporのApple Push Notification Service (APNS) APIを使用すると、Appleデバイスへのプッシュ通知の認証と送信が簡単になります。これは[APNSwift](https://github.com/swift-server-community/APNSwift)の上に構築されています。
|
||||
|
||||
## はじめに {#getting-started}
|
||||
|
||||
APNSの使用を開始する方法を見てみましょう。
|
||||
|
||||
### パッケージ {#package}
|
||||
|
||||
APNSを使用する最初のステップは、依存関係にパッケージを追加することです。
|
||||
|
||||
```swift
|
||||
// swift-tools-version:5.8
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "my-app",
|
||||
dependencies: [
|
||||
// Other dependencies...
|
||||
.package(url: "https://github.com/vapor/apns.git", from: "4.0.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "App", dependencies: [
|
||||
// Other dependencies...
|
||||
.product(name: "VaporAPNS", package: "apns")
|
||||
]),
|
||||
// Other targets...
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
Xcode内で直接マニフェストを編集すると、ファイルが保存されたときに自動的に変更を検出し、新しい依存関係を取得します。それ以外の場合は、ターミナルから`swift package resolve`を実行して新しい依存関係を取得してください。
|
||||
|
||||
### 設定 {#configuration}
|
||||
|
||||
APNSモジュールは`Application`に新しいプロパティ`apns`を追加します。プッシュ通知を送信するには、認証情報を使用して`configuration`プロパティを設定する必要があります。
|
||||
|
||||
```swift
|
||||
import APNS
|
||||
import VaporAPNS
|
||||
import APNSCore
|
||||
|
||||
// JWT認証を使用してAPNSを設定します。
|
||||
let apnsConfig = APNSClientConfiguration(
|
||||
authenticationMethod: .jwt(
|
||||
privateKey: try .loadFrom(string: "<#key.p8 content#>"),
|
||||
keyIdentifier: "<#key identifier#>",
|
||||
teamIdentifier: "<#team identifier#>"
|
||||
),
|
||||
environment: .development
|
||||
)
|
||||
app.apns.containers.use(
|
||||
apnsConfig,
|
||||
eventLoopGroupProvider: .shared(app.eventLoopGroup),
|
||||
responseDecoder: JSONDecoder(),
|
||||
requestEncoder: JSONEncoder(),
|
||||
as: .default
|
||||
)
|
||||
```
|
||||
|
||||
プレースホルダーを認証情報で置き換えてください。上記の例は、Appleの開発者ポータルから取得した`.p8`キーを使用した[JWTベースの認証](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/establishing_a_token-based_connection_to_apns)を示しています。証明書を使用した[TLSベースの認証](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/establishing_a_certificate-based_connection_to_apns)の場合は、`.tls`認証方法を使用してください:
|
||||
|
||||
```swift
|
||||
authenticationMethod: .tls(
|
||||
privateKeyPath: <#path to private key#>,
|
||||
pemPath: <#path to pem file#>,
|
||||
pemPassword: <#optional pem password#>
|
||||
)
|
||||
```
|
||||
|
||||
### 送信 {#send}
|
||||
|
||||
APNSが設定されたら、`Application`または`Request`の`apns.send`メソッドを使用してプッシュ通知を送信できます。
|
||||
|
||||
```swift
|
||||
// カスタムCodableペイロード
|
||||
struct Payload: Codable {
|
||||
let acme1: String
|
||||
let acme2: Int
|
||||
}
|
||||
// プッシュ通知アラートを作成
|
||||
let dt = "70075697aa918ebddd64efb165f5b9cb92ce095f1c4c76d995b384c623a258bb"
|
||||
let payload = Payload(acme1: "hey", acme2: 2)
|
||||
let alert = APNSAlertNotification(
|
||||
alert: .init(
|
||||
title: .raw("Hello"),
|
||||
subtitle: .raw("This is a test from vapor/apns")
|
||||
),
|
||||
expiration: .immediately,
|
||||
priority: .immediately,
|
||||
topic: "<#my topic#>",
|
||||
payload: payload
|
||||
)
|
||||
// 通知を送信
|
||||
try! await req.apns.client.sendAlertNotification(
|
||||
alert,
|
||||
deviceToken: dt,
|
||||
deadline: .distantFuture
|
||||
)
|
||||
```
|
||||
|
||||
ルートハンドラー内にいる場合は、`req.apns`を使用してください。
|
||||
|
||||
```swift
|
||||
// プッシュ通知を送信します。
|
||||
app.get("test-push") { req async throws -> HTTPStatus in
|
||||
try await req.apns.client.send(...)
|
||||
return .ok
|
||||
}
|
||||
```
|
||||
|
||||
最初のパラメータはプッシュ通知アラートを受け取り、2番目のパラメータはターゲットデバイストークンです。
|
||||
|
||||
## アラート {#alert}
|
||||
|
||||
`APNSAlertNotification`は、送信するプッシュ通知アラートの実際のメタデータです。各プロパティの詳細については[こちら](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html)で提供されています。これらはAppleのドキュメントに記載されている1対1の命名規則に従っています。
|
||||
|
||||
```swift
|
||||
let alert = APNSAlertNotification(
|
||||
alert: .init(
|
||||
title: .raw("Hello"),
|
||||
subtitle: .raw("This is a test from vapor/apns")
|
||||
),
|
||||
expiration: .immediately,
|
||||
priority: .immediately,
|
||||
topic: "<#my topic#>",
|
||||
payload: payload
|
||||
)
|
||||
```
|
||||
|
||||
このタイプは`send`メソッドに直接渡すことができます。
|
||||
|
||||
### カスタム通知データ {#custom-notification-data}
|
||||
|
||||
Appleは、各通知にカスタムペイロードデータを追加する機能をエンジニアに提供しています。これを容易にするために、すべての`send` APIのペイロードパラメータに`Codable`準拠を受け入れています。
|
||||
|
||||
```swift
|
||||
// カスタムCodableペイロード
|
||||
struct Payload: Codable {
|
||||
let acme1: String
|
||||
let acme2: Int
|
||||
}
|
||||
```
|
||||
|
||||
## 詳細情報 {#more-information}
|
||||
|
||||
利用可能なメソッドの詳細については、[APNSwiftのREADME](https://github.com/swift-server-community/APNSwift)を参照してください。
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
# コマンド {#commands}
|
||||
|
||||
VaporのCommand APIを使用すると、カスタムコマンドライン関数を構築し、ターミナルと対話できます。これは、`serve`、`routes`、`migrate`などのVaporのデフォルトコマンドが構築されている基盤です。
|
||||
|
||||
## デフォルトコマンド {#default-commands}
|
||||
|
||||
`--help`オプションを使用して、Vaporのデフォルトコマンドについて詳しく学ぶことができます。
|
||||
|
||||
```sh
|
||||
swift run App --help
|
||||
```
|
||||
|
||||
特定のコマンドに`--help`を使用すると、そのコマンドが受け入れる引数とオプションを確認できます。
|
||||
|
||||
```sh
|
||||
swift run App serve --help
|
||||
```
|
||||
|
||||
### Xcode
|
||||
|
||||
Xcodeでコマンドを実行するには、`App`スキームに引数を追加します。これを行うには、次の手順に従います:
|
||||
|
||||
- `App`スキームを選択(再生/停止ボタンの右側)
|
||||
- 「Edit Scheme」をクリック
|
||||
- 「App」プロダクトを選択
|
||||
- 「Arguments」タブを選択
|
||||
- 「Arguments Passed On Launch」にコマンド名を追加(例:`serve`)
|
||||
|
||||
## カスタムコマンド {#custom-commands}
|
||||
|
||||
`AsyncCommand`に準拠する型を作成することで、独自のコマンドを作成できます。
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
struct HelloCommand: AsyncCommand {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
カスタムコマンドを`app.asyncCommands`に追加すると、`swift run`経由で利用可能になります。
|
||||
|
||||
```swift
|
||||
app.asyncCommands.use(HelloCommand(), as: "hello")
|
||||
```
|
||||
|
||||
`AsyncCommand`に準拠するには、`run`メソッドを実装する必要があります。これには`Signature`の宣言が必要です。また、デフォルトのヘルプテキストも提供する必要があります。
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
struct HelloCommand: AsyncCommand {
|
||||
struct Signature: CommandSignature { }
|
||||
|
||||
var help: String {
|
||||
"Says hello"
|
||||
}
|
||||
|
||||
func run(using context: CommandContext, signature: Signature) async throws {
|
||||
context.console.print("Hello, world!")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
このシンプルなコマンドの例には引数やオプションがないため、シグネチャは空のままにします。
|
||||
|
||||
提供されたコンテキストを介して現在のコンソールにアクセスできます。コンソールには、ユーザー入力のプロンプト、出力のフォーマットなど、多くの便利なメソッドがあります。
|
||||
|
||||
```swift
|
||||
let name = context.console.ask("What is your \("name", color: .blue)?")
|
||||
context.console.print("Hello, \(name) 👋")
|
||||
```
|
||||
|
||||
次のコマンドを実行してコマンドをテストします:
|
||||
|
||||
```sh
|
||||
swift run App hello
|
||||
```
|
||||
|
||||
### Cowsay
|
||||
|
||||
`@Argument`と`@Option`の使用例として、有名な[`cowsay`](https://en.wikipedia.org/wiki/Cowsay)コマンドの再現を見てみましょう。
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
struct Cowsay: AsyncCommand {
|
||||
struct Signature: CommandSignature {
|
||||
@Argument(name: "message")
|
||||
var message: String
|
||||
|
||||
@Option(name: "eyes", short: "e")
|
||||
var eyes: String?
|
||||
|
||||
@Option(name: "tongue", short: "t")
|
||||
var tongue: String?
|
||||
}
|
||||
|
||||
var help: String {
|
||||
"Generates ASCII picture of a cow with a message."
|
||||
}
|
||||
|
||||
func run(using context: CommandContext, signature: Signature) async throws {
|
||||
let eyes = signature.eyes ?? "oo"
|
||||
let tongue = signature.tongue ?? " "
|
||||
let cow = #"""
|
||||
< $M >
|
||||
\ ^__^
|
||||
\ ($E)\_______
|
||||
(__)\ )\/\
|
||||
$T ||----w |
|
||||
|| ||
|
||||
"""#.replacingOccurrences(of: "$M", with: signature.message)
|
||||
.replacingOccurrences(of: "$E", with: eyes)
|
||||
.replacingOccurrences(of: "$T", with: tongue)
|
||||
context.console.print(cow)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
これをアプリケーションに追加して実行してみてください。
|
||||
|
||||
```swift
|
||||
app.asyncCommands.use(Cowsay(), as: "cowsay")
|
||||
```
|
||||
|
||||
```sh
|
||||
swift run App cowsay sup --eyes ^^ --tongue "U "
|
||||
```
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
# ファイル {#files}
|
||||
|
||||
Vaporは、ルートハンドラ内でファイルを非同期に読み書きするためのシンプルなAPIを提供しています。このAPIは、NIOの[`NonBlockingFileIO`](https://swiftpackageindex.com/apple/swift-nio/main/documentation/nioposix/nonblockingfileio)型の上に構築されています。
|
||||
|
||||
## 読み取り {#read}
|
||||
|
||||
ファイルを読み取るための主要なメソッドは、ディスクから読み取られたチャンクをコールバックハンドラに配信します。読み取るファイルはパスで指定します。相対パスは、プロセスの現在の作業ディレクトリを参照します。
|
||||
|
||||
```swift
|
||||
// ディスクからファイルを非同期に読み取ります。
|
||||
let readComplete: EventLoopFuture<Void> = req.fileio.readFile(at: "/path/to/file") { chunk in
|
||||
print(chunk) // ByteBuffer
|
||||
}
|
||||
|
||||
// または
|
||||
|
||||
try await req.fileio.readFile(at: "/path/to/file") { chunk in
|
||||
print(chunk) // ByteBuffer
|
||||
}
|
||||
// 読み取り完了
|
||||
```
|
||||
|
||||
`EventLoopFuture`を使用している場合、返されたfutureは読み取りが完了したか、エラーが発生したときにシグナルを送ります。`async`/`await`を使用している場合、`await`が返ると読み取りが完了しています。エラーが発生した場合は、エラーをスローします。
|
||||
|
||||
### ストリーム {#stream}
|
||||
|
||||
`streamFile`メソッドは、ストリーミングファイルを`Response`に変換します。このメソッドは、`ETag`や`Content-Type`などの適切なヘッダーを自動的に設定します。
|
||||
|
||||
```swift
|
||||
// ファイルを非同期にHTTPレスポンスとしてストリームします。
|
||||
req.fileio.streamFile(at: "/path/to/file").map { res in
|
||||
print(res) // Response
|
||||
}
|
||||
|
||||
// または
|
||||
|
||||
let res = req.fileio.streamFile(at: "/path/to/file")
|
||||
print(res)
|
||||
|
||||
```
|
||||
|
||||
結果は、ルートハンドラから直接返すことができます。
|
||||
|
||||
### 収集 {#collect}
|
||||
|
||||
`collectFile`メソッドは、指定されたファイルをバッファに読み込みます。
|
||||
|
||||
```swift
|
||||
// ファイルをバッファに読み込みます。
|
||||
req.fileio.collectFile(at: "/path/to/file").map { buffer in
|
||||
print(buffer) // ByteBuffer
|
||||
}
|
||||
|
||||
// または
|
||||
|
||||
let buffer = req.fileio.collectFile(at: "/path/to/file")
|
||||
print(buffer)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
このメソッドは、ファイル全体を一度にメモリに読み込む必要があります。メモリ使用量を制限するには、チャンクまたはストリーミング読み取りを使用してください。
|
||||
|
||||
## 書き込み {#write}
|
||||
|
||||
`writeFile`メソッドは、バッファをファイルに書き込むことをサポートしています。
|
||||
|
||||
```swift
|
||||
// バッファをファイルに書き込みます。
|
||||
req.fileio.writeFile(ByteBuffer(string: "Hello, world"), at: "/path/to/file")
|
||||
```
|
||||
|
||||
返されたfutureは、書き込みが完了したか、エラーが発生したときにシグナルを送ります。
|
||||
|
||||
## ミドルウェア {#middleware}
|
||||
|
||||
プロジェクトの_Public_フォルダから自動的にファイルを提供する方法の詳細については、[ミドルウェア → FileMiddleware](middleware.md#file-middleware)を参照してください。
|
||||
|
||||
## 高度な使い方 {#advanced}
|
||||
|
||||
VaporのAPIがサポートしていないケースでは、NIOの`NonBlockingFileIO`型を直接使用できます。
|
||||
|
||||
```swift
|
||||
// メインスレッド。
|
||||
let fileHandle = try await app.fileio.openFile(
|
||||
path: "/path/to/file",
|
||||
eventLoop: app.eventLoopGroup.next()
|
||||
).get()
|
||||
print(fileHandle)
|
||||
|
||||
// ルートハンドラ内。
|
||||
let fileHandle = try await req.application.fileio.openFile(
|
||||
path: "/path/to/file",
|
||||
eventLoop: req.eventLoop)
|
||||
print(fileHandle)
|
||||
```
|
||||
|
||||
詳細については、SwiftNIOの[APIリファレンス](https://swiftpackageindex.com/apple/swift-nio/main/documentation/nioposix/nonblockingfileio)をご覧ください。
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
# ミドルウェア {#middleware}
|
||||
|
||||
ミドルウェアは、クライアントとVaporのルートハンドラーの間にあるロジックチェーンです。これにより、受信リクエストがルートハンドラーに到達する前、および送信レスポンスがクライアントに送信される前に操作を実行できます。
|
||||
|
||||
## 設定 {#configuration}
|
||||
|
||||
ミドルウェアは、`configure(_:)`内で`app.middleware`を使用してグローバルに(すべてのルートに)登録できます。
|
||||
|
||||
```swift
|
||||
app.middleware.use(MyMiddleware())
|
||||
```
|
||||
|
||||
また、ルートグループを使用して個々のルートにミドルウェアを追加することもできます。
|
||||
|
||||
```swift
|
||||
let group = app.grouped(MyMiddleware())
|
||||
group.get("foo") { req in
|
||||
// このリクエストはMyMiddlewareを通過しています。
|
||||
}
|
||||
```
|
||||
|
||||
### 順序 {#order}
|
||||
|
||||
ミドルウェアが追加される順序は重要です。アプリケーションに入ってくるリクエストは、追加された順序でミドルウェアを通過します。アプリケーションから出ていくレスポンスは、逆の順序でミドルウェアを通過します。ルート固有のミドルウェアは常にアプリケーションミドルウェアの後に実行されます。以下の例を見てください:
|
||||
|
||||
```swift
|
||||
app.middleware.use(MiddlewareA())
|
||||
app.middleware.use(MiddlewareB())
|
||||
|
||||
app.group(MiddlewareC()) {
|
||||
$0.get("hello") { req in
|
||||
"Hello, middleware."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`GET /hello`へのリクエストは、以下の順序でミドルウェアを通過します:
|
||||
|
||||
```
|
||||
Request → A → B → C → Handler → C → B → A → Response
|
||||
```
|
||||
|
||||
ミドルウェアは先頭に追加することもできます。これは、Vaporが自動的に追加するデフォルトのミドルウェアの前にミドルウェアを追加したい場合に便利です:
|
||||
|
||||
```swift
|
||||
app.middleware.use(someMiddleware, at: .beginning)
|
||||
```
|
||||
|
||||
## ミドルウェアの作成 {#creating-a-middleware}
|
||||
|
||||
Vaporにはいくつかの便利なミドルウェアが付属していますが、アプリケーションの要件により独自のミドルウェアを作成する必要があるかもしれません。例えば、管理者でないユーザーがルートのグループにアクセスすることを防ぐミドルウェアを作成できます。
|
||||
|
||||
> コードを整理するために、`Sources/App`ディレクトリ内に`Middleware`フォルダを作成することをお勧めします
|
||||
|
||||
ミドルウェアは、Vaporの`Middleware`または`AsyncMiddleware`プロトコルに準拠する型です。レスポンダーチェーンに挿入され、リクエストがルートハンドラーに到達する前にアクセス・操作し、レスポンスが返される前にアクセス・操作できます。
|
||||
|
||||
上記の例を使用して、ユーザーが管理者でない場合にアクセスをブロックするミドルウェアを作成します:
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
struct EnsureAdminUserMiddleware: Middleware {
|
||||
func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture<Response> {
|
||||
guard let user = request.auth.get(User.self), user.role == .admin else {
|
||||
return request.eventLoop.future(error: Abort(.unauthorized))
|
||||
}
|
||||
return next.respond(to: request)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
または、`async`/`await`を使用している場合は次のように書けます:
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
struct EnsureAdminUserMiddleware: AsyncMiddleware {
|
||||
func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
|
||||
guard let user = request.auth.get(User.self), user.role == .admin else {
|
||||
throw Abort(.unauthorized)
|
||||
}
|
||||
return try await next.respond(to: request)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
カスタムヘッダーを追加するなど、レスポンスを変更したい場合も、ミドルウェアを使用できます。ミドルウェアは、レスポンダーチェーンからレスポンスを受け取るまで待機し、レスポンスを操作できます:
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
struct AddVersionHeaderMiddleware: Middleware {
|
||||
func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture<Response> {
|
||||
next.respond(to: request).map { response in
|
||||
response.headers.add(name: "My-App-Version", value: "v2.5.9")
|
||||
return response
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
または、`async`/`await`を使用している場合は次のように書けます:
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
struct AddVersionHeaderMiddleware: AsyncMiddleware {
|
||||
func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
|
||||
let response = try await next.respond(to: request)
|
||||
response.headers.add(name: "My-App-Version", value: "v2.5.9")
|
||||
return response
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ファイルミドルウェア {#file-middleware}
|
||||
|
||||
`FileMiddleware`は、プロジェクトのPublicフォルダからクライアントへのアセットの提供を可能にします。スタイルシートやビットマップ画像などの静的ファイルをここに含めることができます。
|
||||
|
||||
```swift
|
||||
let file = FileMiddleware(publicDirectory: app.directory.publicDirectory)
|
||||
app.middleware.use(file)
|
||||
```
|
||||
|
||||
`FileMiddleware`が登録されると、`Public/images/logo.png`のようなファイルはLeafテンプレートから`<img src="/images/logo.png"/>`としてリンクできます。
|
||||
|
||||
サーバーがiOSアプリなどのXcodeプロジェクトに含まれている場合は、代わりに次を使用してください:
|
||||
|
||||
```swift
|
||||
let file = try FileMiddleware(bundle: .main, publicDirectory: "Public")
|
||||
```
|
||||
|
||||
また、アプリケーションをビルドした後のリソース内でフォルダ構造を維持するために、XcodeでGroupsではなくFolder Referencesを使用してください。
|
||||
|
||||
## CORSミドルウェア {#cors-middleware}
|
||||
|
||||
Cross-origin resource sharing(CORS)は、Webページ上の制限されたリソースを、最初のリソースが提供されたドメイン以外の別のドメインからリクエストできるようにするメカニズムです。Vaporで構築されたREST APIは、最新のWebブラウザに安全にリクエストを返すためにCORSポリシーが必要です。
|
||||
|
||||
設定例は次のようになります:
|
||||
|
||||
```swift
|
||||
let corsConfiguration = CORSMiddleware.Configuration(
|
||||
allowedOrigin: .all,
|
||||
allowedMethods: [.GET, .POST, .PUT, .OPTIONS, .DELETE, .PATCH],
|
||||
allowedHeaders: [.accept, .authorization, .contentType, .origin, .xRequestedWith, .userAgent, .accessControlAllowOrigin]
|
||||
)
|
||||
let cors = CORSMiddleware(configuration: corsConfiguration)
|
||||
// corsミドルウェアは`at: .beginning`を使用してデフォルトのエラーミドルウェアの前に配置する必要があります
|
||||
app.middleware.use(cors, at: .beginning)
|
||||
```
|
||||
|
||||
スローされたエラーは即座にクライアントに返されるため、`CORSMiddleware`は`ErrorMiddleware`の前にリストされている必要があります。そうでない場合、HTTPエラーレスポンスはCORSヘッダーなしで返され、ブラウザで読み取ることができません。
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
# リクエスト {#request}
|
||||
|
||||
[`Request`](https://api.vapor.codes/vapor/documentation/vapor/request) オブジェクトは、すべての[ルートハンドラ](../basics/routing.md)に渡されます。
|
||||
|
||||
```swift
|
||||
app.get("hello", ":name") { req -> String in
|
||||
let name = req.parameters.get("name")!
|
||||
return "Hello, \(name)!"
|
||||
}
|
||||
```
|
||||
|
||||
これは、Vaporの他の機能への主要な窓口です。[リクエストボディ](../basics/content.md)、[クエリパラメータ](../basics/content.md#query)、[ロガー](../basics/logging.md)、[HTTPクライアント](../basics/client.md)、[Authenticator](../security/authentication.md)などのAPIが含まれています。リクエストを通じてこれらの機能にアクセスすることで、計算を適切なイベントループ上に保ち、テスト用にモック化することができます。拡張機能を使用して、独自の[サービス](../advanced/services.md)を`Request`に追加することもできます。
|
||||
|
||||
`Request`の完全なAPIドキュメントは[こちら](https://api.vapor.codes/vapor/documentation/vapor/request)で確認できます。
|
||||
|
||||
## アプリケーション {#application}
|
||||
|
||||
`Request.application`プロパティは、[`Application`](https://api.vapor.codes/vapor/documentation/vapor/application)への参照を保持しています。このオブジェクトには、アプリケーションのすべての設定とコア機能が含まれています。そのほとんどは、アプリケーションが完全に起動する前の`configure.swift`でのみ設定されるべきであり、低レベルAPIの多くはほとんどのアプリケーションでは必要ありません。最も便利なプロパティの1つは`Application.eventLoopGroup`で、新しい`EventLoop`が必要なプロセスのために`any()`メソッドを介して`EventLoop`を取得するために使用できます。また、[`Environment`](../basics/environment.md)も含まれています。
|
||||
|
||||
## ボディ {#body}
|
||||
|
||||
リクエストボディに`ByteBuffer`として直接アクセスしたい場合は、`Request.body.data`を使用できます。これは、リクエストボディからファイルへのデータのストリーミング(ただし、この場合はリクエストの[`fileio`](../advanced/files.md)プロパティを使用すべきです)や、別のHTTPクライアントへの転送に使用できます。
|
||||
|
||||
## クッキー {#cookies}
|
||||
|
||||
クッキーの最も便利な用途は組み込みの[セッション](../advanced/sessions.md#configuration)を経由することですが、`Request.cookies`を介してクッキーに直接アクセスすることもできます。
|
||||
|
||||
```swift
|
||||
app.get("my-cookie") { req -> String in
|
||||
guard let cookie = req.cookies["my-cookie"] else {
|
||||
throw Abort(.badRequest)
|
||||
}
|
||||
if let expiration = cookie.expires, expiration < Date() {
|
||||
throw Abort(.badRequest)
|
||||
}
|
||||
return cookie.string
|
||||
}
|
||||
```
|
||||
|
||||
## ヘッダー {#headers}
|
||||
|
||||
`HTTPHeaders`オブジェクトは`Request.headers`でアクセスできます。これには、リクエストとともに送信されたすべてのヘッダーが含まれています。例えば、`Content-Type`ヘッダーにアクセスするために使用できます。
|
||||
|
||||
```swift
|
||||
app.get("json") { req -> String in
|
||||
guard let contentType = req.headers.contentType, contentType == .json else {
|
||||
throw Abort(.badRequest)
|
||||
}
|
||||
return "JSON"
|
||||
}
|
||||
```
|
||||
|
||||
`HTTPHeaders`の詳細なドキュメントは[こちら](https://swiftpackageindex.com/apple/swift-nio/2.56.0/documentation/niohttp1/httpheaders)を参照してください。Vaporは、最もよく使用されるヘッダーの操作を簡単にするために、`HTTPHeaders`にいくつかの拡張機能も追加しています。リストは[こちら](https://api.vapor.codes/vapor/documentation/vapor/niohttp1/httpheaders#instance-properties)で確認できます。
|
||||
|
||||
## IPアドレス {#ip-address}
|
||||
|
||||
クライアントを表す`SocketAddress`は`Request.remoteAddress`を介してアクセスでき、ログ記録やレート制限のために文字列表現`Request.remoteAddress.ipAddress`を使用すると便利です。アプリケーションがリバースプロキシの背後にある場合、クライアントのIPアドレスを正確に表していない可能性があります。
|
||||
|
||||
```swift
|
||||
app.get("ip") { req -> String in
|
||||
return req.remoteAddress.ipAddress
|
||||
}
|
||||
```
|
||||
|
||||
`SocketAddress`の詳細なドキュメントは[こちら](https://swiftpackageindex.com/apple/swift-nio/2.56.0/documentation/niocore/socketaddress)を参照してください。
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
# Server
|
||||
|
||||
Vaporには[SwiftNIO](https://github.com/apple/swift-nio)上に構築された高性能で非同期のHTTPサーバーが含まれています。このサーバーはHTTP/1、HTTP/2、および[WebSockets](websockets.md)などのプロトコルアップグレードをサポートしています。サーバーはTLS(SSL)の有効化もサポートしています。
|
||||
|
||||
## 設定 {#configuration}
|
||||
|
||||
VaporのデフォルトHTTPサーバーは`app.http.server`を介して設定できます。
|
||||
|
||||
```swift
|
||||
// HTTP/2のみをサポート
|
||||
app.http.server.configuration.supportVersions = [.two]
|
||||
```
|
||||
|
||||
HTTPサーバーはいくつかの設定オプションをサポートしています。
|
||||
|
||||
### ホスト名 {#hostname}
|
||||
|
||||
ホスト名は、サーバーが新しい接続を受け入れるアドレスを制御します。デフォルトは`127.0.0.1`です。
|
||||
|
||||
```swift
|
||||
// カスタムホスト名を設定
|
||||
app.http.server.configuration.hostname = "dev.local"
|
||||
```
|
||||
|
||||
サーバー設定のホスト名は、`serve`コマンドに`--hostname`(`-H`)フラグを渡すか、`app.server.start(...)`に`hostname`パラメーターを渡すことでオーバーライドできます。
|
||||
|
||||
```sh
|
||||
# 設定されたホスト名をオーバーライド
|
||||
swift run App serve --hostname dev.local
|
||||
```
|
||||
|
||||
### ポート {#port}
|
||||
|
||||
ポートオプションは、指定されたアドレスでサーバーが新しい接続を受け入れるポートを制御します。デフォルトは`8080`です。
|
||||
|
||||
```swift
|
||||
// カスタムポートを設定
|
||||
app.http.server.configuration.port = 1337
|
||||
```
|
||||
|
||||
!!! info
|
||||
`1024`未満のポートにバインドするには`sudo`が必要な場合があります。`65535`を超えるポートはサポートされていません。
|
||||
|
||||
サーバー設定のポートは、`serve`コマンドに`--port`(`-p`)フラグを渡すか、`app.server.start(...)`に`port`パラメーターを渡すことでオーバーライドできます。
|
||||
|
||||
```sh
|
||||
# 設定されたポートをオーバーライド
|
||||
swift run App serve --port 1337
|
||||
```
|
||||
|
||||
### バックログ {#backlog}
|
||||
|
||||
`backlog`パラメーターは、保留中の接続のキューの最大長を定義します。デフォルトは`256`です。
|
||||
|
||||
```swift
|
||||
// カスタムバックログを設定
|
||||
app.http.server.configuration.backlog = 128
|
||||
```
|
||||
|
||||
### アドレスの再利用 {#reuse-address}
|
||||
|
||||
`reuseAddress`パラメーターは、ローカルアドレスの再利用を許可します。デフォルトは`true`です。
|
||||
|
||||
```swift
|
||||
// アドレスの再利用を無効化
|
||||
app.http.server.configuration.reuseAddress = false
|
||||
```
|
||||
|
||||
### TCP No Delay {#tcp-no-delay}
|
||||
|
||||
`tcpNoDelay`パラメーターを有効にすると、TCPパケットの遅延を最小限に抑えようとします。デフォルトは`true`です。
|
||||
|
||||
```swift
|
||||
// パケットの遅延を最小化
|
||||
app.http.server.configuration.tcpNoDelay = true
|
||||
```
|
||||
|
||||
### レスポンス圧縮 {#response-compression}
|
||||
|
||||
`responseCompression`パラメーターは、gzipを使用したHTTPレスポンスの圧縮を制御します。デフォルトは`.disabled`です。
|
||||
|
||||
```swift
|
||||
// HTTPレスポンス圧縮を有効化
|
||||
app.http.server.configuration.responseCompression = .enabled
|
||||
```
|
||||
|
||||
初期バッファ容量を指定するには、`initialByteBufferCapacity`パラメーターを使用します。
|
||||
|
||||
```swift
|
||||
.enabled(initialByteBufferCapacity: 1024)
|
||||
```
|
||||
|
||||
### リクエスト解凍 {#request-decompression}
|
||||
|
||||
`requestDecompression`パラメーターは、gzipを使用したHTTPリクエストの解凍を制御します。デフォルトは`.disabled`です。
|
||||
|
||||
```swift
|
||||
// HTTPリクエスト解凍を有効化
|
||||
app.http.server.configuration.requestDecompression = .enabled
|
||||
```
|
||||
|
||||
解凍制限を指定するには、`limit`パラメーターを使用します。デフォルトは`.ratio(10)`です。
|
||||
|
||||
```swift
|
||||
// 解凍サイズ制限なし
|
||||
.enabled(limit: .none)
|
||||
```
|
||||
|
||||
利用可能なオプション:
|
||||
|
||||
- `size`:バイト単位での最大解凍サイズ
|
||||
- `ratio`:圧縮バイトに対する比率としての最大解凍サイズ
|
||||
- `none`:サイズ制限なし
|
||||
|
||||
解凍サイズ制限を設定することで、悪意のある圧縮されたHTTPリクエストが大量のメモリを使用することを防ぐことができます。
|
||||
|
||||
### パイプライニング {#pipelining}
|
||||
|
||||
`supportPipelining`パラメーターは、HTTPリクエストとレスポンスのパイプライニングのサポートを有効にします。デフォルトは`false`です。
|
||||
|
||||
```swift
|
||||
// HTTPパイプライニングをサポート
|
||||
app.http.server.configuration.supportPipelining = true
|
||||
```
|
||||
|
||||
### バージョン {#versions}
|
||||
|
||||
`supportVersions`パラメーターは、サーバーが使用するHTTPバージョンを制御します。デフォルトでは、TLSが有効な場合、VaporはHTTP/1とHTTP/2の両方をサポートします。TLSが無効な場合はHTTP/1のみがサポートされます。
|
||||
|
||||
```swift
|
||||
// HTTP/1サポートを無効化
|
||||
app.http.server.configuration.supportVersions = [.two]
|
||||
```
|
||||
|
||||
### TLS {#tls}
|
||||
|
||||
`tlsConfiguration`パラメーターは、サーバーでTLS(SSL)が有効かどうかを制御します。デフォルトは`nil`です。
|
||||
|
||||
```swift
|
||||
// TLSを有効化
|
||||
app.http.server.configuration.tlsConfiguration = .makeServerConfiguration(
|
||||
certificateChain: try NIOSSLCertificate.fromPEMFile("/path/to/cert.pem").map { .certificate($0) },
|
||||
privateKey: .privateKey(try NIOSSLPrivateKey(file: "/path/to/key.pem", format: .pem))
|
||||
)
|
||||
```
|
||||
|
||||
この設定をコンパイルするには、設定ファイルの先頭に`import NIOSSL`を追加する必要があります。また、Package.swiftファイルにNIOSSLを依存関係として追加する必要がある場合もあります。
|
||||
|
||||
### 名前 {#name}
|
||||
|
||||
`serverName`パラメーターは、送信されるHTTPレスポンスの`Server`ヘッダーを制御します。デフォルトは`nil`です。
|
||||
|
||||
```swift
|
||||
// レスポンスに 'Server: vapor' ヘッダーを追加
|
||||
app.http.server.configuration.serverName = "vapor"
|
||||
```
|
||||
|
||||
## Serveコマンド {#serve-command}
|
||||
|
||||
Vaporのサーバーを起動するには、`serve`コマンドを使用します。他のコマンドが指定されていない場合、このコマンドはデフォルトで実行されます。
|
||||
|
||||
```swift
|
||||
swift run App serve
|
||||
```
|
||||
|
||||
`serve`コマンドは以下のパラメーターを受け入れます:
|
||||
|
||||
- `hostname` (`-H`):設定されたホスト名をオーバーライド
|
||||
- `port` (`-p`):設定されたポートをオーバーライド
|
||||
- `bind` (`-b`):`:`で結合された設定済みホスト名とポートをオーバーライド
|
||||
|
||||
`--bind`(`-b`)フラグを使用した例:
|
||||
|
||||
```swift
|
||||
swift run App serve -b 0.0.0.0:80
|
||||
```
|
||||
|
||||
詳細については`swift run App serve --help`を使用してください。
|
||||
|
||||
`serve`コマンドは、サーバーを正常にシャットダウンするために`SIGTERM`と`SIGINT`をリッスンします。`SIGINT`シグナルを送信するには`ctrl+c`(`^c`)を使用します。ログレベルが`debug`以下に設定されている場合、正常なシャットダウンのステータスに関する情報がログに記録されます。
|
||||
|
||||
## 手動起動 {#manual-start}
|
||||
|
||||
Vaporのサーバーは`app.server`を使用して手動で起動できます。
|
||||
|
||||
```swift
|
||||
// Vaporのサーバーを起動
|
||||
try app.server.start()
|
||||
// サーバーのシャットダウンをリクエスト
|
||||
app.server.shutdown()
|
||||
// サーバーのシャットダウンを待機
|
||||
try app.server.onShutdown.wait()
|
||||
```
|
||||
|
||||
## サーバー {#servers}
|
||||
|
||||
Vaporが使用するサーバーは設定可能です。デフォルトでは、組み込みのHTTPサーバーが使用されます。
|
||||
|
||||
```swift
|
||||
app.servers.use(.http)
|
||||
```
|
||||
|
||||
### カスタムサーバー {#custom-server}
|
||||
|
||||
Vaporのデフォルトのサーバーは、`Server`に準拠する任意の型で置き換えることができます。
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
final class MyServer: Server {
|
||||
...
|
||||
}
|
||||
|
||||
app.servers.use { app in
|
||||
MyServer()
|
||||
}
|
||||
```
|
||||
|
||||
カスタムサーバーは、先頭ドット構文のために`Application.Servers.Provider`を拡張できます。
|
||||
|
||||
```swift
|
||||
extension Application.Servers.Provider {
|
||||
static var myServer: Self {
|
||||
.init {
|
||||
$0.servers.use { app in
|
||||
MyServer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.servers.use(.myServer)
|
||||
```
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
# サービス {#services}
|
||||
|
||||
Vaporの`Application`と`Request`は、あなたのアプリケーションやサードパーティパッケージによって拡張できるように構築されています。これらの型に追加される新しい機能は、しばしばサービスと呼ばれます。
|
||||
|
||||
## 読み取り専用 {#read-only}
|
||||
|
||||
最もシンプルなタイプのサービスは読み取り専用です。これらのサービスは、applicationまたはrequestに追加される計算プロパティやメソッドで構成されます。
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
struct MyAPI {
|
||||
let client: Client
|
||||
|
||||
func foos() async throws -> [String] { ... }
|
||||
}
|
||||
|
||||
extension Request {
|
||||
var myAPI: MyAPI {
|
||||
.init(client: self.client)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
読み取り専用サービスは、この例の`client`のような既存のサービスに依存できます。拡張機能が追加されると、カスタムサービスはrequestの他のプロパティと同様に使用できます。
|
||||
|
||||
```swift
|
||||
req.myAPI.foos()
|
||||
```
|
||||
|
||||
## 書き込み可能 {#writable}
|
||||
|
||||
状態や設定が必要なサービスは、データの保存に`Application`と`Request`のストレージを活用できます。次の`MyConfiguration`構造体をアプリケーションに追加したいとしましょう。
|
||||
|
||||
```swift
|
||||
struct MyConfiguration {
|
||||
var apiKey: String
|
||||
}
|
||||
```
|
||||
|
||||
ストレージを使用するには、`StorageKey`を宣言する必要があります。
|
||||
|
||||
```swift
|
||||
struct MyConfigurationKey: StorageKey {
|
||||
typealias Value = MyConfiguration
|
||||
}
|
||||
```
|
||||
|
||||
これは、保存される型を指定する`Value`型エイリアスを持つ空の構造体です。空の型をキーとして使用することで、ストレージ値にアクセスできるコードを制御できます。その型がinternalまたはprivateの場合、あなたのコードのみがストレージ内の関連する値を変更できます。
|
||||
|
||||
最後に、`MyConfiguration`構造体を取得・設定するための`Application`への拡張を追加します。
|
||||
|
||||
```swift
|
||||
extension Application {
|
||||
var myConfiguration: MyConfiguration? {
|
||||
get {
|
||||
self.storage[MyConfigurationKey.self]
|
||||
}
|
||||
set {
|
||||
self.storage[MyConfigurationKey.self] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
拡張機能が追加されると、`myConfiguration`を`Application`の通常のプロパティのように使用できます。
|
||||
|
||||
```swift
|
||||
app.myConfiguration = .init(apiKey: ...)
|
||||
print(app.myConfiguration?.apiKey)
|
||||
```
|
||||
|
||||
## ライフサイクル {#lifecycle}
|
||||
|
||||
Vaporの`Application`では、ライフサイクルハンドラーを登録できます。これにより、起動やシャットダウンなどのイベントにフックできます。
|
||||
|
||||
```swift
|
||||
// 起動時にhelloを出力します。
|
||||
struct Hello: LifecycleHandler {
|
||||
// アプリケーション起動前に呼ばれます。
|
||||
func willBoot(_ app: Application) throws {
|
||||
app.logger.info("Hello!")
|
||||
}
|
||||
|
||||
// アプリケーション起動後に呼ばれます。
|
||||
func didBoot(_ app: Application) throws {
|
||||
app.logger.info("Server is running")
|
||||
}
|
||||
|
||||
// アプリケーションシャットダウン前に呼ばれます。
|
||||
func shutdown(_ app: Application) {
|
||||
app.logger.info("Goodbye!")
|
||||
}
|
||||
}
|
||||
|
||||
// ライフサイクルハンドラーを追加します。
|
||||
app.lifecycle.use(Hello())
|
||||
```
|
||||
|
||||
## ロック {#locks}
|
||||
|
||||
Vaporの`Application`には、ロックを使用してコードを同期するための便利な機能が含まれています。`LockKey`を宣言することで、コードへのアクセスを同期するための一意の共有ロックを取得できます。
|
||||
|
||||
```swift
|
||||
struct TestKey: LockKey { }
|
||||
|
||||
let test = app.locks.lock(for: TestKey.self)
|
||||
test.withLock {
|
||||
// 何か処理を行う。
|
||||
}
|
||||
```
|
||||
|
||||
同じ`LockKey`で`lock(for:)`を呼び出すたびに、同じロックが返されます。このメソッドはスレッドセーフです。
|
||||
|
||||
アプリケーション全体のロックには、`app.sync`を使用できます。
|
||||
|
||||
```swift
|
||||
app.sync.withLock {
|
||||
// 何か処理を行う。
|
||||
}
|
||||
```
|
||||
|
||||
## リクエスト {#request}
|
||||
|
||||
ルートハンドラーで使用されることを意図したサービスは、`Request`に追加する必要があります。リクエストサービスは、リクエストのロガーとイベントループを使用する必要があります。レスポンスがVaporに返されるときにアサーションが発生しないよう、リクエストが同じイベントループに留まることが重要です。
|
||||
|
||||
サービスが作業を行うためにリクエストのイベントループを離れる必要がある場合は、終了前にイベントループに戻るようにする必要があります。これは`EventLoopFuture`の`hop(to:)`を使用して行うことができます。
|
||||
|
||||
設定などのアプリケーションサービスへのアクセスが必要なリクエストサービスは、`req.application`を使用できます。ルートハンドラーからアプリケーションにアクセスする際は、スレッドセーフティを考慮することに注意してください。一般的に、リクエストでは読み取り操作のみを実行すべきです。書き込み操作はロックで保護する必要があります。
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
# セッション {#sessions}
|
||||
|
||||
セッションを使用すると、複数のリクエスト間でユーザーのデータを永続化できます。セッションは、新しいセッションが初期化されたときに、HTTPレスポンスと共に一意のCookieを作成して返すことで機能します。ブラウザは自動的にこのCookieを検出し、将来のリクエストに含めます。これにより、Vaporはリクエストハンドラで特定のユーザーのセッションを自動的に復元できます。
|
||||
|
||||
セッションは、WebブラウザにHTMLを直接提供するVaporで構築されたフロントエンドWebアプリケーションに最適です。APIの場合は、リクエスト間でユーザーデータを永続化するために、ステートレスな[トークンベース認証](../security/authentication.md)の使用をお勧めします。
|
||||
|
||||
## 設定 {#configuration}
|
||||
|
||||
ルートでセッションを使用するには、リクエストが`SessionsMiddleware`を通過する必要があります。これを実現する最も簡単な方法は、このミドルウェアをグローバルに追加することです。Cookieファクトリを宣言した後にこれを追加することをお勧めします。これは、Sessionsが構造体であるため、参照型ではなく値型だからです。値型であるため、`SessionsMiddleware`を使用する前に値を設定する必要があります。
|
||||
|
||||
```swift
|
||||
app.middleware.use(app.sessions.middleware)
|
||||
```
|
||||
|
||||
ルートのサブセットのみがセッションを利用する場合は、代わりに`SessionsMiddleware`をルートグループに追加できます。
|
||||
|
||||
```swift
|
||||
let sessions = app.grouped(app.sessions.middleware)
|
||||
```
|
||||
|
||||
セッションによって生成されるHTTP Cookieは、`app.sessions.configuration`を使用して設定できます。Cookie名を変更し、Cookie値を生成するためのカスタム関数を宣言できます。
|
||||
|
||||
```swift
|
||||
// Cookie名を"foo"に変更します。
|
||||
app.sessions.configuration.cookieName = "foo"
|
||||
|
||||
// Cookie値の作成を設定します。
|
||||
app.sessions.configuration.cookieFactory = { sessionID in
|
||||
.init(string: sessionID.string, isSecure: true)
|
||||
}
|
||||
|
||||
app.middleware.use(app.sessions.middleware)
|
||||
```
|
||||
|
||||
デフォルトでは、VaporはCookie名として`vapor_session`を使用します。
|
||||
|
||||
## ドライバー {#drivers}
|
||||
|
||||
セッションドライバーは、識別子によってセッションデータの保存と取得を担当します。`SessionDriver`プロトコルに準拠することで、カスタムドライバーを作成できます。
|
||||
|
||||
!!! warning
|
||||
セッションドライバーは、`app.sessions.middleware`をアプリケーションに追加する_前に_設定する必要があります。
|
||||
|
||||
### インメモリ {#in-memory}
|
||||
|
||||
Vaporはデフォルトでインメモリセッションを利用します。インメモリセッションは設定が不要で、アプリケーションの起動間で永続化されないため、テストに最適です。インメモリセッションを手動で有効にするには、`.memory`を使用します:
|
||||
|
||||
```swift
|
||||
app.sessions.use(.memory)
|
||||
```
|
||||
|
||||
本番環境での使用例については、データベースを使用してアプリの複数のインスタンス間でセッションを永続化および共有する他のセッションドライバーを確認してください。
|
||||
|
||||
### Fluent {#fluent}
|
||||
|
||||
Fluentには、アプリケーションのデータベースにセッションデータを保存するサポートが含まれています。このセクションでは、[Fluentを設定](../fluent/overview.md)し、データベースに接続できることを前提としています。最初のステップは、Fluentセッションドライバーを有効にすることです。
|
||||
|
||||
```swift
|
||||
import Fluent
|
||||
|
||||
app.sessions.use(.fluent)
|
||||
```
|
||||
|
||||
これにより、アプリケーションのデフォルトデータベースを使用するようにセッションが設定されます。特定のデータベースを指定するには、データベースの識別子を渡します。
|
||||
|
||||
```swift
|
||||
app.sessions.use(.fluent(.sqlite))
|
||||
```
|
||||
|
||||
最後に、`SessionRecord`のマイグレーションをデータベースのマイグレーションに追加します。これにより、`_fluent_sessions`スキーマにセッションデータを保存するためのデータベースが準備されます。
|
||||
|
||||
```swift
|
||||
app.migrations.add(SessionRecord.migration)
|
||||
```
|
||||
|
||||
新しいマイグレーションを追加した後、必ずアプリケーションのマイグレーションを実行してください。セッションはアプリケーションのデータベースに保存されるようになり、再起動間で永続化され、アプリの複数のインスタンス間で共有できます。
|
||||
|
||||
### Redis {#redis}
|
||||
|
||||
Redisは、設定されたRedisインスタンスにセッションデータを保存するサポートを提供します。このセクションでは、[Redisを設定](../redis/overview.md)し、Redisインスタンスにコマンドを送信できることを前提としています。
|
||||
|
||||
セッションにRedisを使用するには、アプリケーションを設定するときに選択します:
|
||||
|
||||
```swift
|
||||
import Redis
|
||||
|
||||
app.sessions.use(.redis)
|
||||
```
|
||||
|
||||
これにより、デフォルトの動作でRedisセッションドライバーを使用するようにセッションが設定されます。
|
||||
|
||||
!!! seealso
|
||||
RedisとSessionsの詳細については、[Redis → Sessions](../redis/sessions.md)を参照してください。
|
||||
|
||||
## セッションデータ {#session-data}
|
||||
|
||||
セッションが設定されたので、リクエスト間でデータを永続化する準備ができました。新しいセッションは、`req.session`にデータが追加されたときに自動的に初期化されます。以下の例のルートハンドラは、動的ルートパラメータを受け入れ、その値を`req.session.data`に追加します。
|
||||
|
||||
```swift
|
||||
app.get("set", ":value") { req -> HTTPStatus in
|
||||
req.session.data["name"] = req.parameters.get("value")
|
||||
return .ok
|
||||
}
|
||||
```
|
||||
|
||||
以下のリクエストを使用して、名前Vaporでセッションを初期化します。
|
||||
|
||||
```http
|
||||
GET /set/vapor HTTP/1.1
|
||||
content-length: 0
|
||||
```
|
||||
|
||||
以下のようなレスポンスを受け取るはずです:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
content-length: 0
|
||||
set-cookie: vapor-session=123; Expires=Fri, 10 Apr 2020 21:08:09 GMT; Path=/
|
||||
```
|
||||
|
||||
`req.session`にデータを追加した後、`set-cookie`ヘッダーがレスポンスに自動的に追加されていることに注意してください。このCookieを後続のリクエストに含めることで、セッションデータにアクセスできます。
|
||||
|
||||
セッションから名前の値にアクセスするための以下のルートハンドラを追加します。
|
||||
|
||||
```swift
|
||||
app.get("get") { req -> String in
|
||||
req.session.data["name"] ?? "n/a"
|
||||
}
|
||||
```
|
||||
|
||||
前のレスポンスからのCookie値を必ず渡しながら、以下のリクエストを使用してこのルートにアクセスします。
|
||||
|
||||
```http
|
||||
GET /get HTTP/1.1
|
||||
cookie: vapor-session=123
|
||||
```
|
||||
|
||||
レスポンスで名前Vaporが返されるのが確認できるはずです。必要に応じてセッションからデータを追加または削除できます。セッションデータは、HTTPレスポンスを返す前にセッションドライバーと自動的に同期されます。
|
||||
|
||||
セッションを終了するには、`req.session.destroy`を使用します。これにより、セッションドライバーからデータが削除され、セッションCookieが無効になります。
|
||||
|
||||
```swift
|
||||
app.get("del") { req -> HTTPStatus in
|
||||
req.session.destroy()
|
||||
return .ok
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
# テスト {#testing}
|
||||
|
||||
## VaporTesting {#vaportesting}
|
||||
|
||||
Vaporには`VaporTesting`というモジュールが含まれており、`Swift Testing`をベースとしたテストヘルパーを提供しています。これらのテストヘルパーを使用すると、Vaporアプリケーションにプログラムでテストリクエストを送信したり、HTTPサーバー経由で実行したりできます。
|
||||
|
||||
!!! note
|
||||
新しいプロジェクトやSwift並行処理を採用しているチームには、`XCTest`よりも`Swift Testing`を強く推奨します。
|
||||
|
||||
### はじめに {#getting-started}
|
||||
|
||||
`VaporTesting`モジュールを使用するには、パッケージのテストターゲットに追加されていることを確認してください。
|
||||
|
||||
```swift
|
||||
let package = Package(
|
||||
...
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/vapor/vapor.git", from: "4.110.1")
|
||||
],
|
||||
targets: [
|
||||
...
|
||||
.testTarget(name: "AppTests", dependencies: [
|
||||
.target(name: "App"),
|
||||
.product(name: "VaporTesting", package: "vapor"),
|
||||
])
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
対応するテストモジュールを使用することを確認してください。そうしないと、Vaporのテスト失敗が適切に報告されない可能性があります。
|
||||
|
||||
次に、テストファイルの先頭に`import VaporTesting`と`import Testing`を追加します。テストケースを記述するために`@Suite`名を持つ構造体を作成します。
|
||||
|
||||
```swift
|
||||
@testable import App
|
||||
import VaporTesting
|
||||
import Testing
|
||||
|
||||
@Suite("App Tests")
|
||||
struct AppTests {
|
||||
@Test("Test Stub")
|
||||
func stub() async throws {
|
||||
// ここでテストします。
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`@Test`でマークされた各関数は、アプリがテストされるときに自動的に実行されます。
|
||||
|
||||
テストがシリアル化された方法で実行されることを確実にするには(例:データベースでテストする場合)、テストスイート宣言に`.serialized`オプションを含めます:
|
||||
|
||||
```swift
|
||||
@Suite("App Tests with DB", .serialized)
|
||||
```
|
||||
|
||||
### テスト可能なアプリケーション {#testable-application}
|
||||
|
||||
テストのセットアップとティアダウンを効率化し標準化するために、プライベートメソッド関数`withApp`を定義します。このメソッドは`Application`インスタンスのライフサイクル管理をカプセル化し、各テストでアプリケーションが適切に初期化、設定、シャットダウンされることを保証します。
|
||||
|
||||
特に、起動時にアプリケーションが要求するスレッドを解放することが重要です。各単体テスト後にアプリで`asyncShutdown()`を呼び出さない場合、`Application`の新しいインスタンスのスレッドを割り当てる際に、precondition失敗でテストスイートがクラッシュする可能性があります。
|
||||
|
||||
```swift
|
||||
private func withApp(_ test: (Application) async throws -> ()) async throws {
|
||||
let app = try await Application.make(.testing)
|
||||
do {
|
||||
try await configure(app)
|
||||
try await test(app)
|
||||
}
|
||||
catch {
|
||||
try await app.asyncShutdown()
|
||||
throw error
|
||||
}
|
||||
try await app.asyncShutdown()
|
||||
}
|
||||
```
|
||||
|
||||
設定を適用するために、`Application`をパッケージの`configure(_:)`メソッドに渡します。その後、`test()`メソッドを呼び出してアプリケーションをテストします。テスト専用の設定も適用できます。
|
||||
|
||||
#### リクエストの送信 {#send-request}
|
||||
|
||||
アプリケーションにテストリクエストを送信するには、`withApp`プライベートメソッドを使用し、その中で`app.testing().test()`メソッドを使用します:
|
||||
|
||||
```swift
|
||||
@Test("Test Hello World Route")
|
||||
func helloWorld() async throws {
|
||||
try await withApp { app in
|
||||
try await app.testing().test(.GET, "hello") { res async in
|
||||
#expect(res.status == .ok)
|
||||
#expect(res.body.string == "Hello, world!")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
最初の2つのパラメータは、HTTPメソッドとリクエストするURLです。末尾のクロージャは、`#expect`マクロを使用して検証できるHTTPレスポンスを受け取ります。
|
||||
|
||||
より複雑なリクエストの場合、`beforeRequest`クロージャを提供してヘッダーを変更したり、コンテンツをエンコードしたりできます。Vaporの[Content API](../basics/content.md)は、テストリクエストとレスポンスの両方で利用できます。
|
||||
|
||||
```swift
|
||||
let newDTO = TodoDTO(id: nil, title: "test")
|
||||
|
||||
try await app.testing().test(.POST, "todos", beforeRequest: { req in
|
||||
try req.content.encode(newDTO)
|
||||
}, afterResponse: { res async throws in
|
||||
#expect(res.status == .ok)
|
||||
let models = try await Todo.query(on: app.db).all()
|
||||
#expect(models.map({ $0.toDTO().title }) == [newDTO.title])
|
||||
})
|
||||
```
|
||||
|
||||
#### テストメソッド {#testing-method}
|
||||
|
||||
Vaporのテスト用APIは、プログラムでのテストリクエスト送信と、ライブHTTPサーバー経由での送信の両方をサポートしています。`testing`メソッドを通じて使用したい方法を指定できます。
|
||||
|
||||
```swift
|
||||
// プログラムによるテストを使用。
|
||||
app.testing(method: .inMemory).test(...)
|
||||
|
||||
// ライブHTTPサーバー経由でテストを実行。
|
||||
app.testing(method: .running).test(...)
|
||||
```
|
||||
|
||||
デフォルトでは`inMemory`オプションが使用されます。
|
||||
|
||||
`running`オプションは、使用する特定のポートを渡すことをサポートしています。デフォルトでは`8080`が使用されます。
|
||||
|
||||
```swift
|
||||
app.testing(method: .running(port: 8123)).test(...)
|
||||
```
|
||||
|
||||
#### データベース統合テスト {#database-integration-tests}
|
||||
|
||||
テスト中にライブデータベースが使用されないように、テスト専用にデータベースを設定します。
|
||||
|
||||
```swift
|
||||
app.databases.use(.sqlite(.memory), as: .sqlite)
|
||||
```
|
||||
|
||||
その後、`autoMigrate()`と`autoRevert()`を使用してテスト中のデータベーススキーマとデータライフサイクルを管理することで、テストを強化できます:
|
||||
|
||||
これらのメソッドを組み合わせることで、各テストが新しく一貫したデータベース状態で開始されることを保証し、テストをより信頼性の高いものにし、残存データによる偽陽性や偽陰性の可能性を減らすことができます。
|
||||
|
||||
更新された設定を含む`withApp`関数は次のようになります:
|
||||
|
||||
```swift
|
||||
private func withApp(_ test: (Application) async throws -> ()) async throws {
|
||||
let app = try await Application.make(.testing)
|
||||
app.databases.use(.sqlite(.memory), as: .sqlite)
|
||||
do {
|
||||
try await configure(app)
|
||||
try await app.autoMigrate()
|
||||
try await test(app)
|
||||
try await app.autoRevert()
|
||||
}
|
||||
catch {
|
||||
try? await app.autoRevert()
|
||||
try await app.asyncShutdown()
|
||||
throw error
|
||||
}
|
||||
try await app.asyncShutdown()
|
||||
}
|
||||
```
|
||||
|
||||
## XCTVapor {#xctvapor}
|
||||
|
||||
Vaporには`XCTVapor`というモジュールが含まれており、`XCTest`をベースとしたテストヘルパーを提供しています。これらのテストヘルパーを使用すると、Vaporアプリケーションにプログラムでテストリクエストを送信したり、HTTPサーバー経由で実行したりできます。
|
||||
|
||||
### はじめに {#getting-started_1}
|
||||
|
||||
`XCTVapor`モジュールを使用するには、パッケージのテストターゲットに追加されていることを確認してください。
|
||||
|
||||
```swift
|
||||
let package = Package(
|
||||
...
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0")
|
||||
],
|
||||
targets: [
|
||||
...
|
||||
.testTarget(name: "AppTests", dependencies: [
|
||||
.target(name: "App"),
|
||||
.product(name: "XCTVapor", package: "vapor"),
|
||||
])
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
次に、テストファイルの先頭に`import XCTVapor`を追加します。テストケースを記述するために`XCTestCase`を拡張するクラスを作成します。
|
||||
|
||||
```swift
|
||||
import XCTVapor
|
||||
|
||||
final class MyTests: XCTestCase {
|
||||
func testStub() throws {
|
||||
// ここでテストします。
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`test`で始まる各関数は、アプリがテストされるときに自動的に実行されます。
|
||||
|
||||
### テスト可能なアプリケーション {#testable-application_1}
|
||||
|
||||
`.testing`環境を使用して`Application`のインスタンスを初期化します。このアプリケーションがdeinitializeされる前に`app.shutdown()`を呼び出す必要があります。
|
||||
|
||||
シャットダウンは、アプリが要求したリソースの解放を助けるために必要です。特に、起動時にアプリケーションが要求するスレッドを解放することが重要です。各単体テスト後にアプリで`shutdown()`を呼び出さない場合、`Application`の新しいインスタンスのスレッドを割り当てる際に、precondition失敗でテストスイートがクラッシュする可能性があります。
|
||||
|
||||
```swift
|
||||
let app = Application(.testing)
|
||||
defer { app.shutdown() }
|
||||
try configure(app)
|
||||
```
|
||||
|
||||
設定を適用するために、`Application`をパッケージの`configure(_:)`メソッドに渡します。テスト専用の設定は後で適用できます。
|
||||
|
||||
#### リクエストの送信 {#send-request_1}
|
||||
|
||||
アプリケーションにテストリクエストを送信するには、`test`メソッドを使用します。
|
||||
|
||||
```swift
|
||||
try app.test(.GET, "hello") { res in
|
||||
XCTAssertEqual(res.status, .ok)
|
||||
XCTAssertEqual(res.body.string, "Hello, world!")
|
||||
}
|
||||
```
|
||||
|
||||
最初の2つのパラメータは、HTTPメソッドとリクエストするURLです。末尾のクロージャは、`XCTAssert`メソッドを使用して検証できるHTTPレスポンスを受け取ります。
|
||||
|
||||
より複雑なリクエストの場合、`beforeRequest`クロージャを提供してヘッダーを変更したり、コンテンツをエンコードしたりできます。Vaporの[Content API](../basics/content.md)は、テストリクエストとレスポンスの両方で利用できます。
|
||||
|
||||
```swift
|
||||
try app.test(.POST, "todos", beforeRequest: { req in
|
||||
try req.content.encode(["title": "Test"])
|
||||
}, afterResponse: { res in
|
||||
XCTAssertEqual(res.status, .created)
|
||||
let todo = try res.content.decode(Todo.self)
|
||||
XCTAssertEqual(todo.title, "Test")
|
||||
})
|
||||
```
|
||||
|
||||
#### テスト可能なメソッド {#testable-method}
|
||||
|
||||
Vaporのテスト用APIは、プログラムでのテストリクエスト送信と、ライブHTTPサーバー経由での送信の両方をサポートしています。`testable`メソッドを使用して、使用したい方法を指定できます。
|
||||
|
||||
```swift
|
||||
// プログラムによるテストを使用。
|
||||
app.testable(method: .inMemory).test(...)
|
||||
|
||||
// ライブHTTPサーバー経由でテストを実行。
|
||||
app.testable(method: .running).test(...)
|
||||
```
|
||||
|
||||
デフォルトでは`inMemory`オプションが使用されます。
|
||||
|
||||
`running`オプションは、使用する特定のポートを渡すことをサポートしています。デフォルトでは`8080`が使用されます。
|
||||
|
||||
```swift
|
||||
.running(port: 8123)
|
||||
```
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
# トレーシング {#tracing}
|
||||
|
||||
トレーシングは、分散システムの監視とデバッグのための強力なツールです。Vaporのトレーシング APIを使用すると、開発者はリクエストのライフサイクルを簡単に追跡し、メタデータを伝播し、OpenTelemetryなどの人気のあるバックエンドと統合できます。
|
||||
|
||||
Vaporのトレーシング APIは[swift-distributed-tracing](https://github.com/apple/swift-distributed-tracing)の上に構築されているため、swift-distributed-tracingのすべての[バックエンド実装](https://github.com/apple/swift-distributed-tracing/blob/main/README.md#tracing-backends)と互換性があります。
|
||||
|
||||
Swiftでのトレーシングとスパンに馴染みがない場合は、[OpenTelemetryトレースドキュメント](https://opentelemetry.io/ja/docs/concepts/signals/traces/)と[swift-distributed-tracingドキュメント](https://swiftpackageindex.com/apple/swift-distributed-tracing/main/documentation/tracing)を確認してください。
|
||||
|
||||
## TracingMiddleware {#tracingmiddleware}
|
||||
|
||||
各リクエストに対して完全に注釈付きのスパンを自動的に作成するには、アプリケーションに`TracingMiddleware`を追加します。
|
||||
|
||||
```swift
|
||||
app.middleware.use(TracingMiddleware())
|
||||
```
|
||||
|
||||
正確なスパン測定を取得し、トレーシング識別子が他のサービスに正しく渡されるようにするには、このミドルウェアを他のミドルウェアの前に追加してください。
|
||||
|
||||
## スパンの追加 {#adding-spans}
|
||||
|
||||
ルートハンドラーにスパンを追加する場合、それらをトップレベルのリクエストスパンに関連付けることが理想的です。これは「スパン伝播」と呼ばれ、自動または手動の2つの方法で処理できます。
|
||||
|
||||
### 自動伝播 {#automatic-propagation}
|
||||
|
||||
Vaporは、ミドルウェアとルートコールバック間でスパンを自動的に伝播する機能をサポートしています。これを行うには、設定時に`Application.traceAutoPropagation`プロパティをtrueに設定します。
|
||||
|
||||
```swift
|
||||
app.traceAutoPropagation = true
|
||||
```
|
||||
|
||||
!!! note
|
||||
自動伝播を有効にすると、スパンが作成されるかどうかに関係なく、すべてのルートハンドラーでリクエストスパンメタデータを復元する必要があるため、最小限のトレーシングニーズを持つ高スループットAPIではパフォーマンスが低下する可能性があります。
|
||||
|
||||
その後、通常の分散トレーシング構文を使用してルートクロージャでスパンを作成できます。
|
||||
|
||||
```swift
|
||||
app.get("fetchAndProcess") { req in
|
||||
let result = try await fetch()
|
||||
return try await withSpan("getNameParameter") { _ in
|
||||
try await process(result)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 手動伝播 {#manual-propagation}
|
||||
|
||||
自動伝播のパフォーマンスへの影響を避けるために、必要に応じて手動でスパンメタデータを復元できます。`TracingMiddleware`は自動的に`Request.serviceContext`プロパティを設定し、これを`withSpan`の`context`パラメータで直接使用できます。
|
||||
|
||||
```swift
|
||||
app.get("fetchAndProcess") { req in
|
||||
let result = try await fetch()
|
||||
return try await withSpan("getNameParameter", context: req.serviceContext) { _ in
|
||||
try await process(result)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
スパンを作成せずにスパンメタデータを復元するには、`ServiceContext.withValue`を使用します。これは、ダウンストリームの非同期ライブラリが独自のトレーシングスパンを発行し、それらが親リクエストスパンの下にネストされるべきであることがわかっている場合に有用です。
|
||||
|
||||
```swift
|
||||
app.get("fetchAndProcess") { req in
|
||||
try await ServiceContext.withValue(req.serviceContext) {
|
||||
try await fetch()
|
||||
return try await process(result)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## NIOに関する考慮事項 {#nio-considerations}
|
||||
|
||||
`swift-distributed-tracing`は[`TaskLocalプロパティ`](https://developer.apple.com/documentation/swift/tasklocal)を使用して伝播するため、スパンが正しくリンクされるようにするには、`NIO EventLoopFuture`の境界を越えるたびに手動でコンテキストを再復元する必要があります。**これは自動伝播が有効かどうかに関係なく必要です**。
|
||||
|
||||
```swift
|
||||
app.get("fetchAndProcessNIO") { req in
|
||||
withSpan("fetch", context: req.serviceContext) { span in
|
||||
fetchSomething().map { result in
|
||||
withSpan("process", context: span.context) { _ in
|
||||
process(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
# WebSockets
|
||||
|
||||
[WebSockets](https://ja.wikipedia.org/wiki/WebSocket)は、クライアントとサーバー間の双方向通信を可能にします。リクエストとレスポンスのパターンを持つHTTPとは異なり、WebSocketのピアは任意の数のメッセージを双方向に送信できます。VaporのWebSocket APIを使用すると、メッセージを非同期に処理するクライアントとサーバーの両方を作成できます。
|
||||
|
||||
## サーバー {#server}
|
||||
|
||||
WebSocketエンドポイントは、Routing APIを使用して既存のVaporアプリケーションに追加できます。`get`や`post`を使用するのと同じように`webSocket`メソッドを使用します。
|
||||
|
||||
```swift
|
||||
app.webSocket("echo") { req, ws in
|
||||
// 接続されたWebSocket
|
||||
print(ws)
|
||||
}
|
||||
```
|
||||
|
||||
WebSocketルートは、通常のルートと同様にグループ化し、ミドルウェアで保護できます。
|
||||
|
||||
WebSocketハンドラーは、受信HTTPリクエストを受け入れるだけでなく、新しく確立されたWebSocket接続も受け入れます。このWebSocketを使用してメッセージを送受信する方法については、以下を参照してください。
|
||||
|
||||
## クライアント {#client}
|
||||
|
||||
リモートのWebSocketエンドポイントに接続するには、`WebSocket.connect`を使用します。
|
||||
|
||||
```swift
|
||||
WebSocket.connect(to: "ws://echo.websocket.org", on: eventLoop) { ws in
|
||||
// 接続されたWebSocket
|
||||
print(ws)
|
||||
}
|
||||
```
|
||||
|
||||
`connect`メソッドは、接続が確立されたときに完了するfutureを返します。接続されると、新しく接続されたWebSocketで提供されたクロージャが呼び出されます。このWebSocketを使用してメッセージを送受信する方法については、以下を参照してください。
|
||||
|
||||
## メッセージ {#messages}
|
||||
|
||||
`WebSocket`クラスには、メッセージの送受信やクローズなどのイベントのリスニングのためのメソッドがあります。WebSocketは、テキストとバイナリの2つのプロトコルでデータを送信できます。テキストメッセージはUTF-8文字列として解釈され、バイナリデータはバイト配列として解釈されます。
|
||||
|
||||
### 送信 {#sending}
|
||||
|
||||
メッセージはWebSocketの`send`メソッドを使用して送信できます。
|
||||
|
||||
```swift
|
||||
ws.send("Hello, world")
|
||||
```
|
||||
|
||||
このメソッドに`String`を渡すと、テキストメッセージが送信されます。`[UInt8]`を渡すことでバイナリメッセージを送信できます。
|
||||
|
||||
```swift
|
||||
ws.send([1, 2, 3])
|
||||
```
|
||||
|
||||
メッセージの送信は非同期です。sendメソッドに`EventLoopPromise`を提供して、メッセージの送信が完了したか失敗したかを通知できます。
|
||||
|
||||
```swift
|
||||
let promise = eventLoop.makePromise(of: Void.self)
|
||||
ws.send(..., promise: promise)
|
||||
promise.futureResult.whenComplete { result in
|
||||
// 送信に成功または失敗
|
||||
}
|
||||
```
|
||||
|
||||
`async`/`await`を使用している場合は、`await`を使用して非同期操作の完了を待つことができます。
|
||||
|
||||
```swift
|
||||
try await ws.send(...)
|
||||
```
|
||||
|
||||
### 受信 {#receiving}
|
||||
|
||||
受信メッセージは`onText`と`onBinary`コールバックで処理されます。
|
||||
|
||||
```swift
|
||||
ws.onText { ws, text in
|
||||
// このWebSocketが受信した文字列
|
||||
print(text)
|
||||
}
|
||||
|
||||
ws.onBinary { ws, binary in
|
||||
// このWebSocketが受信した[UInt8]
|
||||
print(binary)
|
||||
}
|
||||
```
|
||||
|
||||
参照サイクルを防ぐため、WebSocket自体がこれらのコールバックの最初のパラメータとして提供されます。データを受信した後、WebSocketに対してアクションを実行するには、この参照を使用します。例えば、返信を送信する場合:
|
||||
|
||||
```swift
|
||||
// 受信したメッセージをエコーバック
|
||||
ws.onText { ws, text in
|
||||
ws.send(text)
|
||||
}
|
||||
```
|
||||
|
||||
## クローズ {#closing}
|
||||
|
||||
WebSocketを閉じるには、`close`メソッドを呼び出します。
|
||||
|
||||
```swift
|
||||
ws.close()
|
||||
```
|
||||
|
||||
このメソッドは、WebSocketが閉じられたときに完了するfutureを返します。`send`と同様に、このメソッドにpromiseを渡すこともできます。
|
||||
|
||||
```swift
|
||||
ws.close(promise: nil)
|
||||
```
|
||||
|
||||
または、`async`/`await`を使用している場合は`await`できます:
|
||||
|
||||
```swift
|
||||
try await ws.close()
|
||||
```
|
||||
|
||||
ピアが接続を閉じたときに通知を受けるには、`onClose`を使用します。このfutureは、クライアントまたはサーバーがWebSocketを閉じたときに完了します。
|
||||
|
||||
```swift
|
||||
ws.onClose.whenComplete { result in
|
||||
// クローズに成功または失敗
|
||||
}
|
||||
```
|
||||
|
||||
`closeCode`プロパティは、WebSocketが閉じられたときに設定されます。これを使用して、ピアが接続を閉じた理由を判断できます。
|
||||
|
||||
## Ping / Pong {#ping-pong}
|
||||
|
||||
PingとPongメッセージは、WebSocket接続を維持するためにクライアントとサーバーによって自動的に送信されます。アプリケーションは`onPing`と`onPong`コールバックを使用してこれらのイベントをリッスンできます。
|
||||
|
||||
```swift
|
||||
ws.onPing { ws in
|
||||
// Pingを受信
|
||||
}
|
||||
|
||||
ws.onPong { ws in
|
||||
// Pongを受信
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
# Vaporへの貢献 {#contributing-to-vapor}
|
||||
|
||||
Vaporはコミュニティ主導のプロジェクトであり、コミュニティメンバーからの貢献がVaporの開発の大きな部分を占めています。このガイドは、貢献プロセスを理解し、Vaporで最初のコミットを行うのに役立ちます!
|
||||
|
||||
どんな貢献も有用です!タイポの修正のような小さなことでも、Vaporを使用する人々にとって大きな違いをもたらします。
|
||||
|
||||
## 行動規範 {#code-of-conduct}
|
||||
|
||||
VaporはSwiftの行動規範を採用しており、[https://www.swift.org/code-of-conduct/](https://www.swift.org/code-of-conduct/)で確認できます。すべての貢献者は行動規範に従うことが期待されています。
|
||||
|
||||
## 何に取り組むか {#what-to-work-on}
|
||||
|
||||
何に取り組むかを決めることは、オープンソースを始める際の大きなハードルになることがあります!通常、最も良いのは自分が見つけた問題や欲しい機能に取り組むことです。しかし、Vaporには貢献を助けるための便利なものがあります。
|
||||
|
||||
### セキュリティの問題 {#security-issues}
|
||||
|
||||
セキュリティの問題を発見し、報告または修正を手伝いたい場合は、イシューを立てたりプルリクエストを作成したり**しないでください**。脆弱性を修正が利用可能になるまで公開しないよう、セキュリティの問題には別のプロセスがあります。security@vapor.codesにメールするか、詳細については[こちら](https://github.com/vapor/.github/blob/main/SECURITY.md)をご覧ください。
|
||||
|
||||
### 小さな問題 {#small-issues}
|
||||
|
||||
小さな問題、バグ、またはタイポを見つけた場合は、遠慮なくプルリクエストを作成して修正してください。いずれかのリポジトリでオープンなイシューを解決する場合は、サイドバーでプルリクエストにリンクして、プルリクエストがマージされたときにイシューが自動的にクローズされるようにできます。
|
||||
|
||||

|
||||
|
||||
### 新機能 {#new-features}
|
||||
|
||||
新機能や大量のコードを変更するバグ修正のような大きな変更を提案したい場合は、まずイシューを開くか、Discordの`#development`チャンネルに投稿してください。これにより、適用する必要があるコンテキストがあるかもしれませんし、ヒントを提供できるため、変更について議論できます。機能が私たちの計画に合わない場合、時間を無駄にしてほしくありません!
|
||||
|
||||
### Vaporのボード {#vapors-boards}
|
||||
|
||||
貢献したいけれど何に取り組むかのアイデアがない場合、それは素晴らしいことです!Vaporには役立ついくつかのボードがあります。Vaporには積極的に開発されている約40のリポジトリがあり、それらすべてを見て何か取り組むものを見つけるのは実用的ではないため、ボードを使用してこれらを集約しています。
|
||||
|
||||
最初のボードは[good first issueボード](https://github.com/orgs/vapor/projects/14)です。VaporのGitHub組織内で`good first issue`タグが付けられたイシューは、見つけやすいようにボードに追加されます。これらは、コードの経験をあまり必要としないため、Vaporに比較的新しい人が取り組むのに良いと思われるイシューです。
|
||||
|
||||
2番目のボードは[help wantedボード](https://github.com/orgs/vapor/projects/13)です。これは`help wanted`ラベルが付いたイシューを取り込みます。これらは修正するのに良いイシューですが、コアチームは現在他の優先事項があります。これらのイシューは`good first issue`とマークされていない場合、通常もう少し知識が必要ですが、楽しいプロジェクトになる可能性があります!
|
||||
|
||||
### 翻訳 {#translations}
|
||||
|
||||
貢献が非常に価値のある最後の分野はドキュメントです。ドキュメントには複数の言語の翻訳がありますが、すべてのページが翻訳されているわけではなく、サポートしたい言語がまだたくさんあります!新しい言語の貢献や更新に興味がある場合は、[docs README](https://github.com/vapor/docs#translating)を参照するか、Discordの`#documentation`チャンネルで連絡してください。
|
||||
|
||||
## 貢献プロセス {#contributing-process}
|
||||
|
||||
オープンソースプロジェクトに取り組んだことがない場合、実際に貢献する手順は混乱する可能性がありますが、実際にはとても簡単です。
|
||||
|
||||
まず、Vaporまたは作業したいリポジトリをフォークします。これはGitHub UIで行うことができ、GitHubには[これを行う方法に関する優れたドキュメント](https://docs.github.com/ja/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo)があります。
|
||||
|
||||
その後、通常のコミットとプッシュのプロセスで、フォークで変更を加えることができます。修正を提出する準備ができたら、VaporのリポジトリにPRを作成できます。ここでも、GitHubには[これを行う方法に関する優れたドキュメント](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork)があります。
|
||||
|
||||
## プルリクエストの提出 {#submitting-a-pull-request}
|
||||
|
||||
プルリクエストを提出する際には、確認すべきことがいくつかあります:
|
||||
|
||||
* すべてのテストがパスすること
|
||||
* 新しい動作やバグ修正のための新しいテストが追加されていること
|
||||
* 新しいパブリックAPIがドキュメント化されていること。APIドキュメントにはDocCを使用しています。
|
||||
|
||||
Vaporは多くのタスクに必要な作業量を減らすために自動化を使用しています。プルリクエストでは、[Vapor Bot](https://github.com/VaporBot)を使用して、プルリクエストがマージされたときにリリースを生成します。プルリクエストの本文とタイトルはリリースノートの生成に使用されるため、それらが意味をなし、リリースノートで期待される内容をカバーしていることを確認してください。[Vaporの貢献ガイドライン](https://github.com/vapor/vapor/blob/main/.github/contributing.md#release-title)に詳細があります。
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
# DigitalOceanへのデプロイ {#deploying-to-digitalocean}
|
||||
|
||||
このガイドでは、シンプルなHello, world Vaporアプリケーションを[Droplet](https://www.digitalocean.com/products/droplets/)にデプロイする方法を説明します。このガイドに従うには、請求設定が完了した[DigitalOcean](https://www.digitalocean.com)アカウントが必要です。
|
||||
|
||||
## サーバーの作成 {#create-server}
|
||||
|
||||
まずはLinuxサーバーにSwiftをインストールしましょう。作成メニューを使用して新しいDropletを作成します。
|
||||
|
||||

|
||||
|
||||
ディストリビューションでUbuntu 22.04 LTSを選択します。以下のガイドではこのバージョンを例として使用します。
|
||||
|
||||

|
||||
|
||||
!!! note
|
||||
Swiftがサポートするバージョンの任意のLinuxディストリビューションを選択できます。公式にサポートされているオペレーティングシステムは[Swift Releases](https://swift.org/download/#releases)ページで確認できます。
|
||||
|
||||
ディストリビューションを選択した後、お好みのプランとデータセンターのリージョンを選択します。次に、作成後にサーバーにアクセスするためのSSHキーを設定します。最後に、Dropletを作成をクリックして、新しいサーバーが起動するのを待ちます。
|
||||
|
||||
新しいサーバーの準備ができたら、DropletのIPアドレスにカーソルを合わせてコピーをクリックします。
|
||||
|
||||

|
||||
|
||||
## 初期設定 {#initial-setup}
|
||||
|
||||
ターミナルを開き、SSHを使用してrootとしてサーバーに接続します。
|
||||
|
||||
```sh
|
||||
ssh root@your_server_ip
|
||||
```
|
||||
|
||||
DigitalOceanには[Ubuntu 22.04の初期サーバー設定](https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-22-04)に関する詳細なガイドがあります。このガイドでは基本的な内容を簡単に説明します。
|
||||
|
||||
### ファイアウォールの設定 {#configure-firewall}
|
||||
|
||||
OpenSSHをファイアウォール経由で許可し、有効にします。
|
||||
|
||||
```sh
|
||||
ufw allow OpenSSH
|
||||
ufw enable
|
||||
```
|
||||
|
||||
### ユーザーの追加 {#add-user}
|
||||
|
||||
`root`以外の新しいユーザーを作成します。このガイドでは新しいユーザーを`vapor`と呼びます。
|
||||
|
||||
```sh
|
||||
adduser vapor
|
||||
```
|
||||
|
||||
新しく作成したユーザーが`sudo`を使用できるようにします。
|
||||
|
||||
```sh
|
||||
usermod -aG sudo vapor
|
||||
```
|
||||
|
||||
rootユーザーの認証済みSSHキーを新しく作成したユーザーにコピーします。これにより、新しいユーザーとしてSSH接続できるようになります。
|
||||
|
||||
```sh
|
||||
rsync --archive --chown=vapor:vapor ~/.ssh /home/vapor
|
||||
```
|
||||
|
||||
最後に、現在のSSHセッションを終了し、新しく作成したユーザーとしてログインします。
|
||||
|
||||
```sh
|
||||
exit
|
||||
ssh vapor@your_server_ip
|
||||
```
|
||||
|
||||
## Swiftのインストール {#install-swift}
|
||||
|
||||
新しいUbuntuサーバーを作成し、非rootユーザーとしてログインしたので、Swiftをインストールできます。
|
||||
|
||||
### Swiftly CLIツールを使用した自動インストール(推奨) {#automated-installation-using-swiftly-cli-tool-recommended}
|
||||
|
||||
[Swiftlyウェブサイト](https://swiftlang.github.io/swiftly/)にアクセスして、LinuxでSwiftlyとSwiftをインストールする方法の手順を確認してください。その後、次のコマンドでSwiftをインストールします:
|
||||
|
||||
#### 基本的な使い方 {#basic-usage}
|
||||
|
||||
```sh
|
||||
$ swiftly install latest
|
||||
|
||||
Fetching the latest stable Swift release...
|
||||
Installing Swift 5.9.1
|
||||
Downloaded 488.5 MiB of 488.5 MiB
|
||||
Extracting toolchain...
|
||||
Swift 5.9.1 installed successfully!
|
||||
|
||||
$ swift --version
|
||||
|
||||
Swift version 5.9.1 (swift-5.9.1-RELEASE)
|
||||
Target: x86_64-unknown-linux-gnu
|
||||
```
|
||||
|
||||
## Vapor Toolboxを使用したVaporのインストール {#install-vapor-using-the-vapor-toolbox}
|
||||
|
||||
Swiftがインストールされたので、Vapor Toolboxを使用してVaporをインストールしましょう。toolboxをソースからビルドする必要があります。GitHubでtoolboxの[releases](https://github.com/vapor/toolbox/releases)を確認して最新バージョンを見つけてください。この例では、18.6.0を使用しています。
|
||||
|
||||
### Vaporのクローンとビルド {#clone-and-build-vapor}
|
||||
|
||||
Vapor Toolboxリポジトリをクローンします。
|
||||
|
||||
```sh
|
||||
git clone https://github.com/vapor/toolbox.git
|
||||
```
|
||||
|
||||
最新のリリースをチェックアウトします。
|
||||
|
||||
```sh
|
||||
cd toolbox
|
||||
git checkout 18.6.0
|
||||
```
|
||||
|
||||
Vaporをビルドして、バイナリをパスに移動します。
|
||||
|
||||
```sh
|
||||
swift build -c release --disable-sandbox --enable-test-discovery
|
||||
sudo mv .build/release/vapor /usr/local/bin
|
||||
```
|
||||
|
||||
### Vaporプロジェクトの作成 {#create-a-vapor-project}
|
||||
|
||||
Toolboxの新規プロジェクトコマンドを使用してプロジェクトを初期化します。
|
||||
|
||||
```sh
|
||||
vapor new HelloWorld -n
|
||||
```
|
||||
|
||||
!!! tip
|
||||
`-n`フラグは、すべての質問に自動的にnoと答えることで、最小限のテンプレートを提供します。
|
||||
|
||||

|
||||
|
||||
コマンドが完了したら、新しく作成されたフォルダに移動します:
|
||||
|
||||
```sh
|
||||
cd HelloWorld
|
||||
```
|
||||
|
||||
### HTTPポートを開く {#open-http-port}
|
||||
|
||||
サーバー上のVaporにアクセスするために、HTTPポートを開きます。
|
||||
|
||||
```sh
|
||||
sudo ufw allow 8080
|
||||
```
|
||||
|
||||
### 実行 {#run}
|
||||
|
||||
Vaporがセットアップされ、ポートが開いたので、実行してみましょう。
|
||||
|
||||
```sh
|
||||
swift run App serve --hostname 0.0.0.0 --port 8080
|
||||
```
|
||||
|
||||
ブラウザまたはローカルターミナルからサーバーのIPにアクセスすると、「It works!」が表示されるはずです。この例ではIPアドレスは`134.122.126.139`です。
|
||||
|
||||
```
|
||||
$ curl http://134.122.126.139:8080
|
||||
It works!
|
||||
```
|
||||
|
||||
サーバーに戻ると、テストリクエストのログが表示されているはずです。
|
||||
|
||||
```
|
||||
[ NOTICE ] Server starting on http://0.0.0.0:8080
|
||||
[ INFO ] GET /
|
||||
```
|
||||
|
||||
`CTRL+C`を使用してサーバーを終了します。シャットダウンには少し時間がかかる場合があります。
|
||||
|
||||
DigitalOcean DropletでVaporアプリが実行できたことをおめでとうございます!
|
||||
|
||||
## 次のステップ {#next-steps}
|
||||
|
||||
このガイドの残りの部分では、デプロイメントを改善するための追加リソースを紹介します。
|
||||
|
||||
### Supervisor {#supervisor}
|
||||
|
||||
Supervisorは、Vapor実行ファイルを実行および監視できるプロセス制御システムです。Supervisorを設定すると、サーバーの起動時にアプリが自動的に開始され、クラッシュした場合に再起動されます。[Supervisor](../deploy/supervisor.md)について詳しく学びましょう。
|
||||
|
||||
### Nginx {#nginx}
|
||||
|
||||
Nginxは、極めて高速で、実戦で証明されており、設定が簡単なHTTPサーバーおよびプロキシです。VaporはHTTPリクエストを直接処理することをサポートしていますが、Nginxの背後でプロキシすることで、パフォーマンス、セキュリティ、使いやすさが向上します。[Nginx](../deploy/nginx.md)について詳しく学びましょう。
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
# Docker デプロイ {#docker-deploys}
|
||||
|
||||
Docker を使用して Vapor アプリをデプロイすることには、いくつかの利点があります:
|
||||
|
||||
1. Docker 化されたアプリは、Docker デーモンを持つあらゆるプラットフォーム(Linux(CentOS、Debian、Fedora、Ubuntu)、macOS、Windows)で同じコマンドを使用して確実に起動できます。
|
||||
2. docker-compose や Kubernetes マニフェストを使用して、完全なデプロイメントに必要な複数のサービス(Redis、Postgres、nginx など)をオーケストレーションできます。
|
||||
3. 開発マシン上でもローカルで、アプリの水平スケーリング能力を簡単にテストできます。
|
||||
|
||||
このガイドでは、Docker 化されたアプリをサーバーに配置する方法の説明は省略します。最も簡単なデプロイは、サーバーに Docker をインストールし、開発マシンでアプリケーションを起動するのと同じコマンドを実行することです。
|
||||
|
||||
より複雑で堅牢なデプロイメントは、通常、ホスティングソリューションによって異なります。AWS のような多くの人気のあるソリューションには、Kubernetes のビルトインサポートやカスタムデータベースソリューションがあり、すべてのデプロイメントに適用されるベストプラクティスを書くことが困難です。
|
||||
|
||||
それでも、Docker を使用してサーバースタック全体をローカルで起動してテストすることは、大小問わずサーバーサイドアプリにとって非常に価値があります。さらに、このガイドで説明する概念は、すべての Docker デプロイメントに大まかに適用されます。
|
||||
|
||||
## セットアップ {#set-up}
|
||||
|
||||
Docker を実行するための開発環境をセットアップし、Docker スタックを構成するリソースファイルの基本的な理解を得る必要があります。
|
||||
|
||||
### Docker のインストール {#install-docker}
|
||||
|
||||
開発環境用に Docker をインストールする必要があります。Docker Engine Overview の [Supported Platforms](https://docs.docker.jp/get-docker.html) セクションで、任意のプラットフォームの情報を見つけることができます。Mac OS を使用している場合は、[Docker for Mac](https://docs.docker.jp/desktop/install/mac-install.html) のインストールページに直接ジャンプできます。
|
||||
|
||||
### テンプレートの生成 {#generate-template}
|
||||
|
||||
Vapor テンプレートを出発点として使用することをお勧めします。既にアプリがある場合は、既存のアプリを Docker 化する際の参照ポイントとして、以下で説明するようにテンプレートを新しいフォルダにビルドしてください。テンプレートから主要なリソースをアプリにコピーし、出発点として少し調整できます。
|
||||
|
||||
1. Vapor Toolbox をインストールまたはビルドします([macOS](../install/macos.md#install-toolbox)、[Linux](../install/linux.md#install-toolbox))。
|
||||
2. `vapor new my-dockerized-app` で新しい Vapor アプリを作成し、プロンプトに従って関連する機能を有効または無効にします。これらのプロンプトへの回答は、Docker リソースファイルの生成方法に影響します。
|
||||
|
||||
## Docker リソース {#docker-resources}
|
||||
|
||||
今すぐでも近い将来でも、[Docker Overview](https://docs.docker.jp/get-started/overview.html) に慣れることは価値があります。概要では、このガイドで使用するいくつかの重要な用語が説明されています。
|
||||
|
||||
テンプレート Vapor アプリには、2つの重要な Docker 固有のリソースがあります:**Dockerfile** と **docker-compose** ファイルです。
|
||||
|
||||
### Dockerfile
|
||||
|
||||
Dockerfile は、Docker 化されたアプリのイメージをビルドする方法を Docker に指示します。そのイメージには、アプリの実行可能ファイルと、それを実行するために必要なすべての依存関係が含まれています。Dockerfile をカスタマイズする際は、[完全なリファレンス](https://docs.docker.jp/engine/reference/builder.html)を開いておくことをお勧めします。
|
||||
|
||||
Vapor アプリ用に生成された Dockerfile には2つのステージがあります。最初のステージはアプリをビルドし、結果を含む保持領域を設定します。2番目のステージは、安全なランタイム環境の基本を設定し、保持領域内のすべてを最終イメージ内の配置場所に転送し、デフォルトポート(8080)でプロダクションモードでアプリを実行するデフォルトのエントリポイントとコマンドを設定します。この設定は、イメージを使用するときに上書きできます。
|
||||
|
||||
### Docker Compose ファイル {#docker-compose-file}
|
||||
|
||||
Docker Compose ファイルは、Docker が複数のサービスを相互に関連付けてビルドする方法を定義します。Vapor アプリテンプレートの Docker Compose ファイルは、アプリをデプロイするために必要な機能を提供しますが、詳細を学びたい場合は、利用可能なすべてのオプションの詳細が記載されている[完全なリファレンス](https://docs.docker.jp/reference/compose-file/toc.html)を参照してください。
|
||||
|
||||
!!! note
|
||||
最終的に Kubernetes を使用してアプリをオーケストレーションする予定がある場合、Docker Compose ファイルは直接関係ありません。ただし、Kubernetes マニフェストファイルは概念的に似ており、[Docker Compose ファイルの移植](https://kubernetes.io/docs/tasks/configure-pod-container/translate-compose-kubernetes/)を目的としたプロジェクトもあります。
|
||||
|
||||
新しい Vapor アプリの Docker Compose ファイルは、アプリの実行、マイグレーションの実行または元に戻す、およびアプリの永続レイヤーとしてデータベースを実行するためのサービスを定義します。正確な定義は、`vapor new` を実行したときに選択したデータベースによって異なります。
|
||||
|
||||
Docker Compose ファイルの上部付近に共有環境変数があることに注意してください。(Fluent を使用しているかどうか、および使用している場合はどの Fluent ドライバーを使用しているかによって、デフォルト変数のセットが異なる場合があります。)
|
||||
|
||||
```docker
|
||||
x-shared_environment: &shared_environment
|
||||
LOG_LEVEL: ${LOG_LEVEL:-debug}
|
||||
DATABASE_HOST: db
|
||||
DATABASE_NAME: vapor_database
|
||||
DATABASE_USERNAME: vapor_username
|
||||
DATABASE_PASSWORD: vapor_password
|
||||
```
|
||||
|
||||
これらは、`<<: *shared_environment` YAML 参照構文で複数のサービスに取り込まれているのがわかります。
|
||||
|
||||
`DATABASE_HOST`、`DATABASE_NAME`、`DATABASE_USERNAME`、および `DATABASE_PASSWORD` 変数はこの例ではハードコードされていますが、`LOG_LEVEL` はサービスを実行している環境から値を取得するか、その変数が設定されていない場合は `'debug'` にフォールバックします。
|
||||
|
||||
!!! note
|
||||
ユーザー名とパスワードのハードコーディングはローカル開発では許容されますが、本番デプロイメントではこれらの変数をシークレットファイルに保存する必要があります。本番環境でこれを処理する1つの方法は、シークレットファイルをデプロイを実行している環境にエクスポートし、Docker Compose ファイルで次のような行を使用することです:
|
||||
|
||||
```
|
||||
DATABASE_USERNAME: ${DATABASE_USERNAME}
|
||||
```
|
||||
|
||||
これにより、ホストで定義されている環境変数がコンテナに渡されます。
|
||||
|
||||
その他の注意点:
|
||||
|
||||
- サービスの依存関係は `depends_on` 配列で定義されます。
|
||||
- サービスポートは `ports` 配列でサービスを実行しているシステムに公開されます(`<host_port>:<service_port>` の形式)。
|
||||
- `DATABASE_HOST` は `db` として定義されています。これは、アプリが `http://db:5432` でデータベースにアクセスすることを意味します。これは、Docker がサービスで使用されるネットワークを起動し、そのネットワーク上の内部 DNS が `db` という名前を `'db'` という名前のサービスにルーティングするため機能します。
|
||||
- Dockerfile の `CMD` ディレクティブは、一部のサービスで `command` 配列によって上書きされます。`command` で指定されたものは、Dockerfile の `ENTRYPOINT` に対して実行されることに注意してください。
|
||||
- Swarm モード(詳細は後述)では、サービスはデフォルトで1つのインスタンスが与えられますが、`migrate` と `revert` サービスは `deploy` `replicas: 0` を持つように定義されているため、Swarm を実行するときにデフォルトでは起動しません。
|
||||
|
||||
## ビルド {#building}
|
||||
|
||||
Docker Compose ファイルは、アプリをビルドする方法(現在のディレクトリの Dockerfile を使用)と、結果のイメージに付ける名前(`my-dockerized-app:latest`)を Docker に指示します。後者は実際には名前(`my-dockerized-app`)とタグ(`latest`)の組み合わせで、タグは Docker イメージのバージョン管理に使用されます。
|
||||
|
||||
アプリの Docker イメージをビルドするには、アプリのプロジェクトのルートディレクトリ(`docker-compose.yml` を含むフォルダ)から以下を実行します:
|
||||
|
||||
```shell
|
||||
docker compose build
|
||||
```
|
||||
|
||||
開発マシンで以前にビルドしていても、アプリとその依存関係を再度ビルドする必要があることがわかります。Docker が使用している Linux ビルド環境でビルドされているため、開発マシンからのビルドアーティファクトは再利用できません。
|
||||
|
||||
完了すると、以下を実行したときにアプリのイメージが表示されます:
|
||||
|
||||
```shell
|
||||
docker image ls
|
||||
```
|
||||
|
||||
## 実行 {#running}
|
||||
|
||||
サービスのスタックは Docker Compose ファイルから直接実行することも、Swarm モードや Kubernetes などのオーケストレーションレイヤーを使用することもできます。
|
||||
|
||||
### スタンドアロン {#standalone}
|
||||
|
||||
アプリを実行する最も簡単な方法は、スタンドアロンコンテナとして起動することです。Docker は `depends_on` 配列を使用して、依存するサービスも開始されることを確認します。
|
||||
|
||||
まず、以下を実行します:
|
||||
|
||||
```shell
|
||||
docker compose up app
|
||||
```
|
||||
|
||||
`app` と `db` の両方のサービスが開始されることに注意してください。
|
||||
|
||||
アプリはポート 8080 でリッスンしており、Docker Compose ファイルで定義されているように、開発マシンで **http://localhost:8080** でアクセスできます。
|
||||
|
||||
このポートマッピングの区別は非常に重要です。なぜなら、すべてが独自のコンテナで実行され、それぞれがホストマシンに異なるポートを公開している場合、同じポートで任意の数のサービスを実行できるからです。
|
||||
|
||||
`http://localhost:8080` にアクセスすると `It works!` が表示されますが、`http://localhost:8080/todos` にアクセスすると以下が表示されます:
|
||||
|
||||
```
|
||||
{"error":true,"reason":"Something went wrong."}
|
||||
```
|
||||
|
||||
`docker compose up app` を実行したターミナルのログ出力を見ると、以下が表示されます:
|
||||
|
||||
```
|
||||
[ ERROR ] relation "todos" does not exist
|
||||
```
|
||||
|
||||
もちろん!データベースでマイグレーションを実行する必要があります。`Ctrl+C` を押してアプリを停止します。今度は以下でアプリを再起動します:
|
||||
|
||||
```shell
|
||||
docker compose up --detach app
|
||||
```
|
||||
|
||||
これで、アプリは「デタッチ」(バックグラウンド)で起動します。以下を実行して確認できます:
|
||||
|
||||
```shell
|
||||
docker container ls
|
||||
```
|
||||
|
||||
データベースとアプリの両方がコンテナで実行されているのがわかります。以下を実行してログを確認することもできます:
|
||||
|
||||
```shell
|
||||
docker logs <container_id>
|
||||
```
|
||||
|
||||
マイグレーションを実行するには、以下を実行します:
|
||||
|
||||
```shell
|
||||
docker compose run migrate
|
||||
```
|
||||
|
||||
マイグレーションが実行された後、`http://localhost:8080/todos` に再度アクセスすると、エラーメッセージの代わりに空の todos リストが表示されます。
|
||||
|
||||
#### ログレベル {#log-levels}
|
||||
|
||||
上記で説明したように、Docker Compose ファイルの `LOG_LEVEL` 環境変数は、利用可能な場合、サービスが開始される環境から継承されます。
|
||||
|
||||
以下でサービスを起動できます:
|
||||
|
||||
```shell
|
||||
LOG_LEVEL=trace docker-compose up app
|
||||
```
|
||||
|
||||
`trace` レベルのロギング(最も詳細)を取得します。この環境変数を使用して、ロギングを[利用可能な任意のレベル](../basics/logging.md#levels)に設定できます。
|
||||
|
||||
#### すべてのサービスログ {#all-service-logs}
|
||||
|
||||
コンテナを起動するときにデータベースサービスを明示的に指定すると、データベースとアプリの両方のログが表示されます。
|
||||
|
||||
```shell
|
||||
docker-compose up app db
|
||||
```
|
||||
|
||||
#### スタンドアロンコンテナの停止 {#bringing-standalone-containers-down}
|
||||
|
||||
ホストシェルから「デタッチ」されて実行されているコンテナがあるので、何らかの方法でシャットダウンを指示する必要があります。実行中のコンテナは以下でシャットダウンを要求できることを知っておく価値があります:
|
||||
|
||||
```shell
|
||||
docker container stop <container_id>
|
||||
```
|
||||
|
||||
しかし、これらの特定のコンテナを停止する最も簡単な方法は以下です:
|
||||
|
||||
```shell
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
#### データベースのワイプ {#wiping-the-database}
|
||||
|
||||
Docker Compose ファイルは、実行間でデータベースを永続化するために `db_data` ボリュームを定義しています。データベースをリセットする方法はいくつかあります。
|
||||
|
||||
コンテナを停止すると同時に `db_data` ボリュームを削除できます:
|
||||
|
||||
```shell
|
||||
docker-compose down --volumes
|
||||
```
|
||||
|
||||
`docker volume ls` で現在データを永続化しているボリュームを確認できます。ボリューム名は通常、Swarm モードで実行していたかどうかに応じて、`my-dockerized-app_` または `test_` のプレフィックスが付いていることに注意してください。
|
||||
|
||||
これらのボリュームは、例えば以下で1つずつ削除できます:
|
||||
|
||||
```shell
|
||||
docker volume rm my-dockerized-app_db_data
|
||||
```
|
||||
|
||||
以下ですべてのボリュームをクリーンアップすることもできます:
|
||||
|
||||
```shell
|
||||
docker volume prune
|
||||
```
|
||||
|
||||
保持しておきたいデータのあるボリュームを誤って削除しないように注意してください!
|
||||
|
||||
Docker は、実行中または停止したコンテナで現在使用されているボリュームを削除することはできません。`docker container ls` で実行中のコンテナのリストを取得でき、`docker container ls -a` で停止したコンテナも確認できます。
|
||||
|
||||
### Swarm モード {#swarm-mode}
|
||||
|
||||
Swarm モードは、Docker Compose ファイルが手元にあり、アプリが水平方向にどのようにスケールするかをテストしたい場合に使用する簡単なインターフェースです。Swarm モードのすべてについては、[概要](https://docs.docker.com/engine/swarm/)をルートとするページで読むことができます。
|
||||
|
||||
最初に必要なのは、Swarm のマネージャーノードです。以下を実行します:
|
||||
|
||||
```shell
|
||||
docker swarm init
|
||||
```
|
||||
|
||||
次に、Docker Compose ファイルを使用して、サービスを含む `'test'` という名前のスタックを起動します:
|
||||
|
||||
```shell
|
||||
docker stack deploy -c docker-compose.yml test
|
||||
```
|
||||
|
||||
サービスがどのようになっているかは以下で確認できます:
|
||||
|
||||
```shell
|
||||
docker service ls
|
||||
```
|
||||
|
||||
`app` と `db` サービスには `1/1` レプリカ、`migrate` と `revert` サービスには `0/0` レプリカが表示されるはずです。
|
||||
|
||||
Swarm モードでマイグレーションを実行するには、別のコマンドを使用する必要があります。
|
||||
|
||||
```shell
|
||||
docker service scale --detach test_migrate=1
|
||||
```
|
||||
|
||||
!!! note
|
||||
短命なサービスに1つのレプリカにスケールするよう要求しました。正常にスケールアップし、実行し、その後終了します。ただし、これにより `0/1` レプリカが実行されたままになります。マイグレーションを再度実行するまでは大した問題ではありませんが、すでにその状態にある場合は「1つのレプリカにスケールアップ」するように指示することはできません。このセットアップの特徴は、同じ Swarm ランタイム内で次回マイグレーションを実行したい場合、最初にサービスを `0` にスケールダウンしてから `1` に戻す必要があることです。
|
||||
|
||||
この短いガイドの文脈での苦労の見返りは、データベースの競合、クラッシュなどをどれだけうまく処理するかをテストするために、アプリを必要なものにスケールできることです。
|
||||
|
||||
アプリの5つのインスタンスを同時に実行したい場合は、以下を実行します:
|
||||
|
||||
```shell
|
||||
docker service scale test_app=5
|
||||
```
|
||||
|
||||
Docker がアプリをスケールアップするのを見るだけでなく、`docker service ls` を再度確認することで、実際に5つのレプリカが実行されていることを確認できます。
|
||||
|
||||
アプリのログを表示(およびフォロー)できます:
|
||||
|
||||
```shell
|
||||
docker service logs -f test_app
|
||||
```
|
||||
|
||||
#### Swarm サービスの停止 {#bringing-swarm-services-down}
|
||||
|
||||
Swarm モードでサービスを停止したい場合は、以前に作成したスタックを削除することで行います。
|
||||
|
||||
```shell
|
||||
docker stack rm test
|
||||
```
|
||||
|
||||
## 本番デプロイ {#production-deploys}
|
||||
|
||||
冒頭で述べたように、このガイドでは Docker 化されたアプリを本番環境にデプロイする方法について詳しく説明しません。なぜなら、このトピックは大規模であり、ホスティングサービス(AWS、Azure など)、ツール(Terraform、Ansible など)、オーケストレーション(Docker Swarm、Kubernetes など)によって大きく異なるからです。
|
||||
|
||||
ただし、開発マシンで Docker 化されたアプリをローカルで実行するために学ぶテクニックは、本番環境に大部分転用できます。docker デーモンを実行するように設定されたサーバーインスタンスは、すべて同じコマンドを受け入れます。
|
||||
|
||||
プロジェクトファイルをサーバーにコピーし、サーバーに SSH 接続し、`docker-compose` または `docker stack deploy` コマンドを実行してリモートで起動します。
|
||||
|
||||
または、ローカルの `DOCKER_HOST` 環境変数をサーバーを指すように設定し、マシンでローカルに `docker` コマンドを実行します。このアプローチでは、プロジェクトファイルをサーバーにコピーする必要はありません*が*、サーバーがプルできる場所に docker イメージをホストする必要があることに注意することが重要です。
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
# Fly
|
||||
|
||||
Flyは、エッジコンピューティングに焦点を当てたサーバーアプリケーションとデータベースの実行を可能にするホスティングプラットフォームです。詳細については[公式サイト](https://fly.io/)をご覧ください。
|
||||
|
||||
!!! note
|
||||
このドキュメントで指定されるコマンドは[Flyの価格設定](https://fly.io/docs/about/pricing/)の対象となります。続行する前に適切に理解しておいてください。
|
||||
|
||||
## サインアップ {#signing-up}
|
||||
アカウントをお持ちでない場合は、[アカウントを作成](https://fly.io/app/sign-up)する必要があります。
|
||||
|
||||
## flyctlのインストール {#installing-flyctl}
|
||||
Flyとやり取りする主な方法は、専用のCLIツール`flyctl`を使用することです。これをインストールする必要があります。
|
||||
|
||||
### macOS
|
||||
```bash
|
||||
brew install flyctl
|
||||
```
|
||||
|
||||
### Linux
|
||||
```bash
|
||||
curl -L https://fly.io/install.sh | sh
|
||||
```
|
||||
|
||||
### その他のインストールオプション {#other-install-options}
|
||||
その他のオプションと詳細については、[`flyctl`インストールドキュメント](https://fly.io/docs/flyctl/install/)をご覧ください。
|
||||
|
||||
## ログイン {#logging-in}
|
||||
ターミナルからログインするには、次のコマンドを実行します:
|
||||
```bash
|
||||
fly auth login
|
||||
```
|
||||
|
||||
## Vaporプロジェクトの設定 {#configuring-your-vapor-project}
|
||||
Flyにデプロイする前に、Flyがアプリをビルドするために必要なDockerfileが適切に設定されたVaporプロジェクトがあることを確認する必要があります。ほとんどの場合、デフォルトのVaporテンプレートにはすでにDockerfileが含まれているため、これは非常に簡単です。
|
||||
|
||||
### 新しいVaporプロジェクト {#new-vapor-project}
|
||||
新しいプロジェクトを作成する最も簡単な方法は、テンプレートから始めることです。GitHubテンプレートまたはVaporツールボックスを使用して作成できます。データベースが必要な場合は、PostgresでFluentを使用することをお勧めします。Flyでは、アプリが接続できるPostgresデータベースを簡単に作成できます(下記の[専用セクション](#configuring-postgres)を参照)。
|
||||
|
||||
#### Vaporツールボックスを使用 {#using-the-vapor-toolbox}
|
||||
まず、Vaporツールボックスがインストールされていることを確認してください([macOS](../install/macos.md#install-toolbox)または[Linux](../install/linux.md#install-toolbox)のインストール手順を参照)。
|
||||
次のコマンドで新しいアプリを作成し、`app-name`を希望のアプリ名に置き換えてください:
|
||||
```bash
|
||||
vapor new app-name
|
||||
```
|
||||
|
||||
このコマンドは、Vaporプロジェクトを設定できる対話型プロンプトを表示します。ここでFluentとPostgresが必要な場合は選択できます。
|
||||
|
||||
#### GitHubテンプレートを使用 {#using-github-templates}
|
||||
以下のリストから、ニーズに最も適したテンプレートを選択してください。Gitを使用してローカルにクローンするか、「Use this template」ボタンでGitHubプロジェクトを作成できます。
|
||||
|
||||
- [ベアボーンテンプレート](https://github.com/vapor/template-bare)
|
||||
- [Fluent/Postgresテンプレート](https://github.com/vapor/template-fluent-postgres)
|
||||
- [Fluent/Postgres + Leafテンプレート](https://github.com/vapor/template-fluent-postgres-leaf)
|
||||
|
||||
### 既存のVaporプロジェクト {#existing-vapor-project}
|
||||
既存のVaporプロジェクトがある場合は、ディレクトリのルートに適切に設定された`Dockerfile`があることを確認してください。[VaporのDockerに関するドキュメント](../deploy/docker.md)と[FlyのDockerfileを介したアプリのデプロイに関するドキュメント](https://fly.io/docs/languages-and-frameworks/dockerfile/)が役立つかもしれません。
|
||||
|
||||
## Flyでアプリを起動する {#launch-your-app-on-fly}
|
||||
Vaporプロジェクトの準備ができたら、Flyで起動できます。
|
||||
|
||||
まず、現在のディレクトリがVaporアプリケーションのルートディレクトリに設定されていることを確認し、次のコマンドを実行します:
|
||||
```bash
|
||||
fly launch
|
||||
```
|
||||
|
||||
これにより、Flyアプリケーション設定を構成するための対話型プロンプトが開始されます:
|
||||
|
||||
- **名前:** 名前を入力するか、空白のままにして自動生成された名前を取得できます。
|
||||
- **リージョン:** デフォルトは最も近いリージョンです。これを使用するか、リストの他のリージョンを選択できます。これは後で簡単に変更できます。
|
||||
- **データベース:** アプリで使用するデータベースをFlyに作成するよう依頼できます。希望する場合は、後で`fly pg create`と`fly pg attach`コマンドを使用して同じことができます(詳細については[Postgresの設定セクション](#configuring-postgres)を参照)。
|
||||
|
||||
`fly launch`コマンドは自動的に`fly.toml`ファイルを作成します。これには、プライベート/パブリックポートマッピング、ヘルスチェックパラメータなどの設定が含まれています。`vapor new`を使用してゼロから新しいプロジェクトを作成した場合、デフォルトの`fly.toml`ファイルは変更不要です。既存のプロジェクトがある場合も、`fly.toml`は変更なしまたは軽微な変更のみで問題ない可能性があります。詳細については[`fly.toml`ドキュメント](https://fly.io/docs/reference/configuration/)をご覧ください。
|
||||
|
||||
Flyにデータベースの作成を依頼した場合、データベースが作成されてヘルスチェックを通過するまで少し待つ必要があることに注意してください。
|
||||
|
||||
終了する前に、`fly launch`コマンドはアプリをすぐにデプロイするかどうか尋ねます。承諾するか、後で`fly deploy`を使用してデプロイできます。
|
||||
|
||||
!!! tip
|
||||
現在のディレクトリがアプリのルートにある場合、fly CLIツールは`fly.toml`ファイルの存在を自動的に検出し、どのアプリをターゲットにしているかをFlyに知らせます。現在のディレクトリに関係なく特定のアプリをターゲットにしたい場合は、ほとんどのFlyコマンドに`-a name-of-your-app`を追加できます。
|
||||
|
||||
## デプロイ {#deploying}
|
||||
Flyに新しい変更をデプロイする必要があるときはいつでも`fly deploy`コマンドを実行します。
|
||||
|
||||
Flyはディレクトリの`Dockerfile`と`fly.toml`ファイルを読み取り、Vaporプロジェクトのビルドと実行方法を決定します。
|
||||
|
||||
コンテナがビルドされると、Flyはそのインスタンスを開始します。アプリケーションが正常に動作し、サーバーがリクエストに応答することを確認するため、さまざまなヘルスチェックを実行します。ヘルスチェックが失敗した場合、`fly deploy`コマンドはエラーで終了します。
|
||||
|
||||
デフォルトでは、デプロイしようとした新しいバージョンのヘルスチェックが失敗した場合、Flyはアプリの最新の動作バージョンにロールバックします。
|
||||
|
||||
## Postgresの設定 {#configuring-postgres}
|
||||
|
||||
### FlyでPostgresデータベースを作成する {#creating-a-postgres-database-on-fly}
|
||||
アプリを最初に起動したときにデータベースアプリを作成しなかった場合は、後で次のコマンドを使用して作成できます:
|
||||
```bash
|
||||
fly pg create
|
||||
```
|
||||
|
||||
このコマンドは、Fly上の他のアプリが利用できるデータベースをホストできるFlyアプリを作成します。詳細については[専用のFlyドキュメント](https://fly.io/docs/postgres/)をご覧ください。
|
||||
|
||||
データベースアプリが作成されたら、Vaporアプリのルートディレクトリに移動して次を実行します:
|
||||
```bash
|
||||
fly pg attach name-of-your-postgres-app
|
||||
```
|
||||
Postgresアプリの名前がわからない場合は、`fly pg list`で確認できます。
|
||||
|
||||
`fly pg attach`コマンドは、アプリ用のデータベースとユーザーを作成し、`DATABASE_URL`環境変数を通じてアプリに公開します。
|
||||
|
||||
!!! note
|
||||
`fly pg create`と`fly pg attach`の違いは、前者がPostgresデータベースをホストできるFlyアプリを割り当てて設定するのに対し、後者は選択したアプリ用の実際のデータベースとユーザーを作成することです。要件に適合する場合、単一のPostgres Flyアプリが様々なアプリで使用される複数のデータベースをホストできます。`fly launch`でFlyにデータベースアプリの作成を依頼すると、`fly pg create`と`fly pg attach`の両方を呼び出すのと同等の処理が行われます。
|
||||
|
||||
### Vaporアプリをデータベースに接続する {#connecting-your-vapor-app-to-the-database}
|
||||
アプリがデータベースにアタッチされると、Flyは`DATABASE_URL`環境変数に資格情報を含む接続URLを設定します(機密情報として扱う必要があります)。
|
||||
|
||||
最も一般的なVaporプロジェクトの設定では、`configure.swift`でデータベースを設定します。以下は設定例です:
|
||||
|
||||
```swift
|
||||
if let databaseURL = Environment.get("DATABASE_URL") {
|
||||
try app.databases.use(.postgres(url: databaseURL), as: .psql)
|
||||
} else {
|
||||
// ここでDATABASE_URLが欠落している場合の処理...
|
||||
//
|
||||
// または、app.environmentが`.development`か
|
||||
// `.production`に設定されているかによって
|
||||
// 異なる設定を行うこともできます
|
||||
}
|
||||
```
|
||||
|
||||
この時点で、プロジェクトはマイグレーションを実行し、データベースを使用する準備ができているはずです。
|
||||
|
||||
### マイグレーションの実行 {#running-migrations}
|
||||
`fly.toml`の`release_command`を使用すると、メインサーバープロセスを実行する前に特定のコマンドを実行するようFlyに依頼できます。`fly.toml`に以下を追加します:
|
||||
```toml
|
||||
[deploy]
|
||||
release_command = "migrate -y"
|
||||
```
|
||||
|
||||
!!! note
|
||||
上記のコードスニペットは、アプリの`ENTRYPOINT`を`./App`に設定するデフォルトのVapor Dockerfileを使用していることを前提としています。具体的には、`release_command`を`migrate -y`に設定すると、Flyは`./App migrate -y`を呼び出します。`ENTRYPOINT`が異なる値に設定されている場合は、`release_command`の値を適応させる必要があります。
|
||||
|
||||
Flyは、内部Flyネットワーク、シークレット、環境変数にアクセスできる一時的なインスタンスでリリースコマンドを実行します。
|
||||
|
||||
リリースコマンドが失敗した場合、デプロイは続行されません。
|
||||
|
||||
### その他のデータベース {#other-databases}
|
||||
FlyはPostgresデータベースアプリを簡単に作成できますが、他のタイプのデータベースもホストすることが可能です(例えば、Flyドキュメントの["MySQLデータベースを使用する"](https://fly.io/docs/app-guides/mysql-on-fly/)を参照)。
|
||||
|
||||
## シークレットと環境変数 {#secrets-and-environment-variables}
|
||||
### シークレット {#secrets}
|
||||
シークレットを使用して、機密値を環境変数として設定します。
|
||||
```bash
|
||||
fly secrets set MYSECRET=A_SUPER_SECRET_VALUE
|
||||
```
|
||||
|
||||
!!! warning
|
||||
ほとんどのシェルは入力したコマンドの履歴を保持することに留意してください。この方法でシークレットを設定する場合は注意してください。一部のシェルは、空白で始まるコマンドを記憶しないように設定できます。[`fly secrets import`コマンド](https://fly.io/docs/flyctl/secrets-import/)も参照してください。
|
||||
|
||||
詳細については、[`fly secrets`のドキュメント](https://fly.io/docs/apps/secrets/)をご覧ください。
|
||||
|
||||
### 環境変数 {#environment-variables}
|
||||
その他の機密でない[環境変数は`fly.toml`で設定](https://fly.io/docs/reference/configuration/#the-env-variables-section)できます。例:
|
||||
```toml
|
||||
[env]
|
||||
MAX_API_RETRY_COUNT = "3"
|
||||
SMS_LOG_LEVEL = "error"
|
||||
```
|
||||
|
||||
## SSH接続 {#ssh-connection}
|
||||
次のコマンドでアプリのインスタンスに接続できます:
|
||||
```bash
|
||||
fly ssh console -s
|
||||
```
|
||||
|
||||
## ログの確認 {#checking-the-logs}
|
||||
次のコマンドでアプリのライブログを確認できます:
|
||||
```bash
|
||||
fly logs
|
||||
```
|
||||
|
||||
## 次のステップ {#next-steps}
|
||||
Vaporアプリがデプロイされたら、複数のリージョンにわたってアプリを垂直的および水平的にスケーリングしたり、永続ボリュームを追加したり、継続的デプロイメントを設定したり、分散アプリクラスターを作成したりするなど、さらに多くのことができます。これらすべてを行う方法を学ぶ最良の場所は[Flyドキュメント](https://fly.io/docs/)です。
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
# Herokuとは {#what-is-heroku}
|
||||
|
||||
Herokuは人気のオールインワンホスティングソリューションです。詳細は[heroku.com](https://www.heroku.com)をご覧ください。
|
||||
|
||||
## サインアップ {#signing-up}
|
||||
|
||||
Herokuアカウントが必要です。まだお持ちでない場合は、こちらからサインアップしてください:[https://signup.heroku.com/](https://signup.heroku.com/)
|
||||
|
||||
## CLIのインストール {#installing-cli}
|
||||
|
||||
Heroku CLIツールがインストールされていることを確認してください。
|
||||
|
||||
### HomeBrew {#homebrew}
|
||||
|
||||
```bash
|
||||
brew tap heroku/brew && brew install heroku
|
||||
```
|
||||
|
||||
### その他のインストールオプション {#other-install-options}
|
||||
|
||||
代替のインストールオプションはこちらをご覧ください:[https://devcenter.heroku.com/articles/heroku-cli#download-and-install](https://devcenter.heroku.com/articles/heroku-cli#download-and-install)。
|
||||
|
||||
### ログイン {#logging-in}
|
||||
|
||||
CLIをインストールしたら、次のコマンドでログインします:
|
||||
|
||||
```bash
|
||||
heroku login
|
||||
```
|
||||
|
||||
正しいメールアドレスでログインしていることを確認します:
|
||||
|
||||
```bash
|
||||
heroku auth:whoami
|
||||
```
|
||||
|
||||
### アプリケーションの作成 {#create-an-application}
|
||||
|
||||
dashboard.heroku.comにアクセスしてアカウントにログインし、右上のドロップダウンから新しいアプリケーションを作成します。Herokuはリージョンやアプリケーション名などいくつかの質問をしますので、プロンプトに従ってください。
|
||||
|
||||
### Git {#git}
|
||||
|
||||
HerokuはGitを使用してアプリをデプロイするため、プロジェクトをGitリポジトリに配置する必要があります(まだの場合)。
|
||||
|
||||
#### Gitの初期化 {#initialize-git}
|
||||
|
||||
プロジェクトにGitを追加する必要がある場合は、ターミナルで次のコマンドを入力します:
|
||||
|
||||
```bash
|
||||
git init
|
||||
```
|
||||
|
||||
#### マスターブランチ {#master}
|
||||
|
||||
Herokuへのデプロイには、**main**または**master**ブランチのような一つのブランチを決めて、それを使い続ける必要があります。プッシュする前に、すべての変更がこのブランチにチェックインされていることを確認してください。
|
||||
|
||||
現在のブランチを確認します:
|
||||
|
||||
```bash
|
||||
git branch
|
||||
```
|
||||
|
||||
アスタリスクが現在のブランチを示しています。
|
||||
|
||||
```bash
|
||||
* main
|
||||
commander
|
||||
other-branches
|
||||
```
|
||||
|
||||
!!! note
|
||||
`git init`を実行したばかりで出力が表示されない場合、最初にコードをコミットする必要があります。その後、`git branch`コマンドから出力が表示されます。
|
||||
|
||||
正しいブランチにいない場合は、次のコマンドで切り替えます(**main**の場合):
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
```
|
||||
|
||||
#### 変更のコミット {#commit-changes}
|
||||
|
||||
このコマンドが出力を生成する場合、コミットされていない変更があります。
|
||||
|
||||
```bash
|
||||
git status --porcelain
|
||||
```
|
||||
|
||||
次のコマンドでコミットします:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "a description of the changes I made"
|
||||
```
|
||||
|
||||
#### Herokuとの接続 {#connect-with-heroku}
|
||||
|
||||
アプリをHerokuと接続します(アプリの名前に置き換えてください)。
|
||||
|
||||
```bash
|
||||
$ heroku git:remote -a your-apps-name-here
|
||||
```
|
||||
|
||||
### ビルドパックの設定 {#set-buildpack}
|
||||
|
||||
HerokuにVaporの扱い方を教えるためにビルドパックを設定します。
|
||||
|
||||
```bash
|
||||
heroku buildpacks:set vapor/vapor
|
||||
```
|
||||
|
||||
### Swiftバージョンファイル {#swift-version-file}
|
||||
|
||||
追加したビルドパックは、使用するSwiftのバージョンを知るために**.swift-version**ファイルを探します。(5.8.1をプロジェクトが必要とするバージョンに置き換えてください。)
|
||||
|
||||
```bash
|
||||
echo "5.8.1" > .swift-version
|
||||
```
|
||||
|
||||
これにより、`5.8.1`を内容とする**.swift-version**が作成されます。
|
||||
|
||||
### Procfile {#procfile}
|
||||
|
||||
Herokuはアプリの実行方法を知るために**Procfile**を使用します。私たちの場合、次のようになります:
|
||||
|
||||
```
|
||||
web: App serve --env production --hostname 0.0.0.0 --port $PORT
|
||||
```
|
||||
|
||||
次のターミナルコマンドでこれを作成できます:
|
||||
|
||||
```bash
|
||||
echo "web: App serve --env production" \
|
||||
"--hostname 0.0.0.0 --port \$PORT" > Procfile
|
||||
```
|
||||
|
||||
### 変更のコミット {#commit-changes_1}
|
||||
|
||||
これらのファイルを追加しましたが、まだコミットされていません。プッシュしてもHerokuはそれらを見つけられません。
|
||||
|
||||
次のコマンドでコミットします。
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "adding heroku build files"
|
||||
```
|
||||
|
||||
### Herokuへのデプロイ {#deploying-to-heroku}
|
||||
|
||||
デプロイの準備ができました。ターミナルからこれを実行します。ビルドに時間がかかることがありますが、これは正常です。
|
||||
|
||||
```bash
|
||||
git push heroku main
|
||||
```
|
||||
|
||||
### スケールアップ {#scale-up}
|
||||
|
||||
ビルドが成功したら、少なくとも1つのサーバーを追加する必要があります。価格はEcoプランで月額$5から始まります([価格](https://www.heroku.com/pricing#containers)を参照)。Herokuで支払い設定が完了していることを確認してください。次に、単一のWebワーカーの場合:
|
||||
|
||||
```bash
|
||||
heroku ps:scale web=1
|
||||
```
|
||||
|
||||
### 継続的デプロイ {#continued-deployment}
|
||||
|
||||
更新したい場合は、最新の変更をmainに取り込んでHerokuにプッシュするだけで、再デプロイされます。
|
||||
|
||||
## Postgres {#postgres}
|
||||
|
||||
### PostgreSQLデータベースの追加 {#add-postgresql-database}
|
||||
|
||||
dashboard.heroku.comでアプリケーションにアクセスし、**Add-ons**セクションに移動します。
|
||||
|
||||
ここで`postgres`と入力すると、`Heroku Postgres`のオプションが表示されます。それを選択します。
|
||||
|
||||
Essential 0プランを月額$5で選択し([価格](https://www.heroku.com/pricing#data-services)を参照)、プロビジョニングします。Herokuが残りの作業を行います。
|
||||
|
||||
完了すると、**Resources**タブの下にデータベースが表示されます。
|
||||
|
||||
### データベースの設定 {#configure-the-database}
|
||||
|
||||
次に、アプリがデータベースにアクセスする方法を指定する必要があります。アプリディレクトリで実行します。
|
||||
|
||||
```bash
|
||||
heroku config
|
||||
```
|
||||
|
||||
これにより、次のような出力が生成されます:
|
||||
|
||||
```none
|
||||
=== today-i-learned-vapor Config Vars
|
||||
DATABASE_URL: postgres://cybntsgadydqzm:2d9dc7f6d964f4750da1518ad71hag2ba729cd4527d4a18c70e024b11cfa8f4b@ec2-54-221-192-231.compute-1.amazonaws.com:5432/dfr89mvoo550b4
|
||||
```
|
||||
|
||||
ここでの**DATABASE_URL**はPostgresデータベースを表します。この静的URLを**決して**ハードコードしないでください。Herokuはそれをローテーションし、アプリケーションが壊れます。また、これは悪い習慣です。代わりに、実行時に環境変数を読み取ります。
|
||||
|
||||
Heroku Postgresアドオンは、すべての接続を暗号化することを[要求](https://devcenter.heroku.com/changelog-items/2035)します。Postgresサーバーが使用する証明書はHeroku内部のものであるため、**未検証**のTLS接続を設定する必要があります。
|
||||
|
||||
次のスニペットは、両方を達成する方法を示しています:
|
||||
|
||||
```swift
|
||||
if let databaseURL = Environment.get("DATABASE_URL") {
|
||||
var tlsConfig: TLSConfiguration = .makeClientConfiguration()
|
||||
tlsConfig.certificateVerification = .none
|
||||
let nioSSLContext = try NIOSSLContext(configuration: tlsConfig)
|
||||
|
||||
var postgresConfig = try SQLPostgresConfiguration(url: databaseURL)
|
||||
postgresConfig.coreConfiguration.tls = .require(nioSSLContext)
|
||||
|
||||
app.databases.use(.postgres(configuration: postgresConfig), as: .psql)
|
||||
} else {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
これらの変更をコミットすることを忘れないでください:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "configured heroku database"
|
||||
```
|
||||
|
||||
### データベースのリバート {#reverting-your-database}
|
||||
|
||||
`run`コマンドを使用して、Heroku上でリバートやその他のコマンドを実行できます。
|
||||
|
||||
データベースをリバートするには:
|
||||
|
||||
```bash
|
||||
heroku run App -- migrate --revert --all --yes --env production
|
||||
```
|
||||
|
||||
マイグレーションを実行するには:
|
||||
|
||||
```bash
|
||||
heroku run App -- migrate --env production
|
||||
```
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
# Nginxでのデプロイ {#deploying-with-nginx}
|
||||
|
||||
Nginxは非常に高速で、実戦で証明されており、設定が簡単なHTTPサーバーおよびプロキシです。VaporはTLSありまたはなしでHTTPリクエストを直接提供することをサポートしていますが、Nginxの背後でプロキシすることで、パフォーマンス、セキュリティ、使いやすさが向上します。
|
||||
|
||||
!!! note
|
||||
Vapor HTTPサーバーをNginxの背後でプロキシすることをお勧めします。
|
||||
|
||||
## 概要 {#overview}
|
||||
|
||||
HTTPサーバーをプロキシするとはどういう意味でしょうか?簡単に言えば、プロキシはパブリックインターネットとあなたのHTTPサーバーの間の仲介者として機能します。リクエストはプロキシに届き、その後Vaporに送信されます。
|
||||
|
||||
この仲介プロキシの重要な機能は、リクエストを変更したり、リダイレクトしたりできることです。例えば、プロキシはクライアントにTLS(https)の使用を要求したり、リクエストをレート制限したり、Vaporアプリケーションと通信せずにパブリックファイルを提供したりできます。
|
||||
|
||||

|
||||
|
||||
### 詳細 {#more-detail}
|
||||
|
||||
HTTPリクエストを受信するデフォルトのポートはポート`80`(HTTPSの場合は`443`)です。Vaporサーバーをポート`80`にバインドすると、サーバーに届くHTTPリクエストを直接受信して応答します。Nginxのようなプロキシを追加する場合、Vaporを`8080`のような内部ポートにバインドします。
|
||||
|
||||
!!! note
|
||||
1024より大きいポートはバインドに`sudo`を必要としません。
|
||||
|
||||
Vaporが`80`または`443`以外のポートにバインドされている場合、外部のインターネットからアクセスできません。次に、Nginxをポート`80`にバインドし、ポート`8080`(または選択したポート)にバインドされたVaporサーバーにリクエストをルーティングするように設定します。
|
||||
|
||||
以上です。Nginxが適切に設定されていれば、Vaporアプリがポート`80`でリクエストに応答しているのが見えるでしょう。Nginxはリクエストとレスポンスを透過的にプロキシします。
|
||||
|
||||
## Nginxのインストール {#install-nginx}
|
||||
|
||||
最初のステップはNginxのインストールです。Nginxの素晴らしい点の1つは、それを取り巻く膨大なコミュニティリソースとドキュメントです。このため、特定のプラットフォーム、OS、プロバイダー向けのチュートリアルがほぼ確実に存在するため、ここではNginxのインストールについて詳しく説明しません。
|
||||
|
||||
チュートリアル:
|
||||
|
||||
- [Ubuntu 20.04にNginxをインストールする方法](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-20-04-ja)
|
||||
- [Ubuntu 18.04にNginxをインストールする方法](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-18-04)
|
||||
- [CentOS 8にNginxをインストールする方法](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-centos-8)
|
||||
- [Ubuntu 16.04にNginxをインストールする方法](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-16-04)
|
||||
- [HerokuにNginxをデプロイする方法](https://blog.codeship.com/how-to-deploy-nginx-on-heroku/)
|
||||
|
||||
### パッケージマネージャー {#package-managers}
|
||||
|
||||
NginxはLinux上のパッケージマネージャーを通じてインストールできます。
|
||||
|
||||
#### Ubuntu
|
||||
|
||||
```sh
|
||||
sudo apt-get update
|
||||
sudo apt-get install nginx
|
||||
```
|
||||
|
||||
#### CentOSとAmazon Linux {#centos-and-amazon-linux}
|
||||
|
||||
```sh
|
||||
sudo yum install nginx
|
||||
```
|
||||
|
||||
#### Fedora
|
||||
|
||||
```sh
|
||||
sudo dnf install nginx
|
||||
```
|
||||
|
||||
### インストールの検証 {#validate-installation}
|
||||
|
||||
ブラウザでサーバーのIPアドレスにアクセスして、Nginxが正しくインストールされたか確認します
|
||||
|
||||
```
|
||||
http://server_domain_name_or_IP
|
||||
```
|
||||
|
||||
### サービス {#service}
|
||||
|
||||
サービスは開始または停止できます。
|
||||
|
||||
```sh
|
||||
sudo service nginx stop
|
||||
sudo service nginx start
|
||||
sudo service nginx restart
|
||||
```
|
||||
|
||||
## Vaporの起動 {#booting-vapor}
|
||||
|
||||
Nginxは`sudo service nginx ...`コマンドで開始と停止ができます。Vaporサーバーを開始と停止するための同様のものが必要になります。
|
||||
|
||||
これを行う方法は多くあり、デプロイ先のプラットフォームによって異なります。Vaporアプリを開始と停止するコマンドを追加するには、[Supervisor](supervisor.md)の手順を確認してください。
|
||||
|
||||
## プロキシの設定 {#configure-proxy}
|
||||
|
||||
有効なサイトの設定ファイルは`/etc/nginx/sites-enabled/`にあります。
|
||||
|
||||
新しいファイルを作成するか、`/etc/nginx/sites-available/`からサンプルテンプレートをコピーして始めます。
|
||||
|
||||
以下は、ホームディレクトリにある`Hello`というVaporプロジェクトの設定ファイルの例です。
|
||||
|
||||
```sh
|
||||
server {
|
||||
server_name hello.com;
|
||||
listen 80;
|
||||
|
||||
root /home/vapor/Hello/Public/;
|
||||
|
||||
location @proxy {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_pass_header Server;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_connect_timeout 3s;
|
||||
proxy_read_timeout 10s;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
この設定ファイルは、`Hello`プロジェクトがプロダクションモードで起動したときにポート`8080`にバインドすることを前提としています。
|
||||
|
||||
### ファイルの提供 {#serving-files}
|
||||
|
||||
NginxはVaporアプリに尋ねることなくパブリックファイルを提供することもできます。これにより、高負荷時にVaporプロセスを他のタスクのために解放し、パフォーマンスを向上させることができます。
|
||||
|
||||
```sh
|
||||
server {
|
||||
...
|
||||
|
||||
# すべてのpublic/staticファイルをnginx経由で提供し、残りはVaporにフォールバック
|
||||
location / {
|
||||
try_files $uri @proxy;
|
||||
}
|
||||
|
||||
location @proxy {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### TLS
|
||||
|
||||
証明書が適切に生成されていれば、TLSの追加は比較的簡単です。無料でTLS証明書を生成するには、[Let's Encrypt](https://letsencrypt.org/ja/getting-started/)を確認してください。
|
||||
|
||||
```sh
|
||||
server {
|
||||
...
|
||||
|
||||
listen 443 ssl;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/hello.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/hello.com/privkey.pem;
|
||||
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_dhparam /etc/ssl/certs/dhparam.pem;
|
||||
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:50m;
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
add_header Strict-Transport-Security max-age=15768000;
|
||||
|
||||
...
|
||||
|
||||
location @proxy {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
上記の設定は、NginxでのTLSの比較的厳格な設定です。ここにある設定の一部は必須ではありませんが、セキュリティを強化します。
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
# Supervisor
|
||||
|
||||
[Supervisor](http://supervisord.org)は、Vaporアプリの起動、停止、再起動を簡単に行えるプロセス制御システムです。
|
||||
|
||||
## インストール {#install}
|
||||
|
||||
SupervisorはLinuxのパッケージマネージャーからインストールできます。
|
||||
|
||||
### Ubuntu
|
||||
|
||||
```sh
|
||||
sudo apt-get update
|
||||
sudo apt-get install supervisor
|
||||
```
|
||||
|
||||
### CentOSとAmazon Linux {#centos-and-amazon-linux}
|
||||
|
||||
```sh
|
||||
sudo yum install supervisor
|
||||
```
|
||||
|
||||
### Fedora
|
||||
|
||||
```sh
|
||||
sudo dnf install supervisor
|
||||
```
|
||||
|
||||
## 設定 {#configure}
|
||||
|
||||
サーバー上の各Vaporアプリには独自の設定ファイルが必要です。例として`Hello`プロジェクトの場合、設定ファイルは`/etc/supervisor/conf.d/hello.conf`に配置されます。
|
||||
|
||||
```sh
|
||||
[program:hello]
|
||||
command=/home/vapor/hello/.build/release/App serve --env production
|
||||
directory=/home/vapor/hello/
|
||||
user=vapor
|
||||
stdout_logfile=/var/log/supervisor/%(program_name)s-stdout.log
|
||||
stderr_logfile=/var/log/supervisor/%(program_name)s-stderr.log
|
||||
```
|
||||
|
||||
設定ファイルで指定されているように、`Hello`プロジェクトはユーザー`vapor`のホームフォルダに配置されています。`directory`が`Package.swift`ファイルのあるプロジェクトのルートディレクトリを指していることを確認してください。
|
||||
|
||||
`--env production`フラグは冗長なログを無効にします。
|
||||
|
||||
### 環境変数 {#environment}
|
||||
|
||||
supervisorを使ってVaporアプリに変数をエクスポートできます。複数の環境値をエクスポートする場合は、すべて1行に記述します。[Supervisorドキュメント](http://supervisord.org/configuration.html#program-x-section-values)によると:
|
||||
|
||||
> 英数字以外の文字を含む値は引用符で囲む必要があります(例:KEY="val:123",KEY2="val,456")。それ以外の場合、値を引用符で囲むことは任意ですが推奨されます。
|
||||
|
||||
```sh
|
||||
environment=PORT=8123,ANOTHERVALUE="/something/else"
|
||||
```
|
||||
|
||||
エクスポートされた変数は、Vaporで`Environment.get`を使用して利用できます。
|
||||
|
||||
```swift
|
||||
let port = Environment.get("PORT")
|
||||
```
|
||||
|
||||
## 起動 {#start}
|
||||
|
||||
これでアプリをロードして起動できます。
|
||||
|
||||
```sh
|
||||
supervisorctl reread
|
||||
supervisorctl add hello
|
||||
supervisorctl start hello
|
||||
```
|
||||
|
||||
!!! note
|
||||
`add`コマンドはすでにアプリを起動している可能性があります。
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
# Systemd
|
||||
|
||||
Systemdは、ほとんどのLinuxディストリビューションにおけるデフォルトのシステムおよびサービスマネージャーです。通常はデフォルトでインストールされているため、サポートされているSwiftディストリビューションでは追加のインストールは必要ありません。
|
||||
|
||||
## 設定 {#configure}
|
||||
|
||||
サーバー上の各Vaporアプリには独自のサービスファイルが必要です。例えば`Hello`プロジェクトの場合、設定ファイルは`/etc/systemd/system/hello.service`に配置されます。このファイルは以下のようになります:
|
||||
|
||||
```sh
|
||||
[Unit]
|
||||
Description=Hello
|
||||
Requires=network.target
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=vapor
|
||||
Group=vapor
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
WorkingDirectory=/home/vapor/hello
|
||||
ExecStart=/home/vapor/hello/.build/release/App serve --env production
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=vapor-hello
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
設定ファイルで指定されているように、`Hello`プロジェクトはユーザー`vapor`のホームフォルダーに配置されています。`WorkingDirectory`が`Package.swift`ファイルがあるプロジェクトのルートディレクトリを指していることを確認してください。
|
||||
|
||||
`--env production`フラグは詳細なログ出力を無効にします。
|
||||
|
||||
### 環境変数 {#environment}
|
||||
値のクォートは任意ですが、推奨されます。
|
||||
|
||||
systemd経由で変数をエクスポートする方法は2つあります。すべての変数が設定された環境ファイルを作成する方法:
|
||||
|
||||
```sh
|
||||
EnvironmentFile=/path/to/environment/file1
|
||||
EnvironmentFile=/path/to/environment/file2
|
||||
```
|
||||
|
||||
または、`[service]`セクションの下のサービスファイルに直接追加する方法:
|
||||
|
||||
```sh
|
||||
Environment="PORT=8123"
|
||||
Environment="ANOTHERVALUE=/something/else"
|
||||
```
|
||||
エクスポートされた変数は、`Environment.get`を使用してVaporで使用できます
|
||||
|
||||
```swift
|
||||
let port = Environment.get("PORT")
|
||||
```
|
||||
|
||||
## 起動 {#start}
|
||||
|
||||
rootとして以下のコマンドを実行することで、アプリのロード、有効化、開始、停止、再起動ができます。
|
||||
|
||||
```sh
|
||||
systemctl daemon-reload
|
||||
systemctl enable hello
|
||||
systemctl start hello
|
||||
systemctl stop hello
|
||||
systemctl restart hello
|
||||
```
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
# 高度な使い方 {#advanced}
|
||||
|
||||
Fluentは、データを扱うための汎用的でデータベースに依存しないAPIの作成を目指しています。これにより、どのデータベースドライバーを使用しているかに関わらず、Fluentを学習しやすくなります。汎用的なAPIを作成することで、Swiftでデータベースを扱う際により自然に感じられるようになります。
|
||||
|
||||
しかし、Fluentでまだサポートされていない基礎となるデータベースドライバーの機能を使用する必要がある場合があります。このガイドでは、特定のデータベースでのみ動作するFluentの高度なパターンとAPIについて説明します。
|
||||
|
||||
## SQL {#sql}
|
||||
|
||||
Fluentのすべての SQLデータベースドライバーは[SQLKit](https://github.com/vapor/sql-kit)上に構築されています。この汎用SQL実装は、`FluentSQL`モジュールでFluentと共に提供されています。
|
||||
|
||||
### SQLデータベース {#sql-database}
|
||||
|
||||
任意のFluent `Database`は`SQLDatabase`にキャストできます。これには`req.db`、`app.db`、`Migration`に渡される`database`などが含まれます。
|
||||
|
||||
```swift
|
||||
import FluentSQL
|
||||
|
||||
if let sql = req.db as? SQLDatabase {
|
||||
// 基礎となるデータベースドライバーはSQLです。
|
||||
let planets = try await sql.raw("SELECT * FROM planets").all(decoding: Planet.self)
|
||||
} else {
|
||||
// 基礎となるデータベースドライバーはSQLではありません。
|
||||
}
|
||||
```
|
||||
|
||||
このキャストは、基礎となるデータベースドライバーがSQLデータベースである場合にのみ機能します。`SQLDatabase`のメソッドについては、[SQLKitのREADME](https://github.com/vapor/sql-kit)で詳しく学べます。
|
||||
|
||||
### 特定のSQLデータベース {#specific-sql-database}
|
||||
|
||||
ドライバーをインポートすることで、特定のSQLデータベースにキャストすることもできます。
|
||||
|
||||
```swift
|
||||
import FluentPostgresDriver
|
||||
|
||||
if let postgres = req.db as? PostgresDatabase {
|
||||
// 基礎となるデータベースドライバーはPostgreSQLです。
|
||||
postgres.simpleQuery("SELECT * FROM planets").all()
|
||||
} else {
|
||||
// 基礎となるデータベースはPostgreSQLではありません。
|
||||
}
|
||||
```
|
||||
|
||||
執筆時点で、以下のSQLドライバーがサポートされています。
|
||||
|
||||
|データベース|ドライバー|ライブラリ|
|
||||
|-|-|-|
|
||||
|`PostgresDatabase`|[vapor/fluent-postgres-driver](https://github.com/vapor/fluent-postgres-driver)|[vapor/postgres-nio](https://github.com/vapor/postgres-nio)|
|
||||
|`MySQLDatabase`|[vapor/fluent-mysql-driver](https://github.com/vapor/fluent-mysql-driver)|[vapor/mysql-nio](https://github.com/vapor/mysql-nio)|
|
||||
|`SQLiteDatabase`|[vapor/fluent-sqlite-driver](https://github.com/vapor/fluent-sqlite-driver)|[vapor/sqlite-nio](https://github.com/vapor/sqlite-nio)|
|
||||
|
||||
データベース固有のAPIについての詳細は、各ライブラリのREADMEをご覧ください。
|
||||
|
||||
### SQLカスタム {#sql-custom}
|
||||
|
||||
Fluentのクエリとスキーマタイプのほぼすべてが`.custom`ケースをサポートしています。これにより、Fluentがまだサポートしていないデータベース機能を利用できます。
|
||||
|
||||
```swift
|
||||
import FluentPostgresDriver
|
||||
|
||||
let query = Planet.query(on: req.db)
|
||||
if req.db is PostgresDatabase {
|
||||
// ILIKEがサポートされています。
|
||||
query.filter(\.$name, .custom("ILIKE"), "earth")
|
||||
} else {
|
||||
// ILIKEはサポートされていません。
|
||||
query.group(.or) { or in
|
||||
or.filter(\.$name == "earth").filter(\.$name == "Earth")
|
||||
}
|
||||
}
|
||||
query.all()
|
||||
```
|
||||
|
||||
SQLデータベースは、すべての`.custom`ケースで`String`と`SQLExpression`の両方をサポートしています。`FluentSQL`モジュールは、一般的な使用例のための便利なメソッドを提供しています。
|
||||
|
||||
```swift
|
||||
import FluentSQL
|
||||
|
||||
let query = Planet.query(on: req.db)
|
||||
if req.db is SQLDatabase {
|
||||
// 基礎となるデータベースドライバーはSQLです。
|
||||
query.filter(.sql(raw: "LOWER(name) = 'earth'"))
|
||||
} else {
|
||||
// 基礎となるデータベースドライバーはSQLではありません。
|
||||
}
|
||||
```
|
||||
|
||||
以下は、スキーマビルダーで`.sql(raw:)`の便利な機能を介して`.custom`を使用する例です。
|
||||
|
||||
```swift
|
||||
import FluentSQL
|
||||
|
||||
let builder = database.schema("planets").id()
|
||||
if database is MySQLDatabase {
|
||||
// 基礎となるデータベースドライバーはMySQLです。
|
||||
builder.field("name", .sql(raw: "VARCHAR(64)"), .required)
|
||||
} else {
|
||||
// 基礎となるデータベースドライバーはMySQLではありません。
|
||||
builder.field("name", .string, .required)
|
||||
}
|
||||
builder.create()
|
||||
```
|
||||
|
||||
## MongoDB {#mongodb}
|
||||
|
||||
Fluent MongoDBは、[Fluent](../fluent/overview.md)と[MongoKitten](https://github.com/OpenKitten/MongoKitten/)ドライバー間の統合です。Swiftの強力な型システムとFluentのデータベース非依存インターフェースをMongoDBで活用します。
|
||||
|
||||
MongoDBで最も一般的な識別子はObjectIdです。`@ID(custom: .id)`を使用してプロジェクトでこれを使用できます。
|
||||
SQLで同じモデルを使用する必要がある場合は、`ObjectId`を使用しないでください。代わりに`UUID`を使用してください。
|
||||
|
||||
```swift
|
||||
final class User: Model {
|
||||
// テーブルまたはコレクションの名前。
|
||||
static let schema = "users"
|
||||
|
||||
// このUserの一意識別子。
|
||||
// この場合、ObjectIdが使用されています
|
||||
// Fluentはデフォルトで UUID の使用を推奨しますが、ObjectIdもサポートされています
|
||||
@ID(custom: .id)
|
||||
var id: ObjectId?
|
||||
|
||||
// ユーザーのメールアドレス
|
||||
@Field(key: "email")
|
||||
var email: String
|
||||
|
||||
// BCryptハッシュとして保存されるユーザーのパスワード
|
||||
@Field(key: "password")
|
||||
var passwordHash: String
|
||||
|
||||
// Fluentが使用するための新しい空のUserインスタンスを作成します
|
||||
init() { }
|
||||
|
||||
// すべてのプロパティが設定された新しいUserを作成します。
|
||||
init(id: ObjectId? = nil, email: String, passwordHash: String, profile: Profile) {
|
||||
self.id = id
|
||||
self.email = email
|
||||
self.passwordHash = passwordHash
|
||||
self.profile = profile
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### データモデリング {#data-modelling}
|
||||
|
||||
MongoDBでは、モデルは他のFluent環境と同じように定義されます。SQLデータベースとMongoDBの主な違いは、リレーションシップとアーキテクチャにあります。
|
||||
|
||||
SQL環境では、2つのエンティティ間の関係のために結合テーブルを作成することが非常に一般的です。しかし、MongoDBでは、配列を使用して関連する識別子を保存できます。MongoDBの設計により、ネストされたデータ構造でモデルを設計する方がより効率的で実用的です。
|
||||
|
||||
### 柔軟なデータ {#flexible-data}
|
||||
|
||||
MongoDBでは柔軟なデータを追加できますが、このコードはSQL環境では動作しません。
|
||||
グループ化された任意のデータストレージを作成するには、`Document`を使用できます。
|
||||
|
||||
```swift
|
||||
@Field(key: "document")
|
||||
var document: Document
|
||||
```
|
||||
|
||||
Fluentはこれらの値に対する厳密に型付けされたクエリをサポートできません。クエリでドット記法のキーパスを使用できます。
|
||||
これは、ネストされた値にアクセスするためにMongoDBで受け入れられています。
|
||||
|
||||
```swift
|
||||
Something.query(on: db).filter("document.key", .equal, 5).first()
|
||||
```
|
||||
|
||||
### 正規表現の使用 {#use-of-regular-expressions}
|
||||
|
||||
`.custom()`ケースを使用し、正規表現を渡してMongoDBをクエリできます。[MongoDB](https://www.mongodb.com/docs/manual/reference/operator/query/regex/)はPerl互換の正規表現を受け入れます。
|
||||
|
||||
例えば、`name`フィールドで大文字と小文字を区別しない文字をクエリできます:
|
||||
|
||||
```swift
|
||||
import FluentMongoDriver
|
||||
|
||||
var queryDocument = Document()
|
||||
queryDocument["name"]["$regex"] = "e"
|
||||
queryDocument["name"]["$options"] = "i"
|
||||
|
||||
let planets = try Planet.query(on: req.db).filter(.custom(queryDocument)).all()
|
||||
```
|
||||
|
||||
これは'e'と'E'を含む惑星を返します。MongoDBが受け入れる他の複雑な正規表現も作成できます。
|
||||
|
||||
### 生のアクセス {#raw-access}
|
||||
|
||||
生の`MongoDatabase`インスタンスにアクセスするには、データベースインスタンスを`MongoDatabaseRepresentable`にキャストします:
|
||||
|
||||
```swift
|
||||
guard let db = req.db as? MongoDatabaseRepresentable else {
|
||||
throw Abort(.internalServerError)
|
||||
}
|
||||
|
||||
let mongodb = db.raw
|
||||
```
|
||||
|
||||
ここから、すべてのMongoKitten APIを使用できます。
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
# マイグレーション {#migrations}
|
||||
|
||||
マイグレーションは、データベースのバージョン管理システムのようなものです。各マイグレーションは、データベースへの変更とその取り消し方法を定義します。マイグレーションを通じてデータベースを変更することで、時間の経過とともにデータベースを進化させる一貫性のある、テスト可能で、共有可能な方法を作成します。
|
||||
|
||||
```swift
|
||||
// マイグレーションの例
|
||||
struct MyMigration: Migration {
|
||||
func prepare(on database: Database) -> EventLoopFuture<Void> {
|
||||
// データベースに変更を加える
|
||||
}
|
||||
|
||||
func revert(on database: Database) -> EventLoopFuture<Void> {
|
||||
// `prepare`で行った変更を取り消す(可能な場合)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`async`/`await`を使用している場合は、`AsyncMigration`プロトコルを実装する必要があります:
|
||||
|
||||
```swift
|
||||
struct MyMigration: AsyncMigration {
|
||||
func prepare(on database: Database) async throws {
|
||||
// データベースに変更を加える
|
||||
}
|
||||
|
||||
func revert(on database: Database) async throws {
|
||||
// `prepare`で行った変更を取り消す(可能な場合)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`prepare`メソッドは、提供された`Database`に変更を加える場所です。これらは、テーブルやコレクション、フィールド、制約の追加や削除などのデータベーススキーマへの変更である可能性があります。また、新しいモデルインスタンスの作成、フィールド値の更新、クリーンアップなど、データベースの内容を変更することもできます。
|
||||
|
||||
`revert`メソッドは、可能であればこれらの変更を元に戻す場所です。マイグレーションを元に戻せることで、プロトタイピングとテストが容易になります。また、本番環境へのデプロイが計画通りに進まなかった場合のバックアッププランも提供します。
|
||||
|
||||
## 登録 {#register}
|
||||
|
||||
マイグレーションは`app.migrations`を使用してアプリケーションに登録されます。
|
||||
|
||||
```swift
|
||||
import Fluent
|
||||
import Vapor
|
||||
|
||||
app.migrations.add(MyMigration())
|
||||
```
|
||||
|
||||
`to`パラメータを使用して特定のデータベースにマイグレーションを追加できます。それ以外の場合は、デフォルトのデータベースが使用されます。
|
||||
|
||||
```swift
|
||||
app.migrations.add(MyMigration(), to: .myDatabase)
|
||||
```
|
||||
|
||||
マイグレーションは依存関係の順序でリストする必要があります。例えば、`MigrationB`が`MigrationA`に依存している場合、`app.migrations`に2番目に追加する必要があります。
|
||||
|
||||
## マイグレート {#migrate}
|
||||
|
||||
データベースをマイグレートするには、`migrate`コマンドを実行します。
|
||||
|
||||
```sh
|
||||
swift run App migrate
|
||||
```
|
||||
|
||||
このコマンドは[Xcodeから実行](../advanced/commands.md#xcode)することもできます。migrateコマンドは、最後に実行されてから新しいマイグレーションが登録されているかデータベースをチェックします。新しいマイグレーションがある場合は、実行前に確認を求めます。
|
||||
|
||||
### リバート {#revert}
|
||||
|
||||
データベースのマイグレーションを元に戻すには、`--revert`フラグを付けて`migrate`を実行します。
|
||||
|
||||
```sh
|
||||
swift run App migrate --revert
|
||||
```
|
||||
|
||||
このコマンドは、最後に実行されたマイグレーションのバッチをデータベースでチェックし、それらを元に戻す前に確認を求めます。
|
||||
|
||||
### 自動マイグレート {#auto-migrate}
|
||||
|
||||
他のコマンドを実行する前にマイグレーションを自動的に実行したい場合は、`--auto-migrate`フラグを渡すことができます。
|
||||
|
||||
```sh
|
||||
swift run App serve --auto-migrate
|
||||
```
|
||||
|
||||
プログラムで実行することもできます。
|
||||
|
||||
```swift
|
||||
try app.autoMigrate().wait()
|
||||
|
||||
// または
|
||||
try await app.autoMigrate()
|
||||
```
|
||||
|
||||
これらのオプションは両方ともリバートにも存在します:`--auto-revert`と`app.autoRevert()`。
|
||||
|
||||
## 次のステップ {#next-steps}
|
||||
|
||||
マイグレーション内に何を記述するかについての詳細は、[スキーマビルダー](schema.md)と[クエリビルダー](query.md)のガイドを参照してください。
|
||||
|
|
@ -0,0 +1,596 @@
|
|||
# モデル {#models}
|
||||
|
||||
モデルは、データベースのテーブルやコレクションに格納されたデータを表現します。モデルは、コード化可能な値を格納する1つ以上のフィールドを持ちます。すべてのモデルには一意の識別子があります。プロパティラッパーは、識別子、フィールド、リレーションを示すために使用されます。
|
||||
|
||||
以下は、1つのフィールドを持つシンプルなモデルの例です。モデルは、制約、インデックス、外部キーなどのデータベーススキーマ全体を記述するものではないことに注意してください。スキーマは[マイグレーション](migration.md)で定義されます。モデルは、データベーススキーマに格納されているデータの表現に焦点を当てています。
|
||||
|
||||
```swift
|
||||
final class Planet: Model {
|
||||
// テーブルまたはコレクションの名前
|
||||
static let schema = "planets"
|
||||
|
||||
// このPlanetの一意の識別子
|
||||
@ID(key: .id)
|
||||
var id: UUID?
|
||||
|
||||
// Planetの名前
|
||||
@Field(key: "name")
|
||||
var name: String
|
||||
|
||||
// 新しい空のPlanetを作成
|
||||
init() { }
|
||||
|
||||
// すべてのプロパティが設定された新しいPlanetを作成
|
||||
init(id: UUID? = nil, name: String) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## スキーマ {#schema}
|
||||
|
||||
すべてのモデルには、静的なgetオンリーの`schema`プロパティが必要です。この文字列は、このモデルが表すテーブルまたはコレクションの名前を参照します。
|
||||
|
||||
```swift
|
||||
final class Planet: Model {
|
||||
// テーブルまたはコレクションの名前
|
||||
static let schema = "planets"
|
||||
}
|
||||
```
|
||||
|
||||
このモデルをクエリする際、データは`"planets"`という名前のスキーマから取得され、格納されます。
|
||||
|
||||
!!! tip
|
||||
スキーマ名は通常、クラス名を複数形にして小文字にしたものです。
|
||||
|
||||
## 識別子 {#identifier}
|
||||
|
||||
すべてのモデルには、`@ID`プロパティラッパーを使用して定義された`id`プロパティが必要です。このフィールドは、モデルのインスタンスを一意に識別します。
|
||||
|
||||
```swift
|
||||
final class Planet: Model {
|
||||
// このPlanetの一意の識別子
|
||||
@ID(key: .id)
|
||||
var id: UUID?
|
||||
}
|
||||
```
|
||||
|
||||
デフォルトでは、`@ID`プロパティは特別な`.id`キーを使用する必要があります。これは、基礎となるデータベースドライバーに適したキーに解決されます。SQLの場合は`"id"`、NoSQLの場合は`"_id"`です。
|
||||
|
||||
`@ID`は`UUID`型である必要があります。これは現在、すべてのデータベースドライバーでサポートされている唯一の識別子値です。Fluentは、モデルが作成されるときに新しいUUID識別子を自動的に生成します。
|
||||
|
||||
`@ID`は、保存されていないモデルにはまだ識別子がない可能性があるため、オプショナル値です。識別子を取得するか、エラーをスローするには、`requireID`を使用します。
|
||||
|
||||
```swift
|
||||
let id = try planet.requireID()
|
||||
```
|
||||
|
||||
### 存在確認 {#exists}
|
||||
|
||||
`@ID`には、モデルがデータベースに存在するかどうかを表す`exists`プロパティがあります。モデルを初期化すると、値は`false`です。モデルを保存した後、またはデータベースからモデルをフェッチしたときは、値は`true`になります。このプロパティは変更可能です。
|
||||
|
||||
```swift
|
||||
if planet.$id.exists {
|
||||
// このモデルはデータベースに存在します
|
||||
}
|
||||
```
|
||||
|
||||
### カスタム識別子 {#custom-identifier}
|
||||
|
||||
Fluentは、`@ID(custom:)`オーバーロードを使用して、カスタム識別子キーと型をサポートします。
|
||||
|
||||
```swift
|
||||
final class Planet: Model {
|
||||
// このPlanetの一意の識別子
|
||||
@ID(custom: "foo")
|
||||
var id: Int?
|
||||
}
|
||||
```
|
||||
|
||||
上記の例では、カスタムキー`"foo"`と識別子型`Int`を持つ`@ID`を使用しています。これは自動インクリメントのプライマリキーを使用するSQLデータベースと互換性がありますが、NoSQLとは互換性がありません。
|
||||
|
||||
カスタム`@ID`では、`generatedBy`パラメータを使用して識別子の生成方法を指定できます。
|
||||
|
||||
```swift
|
||||
@ID(custom: "foo", generatedBy: .user)
|
||||
```
|
||||
|
||||
`generatedBy`パラメータは以下のケースをサポートします:
|
||||
|
||||
|生成方法|説明|
|
||||
|-|-|
|
||||
|`.user`|新しいモデルを保存する前に`@ID`プロパティが設定されることが期待される|
|
||||
|`.random`|`@ID`値型は`RandomGeneratable`に準拠する必要がある|
|
||||
|`.database`|データベースが保存時に値を生成することが期待される|
|
||||
|
||||
`generatedBy`パラメータが省略された場合、Fluentは`@ID`値型に基づいて適切なケースを推測しようとします。例えば、`Int`は特に指定されない限り、デフォルトで`.database`生成になります。
|
||||
|
||||
## イニシャライザ {#initializer}
|
||||
|
||||
モデルには空のイニシャライザメソッドが必要です。
|
||||
|
||||
```swift
|
||||
final class Planet: Model {
|
||||
// 新しい空のPlanetを作成
|
||||
init() { }
|
||||
}
|
||||
```
|
||||
|
||||
Fluentは、クエリによって返されたモデルを初期化するために、内部的にこのメソッドを必要とします。また、リフレクションにも使用されます。
|
||||
|
||||
すべてのプロパティを受け入れるコンビニエンスイニシャライザをモデルに追加することもできます。
|
||||
|
||||
```swift
|
||||
final class Planet: Model {
|
||||
// すべてのプロパティが設定された新しいPlanetを作成
|
||||
init(id: UUID? = nil, name: String) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
コンビニエンスイニシャライザを使用すると、将来モデルに新しいプロパティを追加しやすくなります。
|
||||
|
||||
## フィールド {#field}
|
||||
|
||||
モデルは、データを格納するために0個以上の`@Field`プロパティを持つことができます。
|
||||
|
||||
```swift
|
||||
final class Planet: Model {
|
||||
// Planetの名前
|
||||
@Field(key: "name")
|
||||
var name: String
|
||||
}
|
||||
```
|
||||
|
||||
フィールドには、データベースキーを明示的に定義する必要があります。これはプロパティ名と同じである必要はありません。
|
||||
|
||||
!!! tip
|
||||
Fluentでは、データベースキーには`snake_case`を、プロパティ名には`camelCase`を使用することを推奨しています。
|
||||
|
||||
フィールド値は、`Codable`に準拠する任意の型にできます。ネストされた構造体や配列を`@Field`に格納することはサポートされていますが、フィルタリング操作は制限されます。代替案については[`@Group`](#group)を参照してください。
|
||||
|
||||
オプショナル値を含むフィールドには、`@OptionalField`を使用します。
|
||||
|
||||
```swift
|
||||
@OptionalField(key: "tag")
|
||||
var tag: String?
|
||||
```
|
||||
|
||||
!!! warning
|
||||
現在の値を参照する`willSet`プロパティオブザーバー、または`oldValue`を参照する`didSet`プロパティオブザーバーを持つ非オプショナルフィールドは、致命的なエラーを引き起こします。
|
||||
|
||||
## リレーション {#relations}
|
||||
|
||||
モデルは、`@Parent`、`@Children`、`@Siblings`など、他のモデルを参照する0個以上のリレーションプロパティを持つことができます。リレーションの詳細については、[リレーション](relations.md)セクションを参照してください。
|
||||
|
||||
## タイムスタンプ {#timestamp}
|
||||
|
||||
`@Timestamp`は、`Foundation.Date`を格納する特別な種類の`@Field`です。タイムスタンプは、選択されたトリガーに応じてFluentによって自動的に設定されます。
|
||||
|
||||
```swift
|
||||
final class Planet: Model {
|
||||
// このPlanetが作成されたとき
|
||||
@Timestamp(key: "created_at", on: .create)
|
||||
var createdAt: Date?
|
||||
|
||||
// このPlanetが最後に更新されたとき
|
||||
@Timestamp(key: "updated_at", on: .update)
|
||||
var updatedAt: Date?
|
||||
}
|
||||
```
|
||||
|
||||
`@Timestamp`は以下のトリガーをサポートします。
|
||||
|
||||
|トリガー|説明|
|
||||
|-|-|
|
||||
|`.create`|新しいモデルインスタンスがデータベースに保存されるときに設定される|
|
||||
|`.update`|既存のモデルインスタンスがデータベースに保存されるときに設定される|
|
||||
|`.delete`|モデルがデータベースから削除されるときに設定される。[論理削除](#soft-delete)を参照|
|
||||
|
||||
`@Timestamp`の日付値はオプショナルで、新しいモデルを初期化するときは`nil`に設定する必要があります。
|
||||
|
||||
### タイムスタンプフォーマット {#timestamp-format}
|
||||
|
||||
デフォルトでは、`@Timestamp`はデータベースドライバーに基づいた効率的な`datetime`エンコーディングを使用します。`format`パラメータを使用して、タイムスタンプがデータベースに格納される方法をカスタマイズできます。
|
||||
|
||||
```swift
|
||||
// このモデルが最後に更新されたときを表す
|
||||
// ISO 8601形式のタイムスタンプを格納
|
||||
@Timestamp(key: "updated_at", on: .update, format: .iso8601)
|
||||
var updatedAt: Date?
|
||||
```
|
||||
|
||||
この`.iso8601`の例に関連するマイグレーションでは、`.string`形式でのストレージが必要になることに注意してください。
|
||||
|
||||
```swift
|
||||
.field("updated_at", .string)
|
||||
```
|
||||
|
||||
利用可能なタイムスタンプフォーマットを以下に示します。
|
||||
|
||||
|フォーマット|説明|型|
|
||||
|-|-|-|
|
||||
|`.default`|特定のデータベース用の効率的な`datetime`エンコーディングを使用|Date|
|
||||
|`.iso8601`|[ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)文字列。`withMilliseconds`パラメータをサポート|String|
|
||||
|`.unix`|小数部を含むUnixエポックからの秒数|Double|
|
||||
|
||||
`timestamp`プロパティを使用して、生のタイムスタンプ値に直接アクセスできます。
|
||||
|
||||
```swift
|
||||
// このISO 8601形式の@Timestampに
|
||||
// タイムスタンプ値を手動で設定
|
||||
model.$updatedAt.timestamp = "2020-06-03T16:20:14+00:00"
|
||||
```
|
||||
|
||||
### 論理削除 {#soft-delete}
|
||||
|
||||
`.delete`トリガーを使用する`@Timestamp`をモデルに追加すると、論理削除が有効になります。
|
||||
|
||||
```swift
|
||||
final class Planet: Model {
|
||||
// このPlanetが削除されたとき
|
||||
@Timestamp(key: "deleted_at", on: .delete)
|
||||
var deletedAt: Date?
|
||||
}
|
||||
```
|
||||
|
||||
論理削除されたモデルは削除後もデータベースに存在しますが、クエリでは返されません。
|
||||
|
||||
!!! tip
|
||||
削除時のタイムスタンプを将来の日付に手動で設定できます。これは有効期限として使用できます。
|
||||
|
||||
論理削除可能なモデルをデータベースから強制的に削除するには、`delete`の`force`パラメータを使用します。
|
||||
|
||||
```swift
|
||||
// モデルが論理削除可能であっても
|
||||
// データベースから削除する
|
||||
model.delete(force: true, on: database)
|
||||
```
|
||||
|
||||
論理削除されたモデルを復元するには、`restore`メソッドを使用します。
|
||||
|
||||
```swift
|
||||
// 削除時のタイムスタンプをクリアして、
|
||||
// このモデルがクエリで返されるようにする
|
||||
model.restore(on: database)
|
||||
```
|
||||
|
||||
クエリに論理削除されたモデルを含めるには、`withDeleted`を使用します。
|
||||
|
||||
```swift
|
||||
// 論理削除されたものを含むすべての惑星を取得
|
||||
Planet.query(on: database).withDeleted().all()
|
||||
```
|
||||
|
||||
## Enum
|
||||
|
||||
`@Enum`は、文字列表現可能な型をネイティブデータベース列挙型として格納する特別な種類の`@Field`です。ネイティブデータベース列挙型は、データベースに型安全性の追加レイヤーを提供し、生の列挙型よりもパフォーマンスが向上する可能性があります。
|
||||
|
||||
```swift
|
||||
// 動物の種類を表す文字列表現可能なCodable列挙型
|
||||
enum Animal: String, Codable {
|
||||
case dog, cat
|
||||
}
|
||||
|
||||
final class Pet: Model {
|
||||
// 動物の種類をネイティブデータベース列挙型として格納
|
||||
@Enum(key: "type")
|
||||
var type: Animal
|
||||
}
|
||||
```
|
||||
|
||||
`RawValue`が`String`である`RawRepresentable`に準拠する型のみが`@Enum`と互換性があります。`String`バックの列挙型はデフォルトでこの要件を満たしています。
|
||||
|
||||
オプショナルの列挙型を格納するには、`@OptionalEnum`を使用します。
|
||||
|
||||
データベースは、マイグレーションを介して列挙型を処理する準備が必要です。詳細については[Enum](schema.md#enum)を参照してください。
|
||||
|
||||
### 生の列挙型 {#raw-enums}
|
||||
|
||||
`String`や`Int`などの`Codable`型でバックされた列挙型は、`@Field`に格納できます。データベースには生の値として格納されます。
|
||||
|
||||
## グループ {#group}
|
||||
|
||||
`@Group`を使用すると、ネストされたフィールドのグループをモデルの単一のプロパティとして格納できます。`@Field`に格納されたCodable構造体とは異なり、`@Group`のフィールドはクエリ可能です。Fluentは、`@Group`をデータベースにフラットな構造として格納することでこれを実現しています。
|
||||
|
||||
`@Group`を使用するには、まず`Fields`プロトコルを使用して格納したいネストされた構造を定義します。これは`Model`に非常に似ていますが、識別子やスキーマ名は必要ありません。ここでは、`@Field`、`@Enum`、さらには別の`@Group`など、`Model`がサポートする多くのプロパティを格納できます。
|
||||
|
||||
```swift
|
||||
// 名前と動物の種類を持つペット
|
||||
final class Pet: Fields {
|
||||
// ペットの名前
|
||||
@Field(key: "name")
|
||||
var name: String
|
||||
|
||||
// ペットの種類
|
||||
@Field(key: "type")
|
||||
var type: String
|
||||
|
||||
// 新しい空のPetを作成
|
||||
init() { }
|
||||
}
|
||||
```
|
||||
|
||||
フィールド定義を作成したら、それを`@Group`プロパティの値として使用できます。
|
||||
|
||||
```swift
|
||||
final class User: Model {
|
||||
// ユーザーのネストされたペット
|
||||
@Group(key: "pet")
|
||||
var pet: Pet
|
||||
}
|
||||
```
|
||||
|
||||
`@Group`のフィールドはドット構文でアクセスできます。
|
||||
|
||||
```swift
|
||||
let user: User = ...
|
||||
print(user.pet.name) // String
|
||||
```
|
||||
|
||||
プロパティラッパーのドット構文を使用して、通常どおりネストされたフィールドをクエリできます。
|
||||
|
||||
```swift
|
||||
User.query(on: database).filter(\.$pet.$name == "Zizek").all()
|
||||
```
|
||||
|
||||
データベースでは、`@Group`は`_`で結合されたキーを持つフラットな構造として格納されます。以下は、`User`がデータベースでどのように見えるかの例です。
|
||||
|
||||
|id|name|pet_name|pet_type|
|
||||
|-|-|-|-|
|
||||
|1|Tanner|Zizek|Cat|
|
||||
|2|Logan|Runa|Dog|
|
||||
|
||||
## Codable {#codable}
|
||||
|
||||
モデルはデフォルトで`Codable`に準拠しています。つまり、`Content`プロトコルへの準拠を追加することで、モデルをVaporの[コンテンツAPI](../basics/content.md)で使用できます。
|
||||
|
||||
```swift
|
||||
extension Planet: Content { }
|
||||
|
||||
app.get("planets") { req async throws in
|
||||
// すべての惑星の配列を返す
|
||||
try await Planet.query(on: req.db).all()
|
||||
}
|
||||
```
|
||||
|
||||
`Codable`にシリアライズ/デシリアライズする際、モデルプロパティはキーの代わりに変数名を使用します。リレーションはネストされた構造としてシリアライズされ、イーガーロードされたデータが含まれます。
|
||||
|
||||
!!! info
|
||||
ほぼすべてのケースで、APIレスポンスとリクエストボディにはモデルの代わりにDTOを使用することをお勧めします。詳細については[データ転送オブジェクト](#data-transfer-object)を参照してください。
|
||||
|
||||
### データ転送オブジェクト {#data-transfer-object}
|
||||
|
||||
モデルのデフォルトの`Codable`準拠により、簡単な使用とプロトタイピングが容易になります。ただし、これは基礎となるデータベース情報をAPIに公開します。これは通常、セキュリティの観点(ユーザーのパスワードハッシュなどの機密フィールドを返すのは良くない)と使いやすさの観点の両方から望ましくありません。APIを破壊せずにデータベーススキーマを変更したり、異なる形式でデータを受け入れたり返したり、APIからフィールドを追加または削除したりすることが困難になります。
|
||||
|
||||
ほとんどの場合、モデルの代わりにDTO(データ転送オブジェクト)を使用する必要があります(これはドメイン転送オブジェクトとも呼ばれます)。DTOは、エンコードまたはデコードしたいデータ構造を表す別個の`Codable`型です。これらはAPIをデータベーススキーマから分離し、アプリの公開APIを破壊することなくモデルに変更を加えたり、異なるバージョンを持ったり、クライアントにとってAPIをより使いやすくしたりできます。
|
||||
|
||||
次の例では、以下の`User`モデルを想定しています。
|
||||
|
||||
```swift
|
||||
// 参照用の省略されたUserモデル
|
||||
final class User: Model {
|
||||
@ID(key: .id)
|
||||
var id: UUID?
|
||||
|
||||
@Field(key: "first_name")
|
||||
var firstName: String
|
||||
|
||||
@Field(key: "last_name")
|
||||
var lastName: String
|
||||
}
|
||||
```
|
||||
|
||||
DTOの一般的な使用例の1つは、`PATCH`リクエストの実装です。これらのリクエストには、更新する必要があるフィールドの値のみが含まれています。必要なフィールドが不足している場合、そのようなリクエストから`Model`を直接デコードしようとすると失敗します。以下の例では、DTOを使用してリクエストデータをデコードし、モデルを更新しています。
|
||||
|
||||
```swift
|
||||
// PATCH /users/:idリクエストの構造
|
||||
struct PatchUser: Decodable {
|
||||
var firstName: String?
|
||||
var lastName: String?
|
||||
}
|
||||
|
||||
app.patch("users", ":id") { req async throws -> User in
|
||||
// リクエストデータをデコード
|
||||
let patch = try req.content.decode(PatchUser.self)
|
||||
// データベースから目的のユーザーを取得
|
||||
guard let user = try await User.find(req.parameters.get("id"), on: req.db) else {
|
||||
throw Abort(.notFound)
|
||||
}
|
||||
// 名が提供された場合、更新する
|
||||
if let firstName = patch.firstName {
|
||||
user.firstName = firstName
|
||||
}
|
||||
// 新しい姓が提供された場合、更新する
|
||||
if let lastName = patch.lastName {
|
||||
user.lastName = lastName
|
||||
}
|
||||
// ユーザーを保存して返す
|
||||
try await user.save(on: req.db)
|
||||
return user
|
||||
}
|
||||
```
|
||||
|
||||
DTOのもう1つの一般的な使用例は、APIレスポンスの形式をカスタマイズすることです。以下の例は、DTOを使用してレスポンスに計算フィールドを追加する方法を示しています。
|
||||
|
||||
```swift
|
||||
// GET /usersレスポンスの構造
|
||||
struct GetUser: Content {
|
||||
var id: UUID
|
||||
var name: String
|
||||
}
|
||||
|
||||
app.get("users") { req async throws -> [GetUser] in
|
||||
// データベースからすべてのユーザーを取得
|
||||
let users = try await User.query(on: req.db).all()
|
||||
return try users.map { user in
|
||||
// 各ユーザーをGET戻り値型に変換
|
||||
try GetUser(
|
||||
id: user.requireID(),
|
||||
name: "\(user.firstName) \(user.lastName)"
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
もう1つの一般的な使用例は、親リレーションや子リレーションなどのリレーションを扱う場合です。`@Parent`リレーションを持つモデルを簡単にデコードするためのDTOの使用例については、[Parentドキュメント](relations.md##encoding-and-decoding-of-parents)を参照してください。
|
||||
|
||||
DTOの構造がモデルの`Codable`準拠と同じであっても、別の型として持つことで大規模なプロジェクトを整理できます。モデルのプロパティに変更を加える必要がある場合でも、アプリの公開APIを破壊する心配はありません。また、DTOをAPIの利用者と共有できる別のパッケージに配置し、VaporアプリでContent準拠を追加することも検討できます。
|
||||
|
||||
## エイリアス {#alias}
|
||||
|
||||
`ModelAlias`プロトコルを使用すると、クエリで複数回結合されるモデルを一意に識別できます。詳細については、[Join](query.md#join)を参照してください。
|
||||
|
||||
## Save
|
||||
|
||||
モデルをデータベースに保存するには、`save(on:)`メソッドを使用します。
|
||||
|
||||
```swift
|
||||
planet.save(on: database)
|
||||
```
|
||||
|
||||
このメソッドは、モデルがすでにデータベースに存在するかどうかに応じて、内部的に`create`または`update`を呼び出します。
|
||||
|
||||
### Create
|
||||
|
||||
新しいモデルをデータベースに保存するには、`create`メソッドを呼び出します。
|
||||
|
||||
```swift
|
||||
let planet = Planet(name: "Earth")
|
||||
planet.create(on: database)
|
||||
```
|
||||
|
||||
`create`はモデルの配列でも利用可能です。これにより、すべてのモデルが単一のバッチ/クエリでデータベースに保存されます。
|
||||
|
||||
```swift
|
||||
// バッチ作成の例
|
||||
[earth, mars].create(on: database)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
`.database`ジェネレーター(通常は自動インクリメントの`Int`)を使用する[`@ID(custom:)`](#custom-identifier)を使用するモデルは、バッチ作成後に新しく作成された識別子にアクセスできません。識別子にアクセスする必要がある状況では、各モデルで`create`を呼び出してください。
|
||||
|
||||
モデルの配列を個別に作成するには、`map` + `flatten`を使用します。
|
||||
|
||||
```swift
|
||||
[earth, mars].map { $0.create(on: database) }
|
||||
.flatten(on: database.eventLoop)
|
||||
```
|
||||
|
||||
`async`/`await`を使用している場合は、以下を使用できます:
|
||||
|
||||
```swift
|
||||
await withThrowingTaskGroup(of: Void.self) { taskGroup in
|
||||
[earth, mars].forEach { model in
|
||||
taskGroup.addTask { try await model.create(on: database) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Update
|
||||
|
||||
データベースから取得したモデルを保存するには、`update`メソッドを呼び出します。
|
||||
|
||||
```swift
|
||||
guard let planet = try await Planet.find(..., on: database) else {
|
||||
throw Abort(.notFound)
|
||||
}
|
||||
planet.name = "Earth"
|
||||
try await planet.update(on: database)
|
||||
```
|
||||
|
||||
モデルの配列を更新するには、`map` + `flatten`を使用します。
|
||||
|
||||
```swift
|
||||
[earth, mars].map { $0.update(on: database) }
|
||||
.flatten(on: database.eventLoop)
|
||||
|
||||
// TOOD
|
||||
```
|
||||
|
||||
## クエリ {#query}
|
||||
|
||||
モデルは、クエリビルダーを返す静的メソッド`query(on:)`を公開します。
|
||||
|
||||
```swift
|
||||
Planet.query(on: database).all()
|
||||
```
|
||||
|
||||
クエリの詳細については、[クエリ](query.md)セクションを参照してください。
|
||||
|
||||
## Find
|
||||
|
||||
モデルには、識別子でモデルインスタンスを検索するための静的`find(_:on:)`メソッドがあります。
|
||||
|
||||
```swift
|
||||
Planet.find(req.parameters.get("id"), on: database)
|
||||
```
|
||||
|
||||
その識別子を持つモデルが見つからない場合、このメソッドは`nil`を返します。
|
||||
|
||||
## ライフサイクル {#lifecycle}
|
||||
|
||||
モデルミドルウェアを使用すると、モデルのライフサイクルイベントにフックできます。以下のライフサイクルイベントがサポートされています。
|
||||
|
||||
|メソッド|説明|
|
||||
|-|-|
|
||||
|`create`|モデルが作成される前に実行される|
|
||||
|`update`|モデルが更新される前に実行される|
|
||||
|`delete(force:)`|モデルが削除される前に実行される|
|
||||
|`softDelete`|モデルが論理削除される前に実行される|
|
||||
|`restore`|モデルが復元される前に実行される(論理削除の反対)|
|
||||
|
||||
モデルミドルウェアは、`ModelMiddleware`または`AsyncModelMiddleware`プロトコルを使用して宣言されます。すべてのライフサイクルメソッドにはデフォルトの実装があるため、必要なメソッドのみを実装する必要があります。各メソッドは、対象のモデル、データベースへの参照、チェーン内の次のアクションを受け入れます。ミドルウェアは、早期に返す、失敗したfutureを返す、または次のアクションを呼び出して通常どおり続行することを選択できます。
|
||||
|
||||
これらのメソッドを使用すると、特定のイベントが完了する前と後の両方でアクションを実行できます。イベント完了後のアクションの実行は、次のレスポンダーから返されたfutureをマップすることで実行できます。
|
||||
|
||||
```swift
|
||||
// 名前を大文字化するミドルウェアの例
|
||||
struct PlanetMiddleware: ModelMiddleware {
|
||||
func create(model: Planet, on db: Database, next: AnyModelResponder) -> EventLoopFuture<Void> {
|
||||
// モデルは作成される前にここで変更できます
|
||||
model.name = model.name.capitalized()
|
||||
return next.create(model, on: db).map {
|
||||
// 惑星が作成されたら、ここのコードが実行されます
|
||||
print ("Planet \(model.name) was created")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
または`async`/`await`を使用する場合:
|
||||
|
||||
```swift
|
||||
struct PlanetMiddleware: AsyncModelMiddleware {
|
||||
func create(model: Planet, on db: Database, next: AnyAsyncModelResponder) async throws {
|
||||
// モデルは作成される前にここで変更できます
|
||||
model.name = model.name.capitalized()
|
||||
try await next.create(model, on: db)
|
||||
// 惑星が作成されたら、ここのコードが実行されます
|
||||
print ("Planet \(model.name) was created")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
ミドルウェアを作成したら、`app.databases.middleware`を使用して有効にできます。
|
||||
|
||||
```swift
|
||||
// モデルミドルウェアの設定例
|
||||
app.databases.middleware.use(PlanetMiddleware(), on: .psql)
|
||||
```
|
||||
|
||||
## データベース空間 {#database-space}
|
||||
|
||||
Fluentは、モデルの空間の設定をサポートしており、個々のFluentモデルをPostgreSQLスキーマ、MySQLデータベース、および複数の添付されたSQLiteデータベース間で分割できます。MongoDBは現時点では空間をサポートしていません。モデルをデフォルト以外の空間に配置するには、モデルに新しい静的プロパティを追加します:
|
||||
|
||||
```swift
|
||||
public static let schema = "planets"
|
||||
public static let space: String? = "mirror_universe"
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
Fluentは、すべてのデータベースクエリを構築する際にこれを使用します。
|
||||
|
|
@ -0,0 +1,382 @@
|
|||
# クエリ {#query}
|
||||
|
||||
Fluentのクエリ APIを使用すると、データベースからモデルの作成、読み取り、更新、削除を行うことができます。結果のフィルタリング、結合、チャンク処理、集約などをサポートしています。
|
||||
|
||||
```swift
|
||||
// Fluentのクエリ APIの例
|
||||
let planets = try await Planet.query(on: database)
|
||||
.filter(\.$type == .gasGiant)
|
||||
.sort(\.$name)
|
||||
.with(\.$star)
|
||||
.all()
|
||||
```
|
||||
|
||||
クエリビルダーは単一のモデルタイプに紐付けられており、静的な[`query`](model.md#query)メソッドを使用して作成できます。また、データベースオブジェクトの`query`メソッドにモデルタイプを渡すことでも作成できます。
|
||||
|
||||
```swift
|
||||
// こちらもクエリビルダーを作成します
|
||||
database.query(Planet.self)
|
||||
```
|
||||
|
||||
!!! note
|
||||
クエリを含むファイルで`import Fluent`を行う必要があります。これにより、コンパイラがFluentのヘルパー関数を認識できるようになります。
|
||||
|
||||
## All {#all}
|
||||
|
||||
`all()`メソッドはモデルの配列を返します。
|
||||
|
||||
```swift
|
||||
// すべての惑星を取得
|
||||
let planets = try await Planet.query(on: database).all()
|
||||
```
|
||||
|
||||
`all`メソッドは、結果セットから単一のフィールドのみを取得することもサポートしています。
|
||||
|
||||
```swift
|
||||
// すべての惑星名を取得
|
||||
let names = try await Planet.query(on: database).all(\.$name)
|
||||
```
|
||||
|
||||
### First {#first}
|
||||
|
||||
`first()`メソッドは、単一のオプショナルなモデルを返します。クエリが複数のモデルを返す場合、最初のものだけが返されます。クエリ結果がない場合は、`nil`が返されます。
|
||||
|
||||
```swift
|
||||
// Earthという名前の最初の惑星を取得
|
||||
let earth = try await Planet.query(on: database)
|
||||
.filter(\.$name == "Earth")
|
||||
.first()
|
||||
```
|
||||
|
||||
!!! tip
|
||||
`EventLoopFuture`を使用している場合、このメソッドは[`unwrap(or:)`](../basics/errors.md#abort)と組み合わせて、非オプショナルなモデルを返すか、エラーをスローすることができます。
|
||||
|
||||
## フィルター {#filter}
|
||||
|
||||
`filter`メソッドを使用すると、結果セットに含まれるモデルを制限できます。このメソッドにはいくつかのオーバーロードがあります。
|
||||
|
||||
### 値フィルター {#value-filter}
|
||||
|
||||
最もよく使用される`filter`メソッドは、値を含む演算子式を受け入れます。
|
||||
|
||||
```swift
|
||||
// フィールド値フィルタリングの例
|
||||
Planet.query(on: database).filter(\.$type == .gasGiant)
|
||||
```
|
||||
|
||||
これらの演算子式は、左側にフィールドのキーパスを、右側に値を受け取ります。提供される値はフィールドの期待される値型と一致する必要があり、結果のクエリにバインドされます。フィルター式は強く型付けされているため、先頭ドット構文を使用できます。
|
||||
|
||||
以下は、サポートされているすべての値演算子のリストです。
|
||||
|
||||
|演算子|説明|
|
||||
|-|-|
|
||||
|`==`|等しい|
|
||||
|`!=`|等しくない|
|
||||
|`>=`|以上|
|
||||
|`>`|より大きい|
|
||||
|`<`|より小さい|
|
||||
|`<=`|以下|
|
||||
|
||||
### フィールドフィルター {#field-filter}
|
||||
|
||||
`filter`メソッドは、2つのフィールドの比較をサポートしています。
|
||||
|
||||
```swift
|
||||
// 名と姓が同じすべてのユーザー
|
||||
User.query(on: database)
|
||||
.filter(\.$firstName == \.$lastName)
|
||||
```
|
||||
|
||||
フィールドフィルターは[値フィルター](#value-filter)と同じ演算子をサポートしています。
|
||||
|
||||
### サブセットフィルター {#subset-filter}
|
||||
|
||||
`filter`メソッドは、フィールドの値が指定された値のセットに存在するかどうかをチェックすることをサポートしています。
|
||||
|
||||
```swift
|
||||
// ガス巨星または小岩石型のいずれかのタイプを持つすべての惑星
|
||||
Planet.query(on: database)
|
||||
.filter(\.$type ~~ [.gasGiant, .smallRocky])
|
||||
```
|
||||
|
||||
提供される値のセットは、`Element`型がフィールドの値型と一致する任意のSwiftの`Collection`にすることができます。
|
||||
|
||||
以下は、サポートされているすべてのサブセット演算子のリストです。
|
||||
|
||||
|演算子|説明|
|
||||
|-|-|
|
||||
|`~~`|セット内の値|
|
||||
|`!~`|セット内にない値|
|
||||
|
||||
### 含有フィルター {#contains-filter}
|
||||
|
||||
`filter`メソッドは、文字列フィールドの値が指定された部分文字列を含むかどうかをチェックすることをサポートしています。
|
||||
|
||||
```swift
|
||||
// 名前がMで始まるすべての惑星
|
||||
Planet.query(on: database)
|
||||
.filter(\.$name =~ "M")
|
||||
```
|
||||
|
||||
これらの演算子は、文字列値を持つフィールドでのみ使用できます。
|
||||
|
||||
以下は、サポートされているすべての含有演算子のリストです。
|
||||
|
||||
|演算子|説明|
|
||||
|-|-|
|
||||
|`~~`|部分文字列を含む|
|
||||
|`!~`|部分文字列を含まない|
|
||||
|`=~`|プレフィックスに一致|
|
||||
|`!=~`|プレフィックスに一致しない|
|
||||
|`~=`|サフィックスに一致|
|
||||
|`!~=`|サフィックスに一致しない|
|
||||
|
||||
### グループ {#group}
|
||||
|
||||
デフォルトでは、クエリに追加されたすべてのフィルターが一致する必要があります。クエリビルダーは、1つのフィルターのみが一致する必要があるフィルターのグループを作成することをサポートしています。
|
||||
|
||||
```swift
|
||||
// 名前がEarthまたはMarsのすべての惑星
|
||||
Planet.query(on: database).group(.or) { group in
|
||||
group.filter(\.$name == "Earth").filter(\.$name == "Mars")
|
||||
}.all()
|
||||
```
|
||||
|
||||
`group`メソッドは、`and`または`or`ロジックによるフィルターの組み合わせをサポートしています。これらのグループは無限にネストできます。トップレベルのフィルターは`and`グループ内にあると考えることができます。
|
||||
|
||||
## 集約 {#aggregate}
|
||||
|
||||
クエリビルダーは、カウントや平均などの値のセットに対する計算を実行するためのいくつかのメソッドをサポートしています。
|
||||
|
||||
```swift
|
||||
// データベース内の惑星数
|
||||
Planet.query(on: database).count()
|
||||
```
|
||||
|
||||
`count`以外のすべての集約メソッドには、フィールドへのキーパスを渡す必要があります。
|
||||
|
||||
```swift
|
||||
// アルファベット順で最も低い名前
|
||||
Planet.query(on: database).min(\.$name)
|
||||
```
|
||||
|
||||
以下は、利用可能なすべての集約メソッドのリストです。
|
||||
|
||||
|集約|説明|
|
||||
|-|-|
|
||||
|`count`|結果数|
|
||||
|`sum`|結果値の合計|
|
||||
|`average`|結果値の平均|
|
||||
|`min`|最小結果値|
|
||||
|`max`|最大結果値|
|
||||
|
||||
`count`を除くすべての集約メソッドは、結果としてフィールドの値型を返します。`count`は常に整数を返します。
|
||||
|
||||
## チャンク {#chunk}
|
||||
|
||||
クエリビルダーは、結果セットを別々のチャンクとして返すことをサポートしています。これにより、大規模なデータベース読み取りを処理する際のメモリ使用量を制御できます。
|
||||
|
||||
```swift
|
||||
// 一度に最大64個ずつ、すべての惑星をチャンクで取得
|
||||
Planet.query(on: self.database).chunk(max: 64) { planets in
|
||||
// 惑星のチャンクを処理
|
||||
}
|
||||
```
|
||||
|
||||
提供されたクロージャは、結果の総数に応じて0回以上呼び出されます。返される各アイテムは、モデルまたはデータベースエントリのデコードを試みて返されたエラーのいずれかを含む`Result`です。
|
||||
|
||||
## フィールド {#field}
|
||||
|
||||
デフォルトでは、モデルのすべてのフィールドがクエリによってデータベースから読み取られます。`field`メソッドを使用して、モデルのフィールドのサブセットのみを選択できます。
|
||||
|
||||
```swift
|
||||
// 惑星のidとnameフィールドのみを選択
|
||||
Planet.query(on: database)
|
||||
.field(\.$id).field(\.$name)
|
||||
.all()
|
||||
```
|
||||
|
||||
クエリ中に選択されなかったモデルフィールドは、初期化されていない状態になります。初期化されていないフィールドに直接アクセスしようとすると、致命的なエラーが発生します。モデルのフィールド値が設定されているかどうかを確認するには、`value`プロパティを使用します。
|
||||
|
||||
```swift
|
||||
if let name = planet.$name.value {
|
||||
// Nameが取得されました
|
||||
} else {
|
||||
// Nameは取得されませんでした
|
||||
// `planet.name`へのアクセスは失敗します
|
||||
}
|
||||
```
|
||||
|
||||
## ユニーク {#unique}
|
||||
|
||||
クエリビルダーの`unique`メソッドは、一意の結果(重複なし)のみが返されるようにします。
|
||||
|
||||
```swift
|
||||
// すべての一意のユーザーの名を返します
|
||||
User.query(on: database).unique().all(\.$firstName)
|
||||
```
|
||||
|
||||
`unique`は、`all`で単一のフィールドを取得する場合に特に便利です。ただし、[`field`](#field)メソッドを使用して複数のフィールドを選択することもできます。モデル識別子は常に一意であるため、`unique`を使用する場合は選択を避けるべきです。
|
||||
|
||||
## 範囲 {#range}
|
||||
|
||||
クエリビルダーの`range`メソッドを使用すると、Swift範囲を使用して結果のサブセットを選択できます。
|
||||
|
||||
```swift
|
||||
// 最初の5つの惑星を取得
|
||||
Planet.query(on: self.database)
|
||||
.range(..<5)
|
||||
```
|
||||
|
||||
範囲値は、ゼロから始まる符号なし整数です。[Swift範囲](https://developer.apple.com/documentation/swift/range)の詳細について学びましょう。
|
||||
|
||||
```swift
|
||||
// 最初の2つの結果をスキップ
|
||||
.range(2...)
|
||||
```
|
||||
|
||||
## Join
|
||||
|
||||
クエリビルダーの`join`メソッドを使用すると、結果セットに別のモデルのフィールドを含めることができます。クエリに複数のモデルを結合できます。
|
||||
|
||||
```swift
|
||||
// Sunという名前の星を持つすべての惑星を取得
|
||||
Planet.query(on: database)
|
||||
.join(Star.self, on: \Planet.$star.$id == \Star.$id)
|
||||
.filter(Star.self, \.$name == "Sun")
|
||||
.all()
|
||||
```
|
||||
|
||||
`on`パラメータは、2つのフィールド間の等価式を受け入れます。フィールドの1つは現在の結果セットに既に存在している必要があります。もう1つのフィールドは、結合されるモデルに存在する必要があります。これらのフィールドは同じ値型を持つ必要があります。
|
||||
|
||||
`filter`や`sort`などのほとんどのクエリビルダーメソッドは、結合されたモデルをサポートしています。メソッドが結合されたモデルをサポートしている場合、最初のパラメータとして結合されたモデルタイプを受け入れます。
|
||||
|
||||
```swift
|
||||
// Starモデルの結合されたフィールド「name」でソート
|
||||
.sort(Star.self, \.$name)
|
||||
```
|
||||
|
||||
結合を使用するクエリは、依然としてベースモデルの配列を返します。結合されたモデルにアクセスするには、`joined`メソッドを使用します。
|
||||
|
||||
```swift
|
||||
// クエリ結果から結合されたモデルへのアクセス
|
||||
let planet: Planet = ...
|
||||
let star = try planet.joined(Star.self)
|
||||
```
|
||||
|
||||
### モデルエイリアス {#model-alias}
|
||||
|
||||
モデルエイリアスを使用すると、同じモデルをクエリに複数回結合できます。モデルエイリアスを宣言するには、`ModelAlias`に準拠する1つ以上の型を作成します。
|
||||
|
||||
```swift
|
||||
// モデルエイリアスの例
|
||||
final class HomeTeam: ModelAlias {
|
||||
static let name = "home_teams"
|
||||
let model = Team()
|
||||
}
|
||||
final class AwayTeam: ModelAlias {
|
||||
static let name = "away_teams"
|
||||
let model = Team()
|
||||
}
|
||||
```
|
||||
|
||||
これらの型は、`model`プロパティを介してエイリアスされるモデルを参照します。作成されると、クエリビルダーで通常のモデルのようにモデルエイリアスを使用できます。
|
||||
|
||||
```swift
|
||||
// ホームチームの名前がVaporで、
|
||||
// アウェイチームの名前でソートされたすべての試合を取得
|
||||
let matches = try await Match.query(on: self.database)
|
||||
.join(HomeTeam.self, on: \Match.$homeTeam.$id == \HomeTeam.$id)
|
||||
.join(AwayTeam.self, on: \Match.$awayTeam.$id == \AwayTeam.$id)
|
||||
.filter(HomeTeam.self, \.$name == "Vapor")
|
||||
.sort(AwayTeam.self, \.$name)
|
||||
.all()
|
||||
```
|
||||
|
||||
すべてのモデルフィールドは、`@dynamicMemberLookup`を介してモデルエイリアスタイプを通じてアクセスできます。
|
||||
|
||||
```swift
|
||||
// 結果から結合されたモデルにアクセス
|
||||
let home = try match.joined(HomeTeam.self)
|
||||
print(home.name)
|
||||
```
|
||||
|
||||
## Update
|
||||
|
||||
クエリビルダーは、`update`メソッドを使用して一度に複数のモデルを更新することをサポートしています。
|
||||
|
||||
```swift
|
||||
// 「Pluto」という名前のすべての惑星を更新
|
||||
Planet.query(on: database)
|
||||
.set(\.$type, to: .dwarf)
|
||||
.filter(\.$name == "Pluto")
|
||||
.update()
|
||||
```
|
||||
|
||||
`update`は`set`、`filter`、`range`メソッドをサポートしています。
|
||||
|
||||
## Delete
|
||||
|
||||
クエリビルダーは、`delete`メソッドを使用して一度に複数のモデルを削除することをサポートしています。
|
||||
|
||||
```swift
|
||||
// 「Vulcan」という名前のすべての惑星を削除
|
||||
Planet.query(on: database)
|
||||
.filter(\.$name == "Vulcan")
|
||||
.delete()
|
||||
```
|
||||
|
||||
`delete`は`filter`メソッドをサポートしています。
|
||||
|
||||
## ページネーション {#paginate}
|
||||
|
||||
Fluentのクエリ APIは、`paginate`メソッドを使用した自動結果ページネーションをサポートしています。
|
||||
|
||||
```swift
|
||||
// リクエストベースのページネーションの例
|
||||
app.get("planets") { req in
|
||||
try await Planet.query(on: req.db).paginate(for: req)
|
||||
}
|
||||
```
|
||||
|
||||
`paginate(for:)`メソッドは、リクエストURIで利用可能な`page`と`per`パラメータを使用して、目的の結果セットを返します。現在のページと結果の総数に関するメタデータは、`metadata`キーに含まれます。
|
||||
|
||||
```http
|
||||
GET /planets?page=2&per=5 HTTP/1.1
|
||||
```
|
||||
|
||||
上記のリクエストは、以下のような構造のレスポンスを生成します。
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [...],
|
||||
"metadata": {
|
||||
"page": 2,
|
||||
"per": 5,
|
||||
"total": 8
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
ページ番号は`1`から始まります。手動でページリクエストを作成することもできます。
|
||||
|
||||
```swift
|
||||
// 手動ページネーションの例
|
||||
.paginate(PageRequest(page: 1, per: 2))
|
||||
```
|
||||
|
||||
## ソート {#sort}
|
||||
|
||||
クエリ結果は、`sort`メソッドを使用してフィールド値でソートできます。
|
||||
|
||||
```swift
|
||||
// 名前でソートされた惑星を取得
|
||||
Planet.query(on: database).sort(\.$name)
|
||||
```
|
||||
|
||||
同点の場合のフォールバックとして、追加のソートを追加できます。フォールバックは、クエリビルダーに追加された順序で使用されます。
|
||||
|
||||
```swift
|
||||
// 名前でソートされたユーザーを取得。2人のユーザーが同じ名前の場合、年齢でソート
|
||||
User.query(on: database).sort(\.$name).sort(\.$age)
|
||||
```
|
||||
|
|
@ -0,0 +1,404 @@
|
|||
# リレーション {#relations}
|
||||
|
||||
Fluent の [model API](model.md) は、リレーションを通じてモデル間の参照を作成・管理するのに役立ちます。3 つのタイプのリレーションがサポートされています:
|
||||
|
||||
- [Parent](#parent) / [Child](#optional-child)(1対1)
|
||||
- [Parent](#parent) / [Children](#children)(1対多)
|
||||
- [Siblings](#siblings)(多対多)
|
||||
|
||||
## Parent {#parent}
|
||||
|
||||
`@Parent` リレーションは、別のモデルの `@ID` プロパティへの参照を保存します。
|
||||
|
||||
```swift
|
||||
final class Planet: Model {
|
||||
// parent リレーションの例
|
||||
@Parent(key: "star_id")
|
||||
var star: Star
|
||||
}
|
||||
```
|
||||
|
||||
`@Parent` には `id` という名前の `@Field` が含まれており、リレーションの設定と更新に使用されます。
|
||||
|
||||
```swift
|
||||
// parent リレーション id を設定
|
||||
earth.$star.id = sun.id
|
||||
```
|
||||
|
||||
例えば、`Planet` の初期化メソッドは次のようになります:
|
||||
|
||||
```swift
|
||||
init(name: String, starID: Star.IDValue) {
|
||||
self.name = name
|
||||
// ...
|
||||
self.$star.id = starID
|
||||
}
|
||||
```
|
||||
|
||||
`key` パラメータは、親の識別子を保存するために使用するフィールドキーを定義します。`Star` が `UUID` 識別子を持つと仮定すると、この `@Parent` リレーションは以下の [field definition](schema.md#field) と互換性があります。
|
||||
|
||||
```swift
|
||||
.field("star_id", .uuid, .required, .references("star", "id"))
|
||||
```
|
||||
|
||||
[`.references`](schema.md#field-constraint) 制約はオプションであることに注意してください。詳細については [schema](schema.md) を参照してください。
|
||||
|
||||
### Optional Parent {#optional-parent}
|
||||
|
||||
`@OptionalParent` リレーションは、別のモデルの `@ID` プロパティへのオプショナルな参照を保存します。`@Parent` と同様に動作しますが、リレーションが `nil` になることを許可します。
|
||||
|
||||
```swift
|
||||
final class Planet: Model {
|
||||
// optional parent リレーションの例
|
||||
@OptionalParent(key: "star_id")
|
||||
var star: Star?
|
||||
}
|
||||
```
|
||||
|
||||
フィールド定義は `@Parent` と似ていますが、`.required` 制約を省略する必要があります。
|
||||
|
||||
```swift
|
||||
.field("star_id", .uuid, .references("star", "id"))
|
||||
```
|
||||
|
||||
### Parent のエンコードとデコード {#encoding-and-decoding-of-parents}
|
||||
|
||||
`@Parent` リレーションを扱う際に注意すべき点の1つは、それらを送受信する方法です。例えば、JSON では、`Planet` モデルの `@Parent` は次のようになるかもしれません:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "A616B398-A963-4EC7-9D1D-B1AA8A6F1107",
|
||||
"star": {
|
||||
"id": "A1B2C3D4-1234-5678-90AB-CDEF12345678"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`star` プロパティが期待される ID ではなくオブジェクトであることに注意してください。モデルを HTTP ボディとして送信する場合、デコードが機能するためにはこれに一致する必要があります。この理由から、ネットワーク経由でモデルを送信する際には、モデルを表現するための DTO を使用することを強く推奨します。例えば:
|
||||
|
||||
```swift
|
||||
struct PlanetDTO: Content {
|
||||
var id: UUID?
|
||||
var name: String
|
||||
var star: Star.IDValue
|
||||
}
|
||||
```
|
||||
|
||||
そして、DTO をデコードしてモデルに変換できます:
|
||||
|
||||
```swift
|
||||
let planetData = try req.content.decode(PlanetDTO.self)
|
||||
let planet = Planet(id: planetData.id, name: planetData.name, starID: planetData.star)
|
||||
try await planet.create(on: req.db)
|
||||
```
|
||||
|
||||
同じことがクライアントにモデルを返す際にも適用されます。クライアントはネストされた構造を処理できる必要があるか、返す前にモデルを DTO に変換する必要があります。DTO の詳細については、[Model ドキュメント](model.md#data-transfer-object) を参照してください。
|
||||
|
||||
## Optional Child {#optional-child}
|
||||
|
||||
`@OptionalChild` プロパティは、2つのモデル間に1対1のリレーションを作成します。ルートモデルには値を保存しません。
|
||||
|
||||
```swift
|
||||
final class Planet: Model {
|
||||
// optional child リレーションの例
|
||||
@OptionalChild(for: \.$planet)
|
||||
var governor: Governor?
|
||||
}
|
||||
```
|
||||
|
||||
`for` パラメータは、ルートモデルを参照する `@Parent` または `@OptionalParent` リレーションへのキーパスを受け取ります。
|
||||
|
||||
`create` メソッドを使用して、新しいモデルをこのリレーションに追加できます。
|
||||
|
||||
```swift
|
||||
// リレーションに新しいモデルを追加する例
|
||||
let jane = Governor(name: "Jane Doe")
|
||||
try await mars.$governor.create(jane, on: database)
|
||||
```
|
||||
|
||||
これにより、子モデルの親 ID が自動的に設定されます。
|
||||
|
||||
このリレーションは値を保存しないため、ルートモデルのデータベーススキーマエントリは必要ありません。
|
||||
|
||||
リレーションの1対1の性質は、親モデルを参照するカラムに `.unique` 制約を使用して、子モデルのスキーマで強制される必要があります。
|
||||
|
||||
```swift
|
||||
try await database.schema(Governor.schema)
|
||||
.id()
|
||||
.field("name", .string, .required)
|
||||
.field("planet_id", .uuid, .required, .references("planets", "id"))
|
||||
// unique 制約の例
|
||||
.unique(on: "planet_id")
|
||||
.create()
|
||||
```
|
||||
!!! warning
|
||||
クライアントのスキーマから親 ID フィールドのユニーク制約を省略すると、予測できない結果につながる可能性があります。
|
||||
一意性制約がない場合、子テーブルには任意の親に対して複数の子行が含まれる可能性があります。この場合、`@OptionalChild` プロパティは一度に1つの子にしかアクセスできず、どの子が読み込まれるかを制御する方法がありません。任意の親に対して複数の子行を保存する必要がある場合は、代わりに `@Children` を使用してください。
|
||||
|
||||
## Children {#children}
|
||||
|
||||
`@Children` プロパティは、2つのモデル間に1対多のリレーションを作成します。ルートモデルには値を保存しません。
|
||||
|
||||
```swift
|
||||
final class Star: Model {
|
||||
// children リレーションの例
|
||||
@Children(for: \.$star)
|
||||
var planets: [Planet]
|
||||
}
|
||||
```
|
||||
|
||||
`for` パラメータは、ルートモデルを参照する `@Parent` または `@OptionalParent` リレーションへのキーパスを受け取ります。この場合、前の[例](#parent)の `@Parent` リレーションを参照しています。
|
||||
|
||||
`create` メソッドを使用して、新しいモデルをこのリレーションに追加できます。
|
||||
|
||||
```swift
|
||||
// リレーションに新しいモデルを追加する例
|
||||
let earth = Planet(name: "Earth")
|
||||
try await sun.$planets.create(earth, on: database)
|
||||
```
|
||||
|
||||
これにより、子モデルの親 ID が自動的に設定されます。
|
||||
|
||||
このリレーションは値を保存しないため、データベーススキーマエントリは必要ありません。
|
||||
|
||||
## Siblings {#siblings}
|
||||
|
||||
`@Siblings` プロパティは、2つのモデル間に多対多のリレーションを作成します。これはピボットと呼ばれる第3のモデルを通じて行われます。
|
||||
|
||||
`Planet` と `Tag` 間の多対多リレーションの例を見てみましょう。
|
||||
|
||||
```swift
|
||||
enum PlanetTagStatus: String, Codable { case accepted, pending }
|
||||
|
||||
// ピボットモデルの例
|
||||
final class PlanetTag: Model {
|
||||
static let schema = "planet+tag"
|
||||
|
||||
@ID(key: .id)
|
||||
var id: UUID?
|
||||
|
||||
@Parent(key: "planet_id")
|
||||
var planet: Planet
|
||||
|
||||
@Parent(key: "tag_id")
|
||||
var tag: Tag
|
||||
|
||||
@OptionalField(key: "comments")
|
||||
var comments: String?
|
||||
|
||||
@OptionalEnum(key: "status")
|
||||
var status: PlanetTagStatus?
|
||||
|
||||
init() { }
|
||||
|
||||
init(id: UUID? = nil, planet: Planet, tag: Tag, comments: String?, status: PlanetTagStatus?) throws {
|
||||
self.id = id
|
||||
self.$planet.id = try planet.requireID()
|
||||
self.$tag.id = try tag.requireID()
|
||||
self.comments = comments
|
||||
self.status = status
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
関連付けされる各モデルに対して少なくとも2つの `@Parent` リレーションを含むモデルは、ピボットとして使用できます。モデルは ID などの追加のプロパティを含むことができ、他の `@Parent` リレーションを含むこともできます。
|
||||
|
||||
ピボットモデルに [unique](schema.md#unique) 制約を追加すると、重複エントリを防ぐのに役立ちます。詳細については [schema](schema.md) を参照してください。
|
||||
|
||||
```swift
|
||||
// 重複リレーションを禁止
|
||||
.unique(on: "planet_id", "tag_id")
|
||||
```
|
||||
|
||||
ピボットが作成されたら、`@Siblings` プロパティを使用してリレーションを作成します。
|
||||
|
||||
```swift
|
||||
final class Planet: Model {
|
||||
// siblings リレーションの例
|
||||
@Siblings(through: PlanetTag.self, from: \.$planet, to: \.$tag)
|
||||
public var tags: [Tag]
|
||||
}
|
||||
```
|
||||
|
||||
`@Siblings` プロパティには3つのパラメータが必要です:
|
||||
|
||||
- `through`: ピボットモデルの型
|
||||
- `from`: ピボットからルートモデルを参照する親リレーションへのキーパス
|
||||
- `to`: ピボットから関連モデルを参照する親リレーションへのキーパス
|
||||
|
||||
関連モデルの逆 `@Siblings` プロパティがリレーションを完成させます。
|
||||
|
||||
```swift
|
||||
final class Tag: Model {
|
||||
// siblings リレーションの例
|
||||
@Siblings(through: PlanetTag.self, from: \.$tag, to: \.$planet)
|
||||
public var planets: [Planet]
|
||||
}
|
||||
```
|
||||
|
||||
### Siblings の追加 {#siblings-attach}
|
||||
|
||||
`@Siblings` プロパティには、リレーションにモデルを追加または削除するメソッドがあります。
|
||||
|
||||
`attach()` メソッドを使用して、単一のモデルまたはモデルの配列をリレーションに追加します。ピボットモデルは必要に応じて自動的に作成および保存されます。作成された各ピボットの追加プロパティを設定するためのコールバッククロージャを指定できます:
|
||||
|
||||
```swift
|
||||
let earth: Planet = ...
|
||||
let inhabited: Tag = ...
|
||||
// モデルをリレーションに追加
|
||||
try await earth.$tags.attach(inhabited, on: database)
|
||||
// リレーションを確立する際にピボット属性を設定
|
||||
try await earth.$tags.attach(inhabited, on: database) { pivot in
|
||||
pivot.comments = "This is a life-bearing planet."
|
||||
pivot.status = .accepted
|
||||
}
|
||||
// 複数のモデルを属性とともにリレーションに追加
|
||||
let volcanic: Tag = ..., oceanic: Tag = ...
|
||||
try await earth.$tags.attach([volcanic, oceanic], on: database) { pivot in
|
||||
pivot.comments = "This planet has a tag named \(pivot.$tag.name)."
|
||||
pivot.status = .pending
|
||||
}
|
||||
```
|
||||
|
||||
単一のモデルを追加する場合、`method` パラメータを使用して、保存前にリレーションをチェックするかどうかを選択できます。
|
||||
|
||||
```swift
|
||||
// リレーションがまだ存在しない場合のみ追加
|
||||
try await earth.$tags.attach(inhabited, method: .ifNotExists, on: database)
|
||||
```
|
||||
|
||||
`detach` メソッドを使用して、リレーションからモデルを削除します。これにより、対応するピボットモデルが削除されます。
|
||||
|
||||
```swift
|
||||
// リレーションからモデルを削除
|
||||
try await earth.$tags.detach(inhabited, on: database)
|
||||
```
|
||||
|
||||
`isAttached` メソッドを使用して、モデルが関連付けられているかどうかを確認できます。
|
||||
|
||||
```swift
|
||||
// モデルが関連付けられているかチェック
|
||||
earth.$tags.isAttached(to: inhabited)
|
||||
```
|
||||
|
||||
## Get {#get}
|
||||
|
||||
`get(on:)` メソッドを使用して、リレーションの値を取得します。
|
||||
|
||||
```swift
|
||||
// 太陽のすべての惑星を取得
|
||||
sun.$planets.get(on: database).map { planets in
|
||||
print(planets)
|
||||
}
|
||||
|
||||
// または
|
||||
|
||||
let planets = try await sun.$planets.get(on: database)
|
||||
print(planets)
|
||||
```
|
||||
|
||||
`reload` パラメータを使用して、すでに読み込まれている場合にリレーションをデータベースから再取得するかどうかを選択します。
|
||||
|
||||
```swift
|
||||
try await sun.$planets.get(reload: true, on: database)
|
||||
```
|
||||
|
||||
## Query {#query}
|
||||
|
||||
リレーションで `query(on:)` メソッドを使用して、関連モデルのクエリビルダーを作成します。
|
||||
|
||||
```swift
|
||||
// M で始まる名前を持つ太陽のすべての惑星を取得
|
||||
try await sun.$planets.query(on: database).filter(\.$name =~ "M").all()
|
||||
```
|
||||
|
||||
詳細については [query](query.md) を参照してください。
|
||||
|
||||
## Eager Loading {#eager-loading}
|
||||
|
||||
Fluent のクエリビルダーを使用すると、モデルがデータベースから取得されるときにリレーションを事前に読み込むことができます。これは eager loading と呼ばれ、最初に [`get`](#get) を呼び出す必要なく、リレーションに同期的にアクセスできるようになります。
|
||||
|
||||
リレーションを eager load するには、クエリビルダーの `with` メソッドにリレーションへのキーパスを渡します。
|
||||
|
||||
```swift
|
||||
// eager loading の例
|
||||
Planet.query(on: database).with(\.$star).all().map { planets in
|
||||
for planet in planets {
|
||||
// `star` は eager load されているため
|
||||
// ここで同期的にアクセス可能
|
||||
print(planet.star.name)
|
||||
}
|
||||
}
|
||||
|
||||
// または
|
||||
|
||||
let planets = try await Planet.query(on: database).with(\.$star).all()
|
||||
for planet in planets {
|
||||
// `star` は eager load されているため
|
||||
// ここで同期的にアクセス可能
|
||||
print(planet.star.name)
|
||||
}
|
||||
```
|
||||
|
||||
上記の例では、`star` という名前の [`@Parent`](#parent) リレーションへのキーパスが `with` に渡されています。これにより、すべての惑星が読み込まれた後、クエリビルダーは関連するすべての星を取得するための追加のクエリを実行します。その後、星は `@Parent` プロパティを介して同期的にアクセスできるようになります。
|
||||
|
||||
eager load される各リレーションは、返されるモデルの数に関係なく、追加のクエリを1つだけ必要とします。Eager loading は、クエリビルダーの `all` と `first` メソッドでのみ可能です。
|
||||
|
||||
### ネストされた Eager Load {#nested-eager-load}
|
||||
|
||||
クエリビルダーの `with` メソッドを使用すると、クエリ対象のモデルのリレーションを eager load できます。ただし、関連モデルのリレーションも eager load できます。
|
||||
|
||||
```swift
|
||||
let planets = try await Planet.query(on: database).with(\.$star) { star in
|
||||
star.with(\.$galaxy)
|
||||
}.all()
|
||||
for planet in planets {
|
||||
// `star.galaxy` は eager load されているため
|
||||
// ここで同期的にアクセス可能
|
||||
print(planet.star.galaxy.name)
|
||||
}
|
||||
```
|
||||
|
||||
`with` メソッドは、2番目のパラメータとしてオプションのクロージャを受け取ります。このクロージャは、選択されたリレーションの eager load ビルダーを受け取ります。eager loading のネストの深さに制限はありません。
|
||||
|
||||
## Lazy Eager Loading {#lazy-eager-loading}
|
||||
|
||||
親モデルをすでに取得していて、そのリレーションの1つを読み込みたい場合は、その目的で `get(reload:on:)` メソッドを使用できます。これにより、関連モデルがデータベース(または利用可能な場合はキャッシュ)から取得され、ローカルプロパティとしてアクセスできるようになります。
|
||||
|
||||
```swift
|
||||
planet.$star.get(on: database).map {
|
||||
print(planet.star.name)
|
||||
}
|
||||
|
||||
// または
|
||||
|
||||
try await planet.$star.get(on: database)
|
||||
print(planet.star.name)
|
||||
```
|
||||
|
||||
受け取るデータがキャッシュから取得されないようにしたい場合は、`reload:` パラメータを使用します。
|
||||
|
||||
```swift
|
||||
try await planet.$star.get(reload: true, on: database)
|
||||
print(planet.star.name)
|
||||
```
|
||||
|
||||
リレーションが読み込まれているかどうかを確認するには、`value` プロパティを使用します。
|
||||
|
||||
```swift
|
||||
if planet.$star.value != nil {
|
||||
// リレーションが読み込まれている
|
||||
print(planet.star.name)
|
||||
} else {
|
||||
// リレーションが読み込まれていない
|
||||
// planet.star にアクセスしようとすると失敗する
|
||||
}
|
||||
```
|
||||
|
||||
関連モデルがすでに変数にある場合は、上記の `value` プロパティを使用してリレーションを手動で設定できます。
|
||||
|
||||
```swift
|
||||
planet.$star.value = star
|
||||
```
|
||||
|
||||
これにより、追加のデータベースクエリなしで、eager load または lazy load されたかのように関連モデルが親に添付されます。
|
||||
|
|
@ -0,0 +1,426 @@
|
|||
# スキーマ {#schema}
|
||||
|
||||
Fluentのスキーマ APIを使用すると、データベーススキーマをプログラム的に作成および更新できます。[モデル](model.md)での使用に備えてデータベースを準備するために、[マイグレーション](migration.md)と組み合わせて使用されることがよくあります。
|
||||
|
||||
```swift
|
||||
// FluentのスキーマAPIの例
|
||||
try await database.schema("planets")
|
||||
.id()
|
||||
.field("name", .string, .required)
|
||||
.field("star_id", .uuid, .required, .references("stars", "id"))
|
||||
.create()
|
||||
```
|
||||
|
||||
`SchemaBuilder`を作成するには、データベースで`schema`メソッドを使用します。影響を与えたいテーブルまたはコレクションの名前を渡します。モデルのスキーマを編集する場合は、この名前がモデルの[`schema`](model.md#schema)と一致することを確認してください。
|
||||
|
||||
## アクション {#actions}
|
||||
|
||||
スキーマAPIは、スキーマの作成、更新、削除をサポートしています。各アクションは、APIで利用可能なメソッドのサブセットをサポートしています。
|
||||
|
||||
### 作成 {#create}
|
||||
|
||||
`create()`を呼び出すと、データベースに新しいテーブルまたはコレクションが作成されます。新しいフィールドと制約を定義するためのすべてのメソッドがサポートされています。更新または削除のためのメソッドは無視されます。
|
||||
|
||||
```swift
|
||||
// スキーマ作成の例
|
||||
try await database.schema("planets")
|
||||
.id()
|
||||
.field("name", .string, .required)
|
||||
.create()
|
||||
```
|
||||
|
||||
選択した名前のテーブルまたはコレクションがすでに存在する場合、エラーがスローされます。これを無視するには、`.ignoreExisting()`を使用します。
|
||||
|
||||
### 更新 {#update}
|
||||
|
||||
`update()`を呼び出すと、データベース内の既存のテーブルまたはコレクションが更新されます。フィールドと制約の作成、更新、削除のためのすべてのメソッドがサポートされています。
|
||||
|
||||
```swift
|
||||
// スキーマ更新の例
|
||||
try await database.schema("planets")
|
||||
.unique(on: "name")
|
||||
.deleteField("star_id")
|
||||
.update()
|
||||
```
|
||||
|
||||
### 削除 {#delete}
|
||||
|
||||
`delete()`を呼び出すと、データベースから既存のテーブルまたはコレクションが削除されます。追加のメソッドはサポートされていません。
|
||||
|
||||
```swift
|
||||
// スキーマ削除の例
|
||||
database.schema("planets").delete()
|
||||
```
|
||||
|
||||
## フィールド {#field}
|
||||
|
||||
スキーマの作成または更新時にフィールドを追加できます。
|
||||
|
||||
```swift
|
||||
// 新しいフィールドを追加
|
||||
.field("name", .string, .required)
|
||||
```
|
||||
|
||||
最初のパラメータはフィールドの名前です。これは、関連するモデルプロパティで使用されるキーと一致する必要があります。2番目のパラメータはフィールドの[データ型](#data-type)です。最後に、0個以上の[制約](#field-constraint)を追加できます。
|
||||
|
||||
### データ型 {#data-type}
|
||||
|
||||
サポートされているフィールドのデータ型は以下のとおりです。
|
||||
|
||||
|DataType|Swift Type|
|
||||
|-|-|
|
||||
|`.string`|`String`|
|
||||
|`.int{8,16,32,64}`|`Int{8,16,32,64}`|
|
||||
|`.uint{8,16,32,64}`|`UInt{8,16,32,64}`|
|
||||
|`.bool`|`Bool`|
|
||||
|`.datetime`|`Date` (推奨)|
|
||||
|`.date`|`Date` (時刻を省略)|
|
||||
|`.float`|`Float`|
|
||||
|`.double`|`Double`|
|
||||
|`.data`|`Data`|
|
||||
|`.uuid`|`UUID`|
|
||||
|`.dictionary`|[dictionary](#dictionary)を参照|
|
||||
|`.array`|[array](#array)を参照|
|
||||
|`.enum`|[enum](#enum)を参照|
|
||||
|
||||
### フィールド制約 {#field-constraint}
|
||||
|
||||
サポートされているフィールド制約は以下のとおりです。
|
||||
|
||||
|FieldConstraint|説明|
|
||||
|-|-|
|
||||
|`.required`|`nil`値を許可しません。|
|
||||
|`.references`|このフィールドの値が参照されているスキーマの値と一致することを要求します。[外部キー](#foreign-key)を参照。|
|
||||
|`.identifier`|主キーを示します。[識別子](#identifier)を参照。|
|
||||
|`.sql(SQLColumnConstraintAlgorithm)`|サポートされていない制約(例:`default`)を定義します。[SQL](#sql)と[SQLColumnConstraintAlgorithm](https://api.vapor.codes/sqlkit/documentation/sqlkit/sqlcolumnconstraintalgorithm/)を参照。|
|
||||
|
||||
### 識別子 {#identifier}
|
||||
|
||||
モデルが標準の`@ID`プロパティを使用している場合、`id()`ヘルパーを使用してそのフィールドを作成できます。これは特別な`.id`フィールドキーと`UUID`値型を使用します。
|
||||
|
||||
```swift
|
||||
// デフォルト識別子のフィールドを追加
|
||||
.id()
|
||||
```
|
||||
|
||||
カスタム識別子型の場合、フィールドを手動で指定する必要があります。
|
||||
|
||||
```swift
|
||||
// カスタム識別子のフィールドを追加
|
||||
.field("id", .int, .identifier(auto: true))
|
||||
```
|
||||
|
||||
`identifier`制約は単一のフィールドで使用でき、主キーを示します。`auto`フラグは、データベースがこの値を自動的に生成するかどうかを決定します。
|
||||
|
||||
### フィールドの更新 {#update-field}
|
||||
|
||||
`updateField`を使用してフィールドのデータ型を更新できます。
|
||||
|
||||
```swift
|
||||
// フィールドを`double`データ型に更新
|
||||
.updateField("age", .double)
|
||||
```
|
||||
|
||||
高度なスキーマ更新の詳細については、[advanced](advanced.md#sql)を参照してください。
|
||||
|
||||
### フィールドの削除 {#delete-field}
|
||||
|
||||
`deleteField`を使用してスキーマからフィールドを削除できます。
|
||||
|
||||
```swift
|
||||
// "age"フィールドを削除
|
||||
.deleteField("age")
|
||||
```
|
||||
|
||||
## 制約 {#constraint}
|
||||
|
||||
スキーマの作成または更新時に制約を追加できます。[フィールド制約](#field-constraint)とは異なり、トップレベルの制約は複数のフィールドに影響を与えることができます。
|
||||
|
||||
### ユニーク {#unique}
|
||||
|
||||
ユニーク制約は、1つ以上のフィールドに重複する値がないことを要求します。
|
||||
|
||||
```swift
|
||||
// 重複するメールアドレスを許可しない
|
||||
.unique(on: "email")
|
||||
```
|
||||
|
||||
複数のフィールドが制約されている場合、各フィールドの値の特定の組み合わせがユニークである必要があります。
|
||||
|
||||
```swift
|
||||
// 同じフルネームのユーザーを許可しない
|
||||
.unique(on: "first_name", "last_name")
|
||||
```
|
||||
|
||||
ユニーク制約を削除するには、`deleteUnique`を使用します。
|
||||
|
||||
```swift
|
||||
// 重複メール制約を削除
|
||||
.deleteUnique(on: "email")
|
||||
```
|
||||
|
||||
### 制約名 {#constraint-name}
|
||||
|
||||
Fluentはデフォルトでユニーク制約名を生成します。ただし、カスタム制約名を渡したい場合があります。これは`name`パラメータを使用して行うことができます。
|
||||
|
||||
```swift
|
||||
// 重複するメールアドレスを許可しない
|
||||
.unique(on: "email", name: "no_duplicate_emails")
|
||||
```
|
||||
|
||||
名前付き制約を削除するには、`deleteConstraint(name:)`を使用する必要があります。
|
||||
|
||||
```swift
|
||||
// 重複メール制約を削除
|
||||
.deleteConstraint(name: "no_duplicate_emails")
|
||||
```
|
||||
|
||||
## 外部キー {#foreign-key}
|
||||
|
||||
外部キー制約は、フィールドの値が参照されているフィールドの値のいずれかと一致することを要求します。これは、無効なデータが保存されるのを防ぐのに役立ちます。外部キー制約は、フィールドまたはトップレベルの制約として追加できます。
|
||||
|
||||
フィールドに外部キー制約を追加するには、`.references`を使用します。
|
||||
|
||||
```swift
|
||||
// フィールド外部キー制約を追加する例
|
||||
.field("star_id", .uuid, .required, .references("stars", "id"))
|
||||
```
|
||||
|
||||
上記の制約は、"star_id"フィールドのすべての値がStarの"id"フィールドの値のいずれかと一致する必要があることを要求します。
|
||||
|
||||
この同じ制約は、`foreignKey`を使用してトップレベルの制約として追加できます。
|
||||
|
||||
```swift
|
||||
// トップレベルの外部キー制約を追加する例
|
||||
.foreignKey("star_id", references: "stars", "id")
|
||||
```
|
||||
|
||||
フィールド制約とは異なり、トップレベルの制約はスキーマ更新で追加できます。また、[名前を付ける](#constraint-name)こともできます。
|
||||
|
||||
外部キー制約は、オプションの`onDelete`と`onUpdate`アクションをサポートしています。
|
||||
|
||||
|ForeignKeyAction|説明|
|
||||
|-|-|
|
||||
|`.noAction`|外部キー違反を防ぎます(デフォルト)。|
|
||||
|`.restrict`|`.noAction`と同じ。|
|
||||
|`.cascade`|外部キーを通じて削除を伝播します。|
|
||||
|`.setNull`|参照が切れた場合、フィールドをnullに設定します。|
|
||||
|`.setDefault`|参照が切れた場合、フィールドをデフォルトに設定します。|
|
||||
|
||||
以下は外部キーアクションを使用した例です。
|
||||
|
||||
```swift
|
||||
// トップレベルの外部キー制約を追加する例
|
||||
.foreignKey("star_id", references: "stars", "id", onDelete: .cascade)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
外部キーアクションはデータベース内でのみ発生し、Fluentをバイパスします。
|
||||
これは、モデルミドルウェアや論理削除などが正しく動作しない可能性があることを意味します。
|
||||
|
||||
## SQL {#sql}
|
||||
|
||||
`.sql`パラメータを使用すると、スキーマに任意のSQLを追加できます。これは、特定の制約やデータ型を追加するのに役立ちます。
|
||||
一般的な使用例は、フィールドのデフォルト値を定義することです:
|
||||
|
||||
```swift
|
||||
.field("active", .bool, .required, .sql(.default(true)))
|
||||
```
|
||||
|
||||
またはタイムスタンプのデフォルト値:
|
||||
|
||||
```swift
|
||||
.field("created_at", .datetime, .required, .sql(.default(SQLFunction("now"))))
|
||||
```
|
||||
|
||||
## Dictionary {#dictionary}
|
||||
|
||||
dictionary データ型は、ネストされた辞書値を格納できます。これには、`Codable`に準拠する構造体と、`Codable`値を持つSwift辞書が含まれます。
|
||||
|
||||
!!! note
|
||||
FluentのSQLデータベースドライバーは、ネストされた辞書をJSON列に格納します。
|
||||
|
||||
次の`Codable`構造体を考えてみましょう。
|
||||
|
||||
```swift
|
||||
struct Pet: Codable {
|
||||
var name: String
|
||||
var age: Int
|
||||
}
|
||||
```
|
||||
|
||||
この`Pet`構造体は`Codable`であるため、`@Field`に格納できます。
|
||||
|
||||
```swift
|
||||
@Field(key: "pet")
|
||||
var pet: Pet
|
||||
```
|
||||
|
||||
このフィールドは`.dictionary(of:)`データ型を使用して格納できます。
|
||||
|
||||
```swift
|
||||
.field("pet", .dictionary, .required)
|
||||
```
|
||||
|
||||
`Codable`型は異種辞書であるため、`of`パラメータを指定しません。
|
||||
|
||||
辞書の値が同種の場合、例えば`[String: Int]`の場合、`of`パラメータは値の型を指定します。
|
||||
|
||||
```swift
|
||||
.field("numbers", .dictionary(of: .int), .required)
|
||||
```
|
||||
|
||||
辞書のキーは常に文字列である必要があります。
|
||||
|
||||
## Array {#array}
|
||||
|
||||
array データ型は、ネストされた配列を格納できます。これには、`Codable`値を含むSwift配列と、キーなしコンテナを使用する`Codable`型が含まれます。
|
||||
|
||||
文字列の配列を格納する次の`@Field`を考えてみましょう。
|
||||
|
||||
```swift
|
||||
@Field(key: "tags")
|
||||
var tags: [String]
|
||||
```
|
||||
|
||||
このフィールドは`.array(of:)`データ型を使用して格納できます。
|
||||
|
||||
```swift
|
||||
.field("tags", .array(of: .string), .required)
|
||||
```
|
||||
|
||||
配列は同種であるため、`of`パラメータを指定します。
|
||||
|
||||
Codable Swiftの`Array`は常に同種の値型を持ちます。異種の値をキーなしコンテナにシリアライズするカスタム`Codable`型は例外であり、`.array`データ型を使用する必要があります。
|
||||
|
||||
## Enum {#enum}
|
||||
|
||||
enum データ型は、文字列ベースのSwift enumをネイティブに格納できます。ネイティブデータベースenumは、データベースに型安全性の追加レイヤーを提供し、生のenumよりもパフォーマンスが高い場合があります。
|
||||
|
||||
ネイティブデータベースenumを定義するには、`Database`で`enum`メソッドを使用します。`case`を使用してenumの各ケースを定義します。
|
||||
|
||||
```swift
|
||||
// enum作成の例
|
||||
database.enum("planet_type")
|
||||
.case("smallRocky")
|
||||
.case("gasGiant")
|
||||
.case("dwarf")
|
||||
.create()
|
||||
```
|
||||
|
||||
enumが作成されたら、`read()`メソッドを使用してスキーマフィールドのデータ型を生成できます。
|
||||
|
||||
```swift
|
||||
// enumを読み取り、新しいフィールドを定義するために使用する例
|
||||
database.enum("planet_type").read().flatMap { planetType in
|
||||
database.schema("planets")
|
||||
.field("type", planetType, .required)
|
||||
.update()
|
||||
}
|
||||
|
||||
// または
|
||||
|
||||
let planetType = try await database.enum("planet_type").read()
|
||||
try await database.schema("planets")
|
||||
.field("type", planetType, .required)
|
||||
.update()
|
||||
```
|
||||
|
||||
enumを更新するには、`update()`を呼び出します。既存のenumからケースを削除できます。
|
||||
|
||||
```swift
|
||||
// enum更新の例
|
||||
database.enum("planet_type")
|
||||
.deleteCase("gasGiant")
|
||||
.update()
|
||||
```
|
||||
|
||||
enumを削除するには、`delete()`を呼び出します。
|
||||
|
||||
```swift
|
||||
// enum削除の例
|
||||
database.enum("planet_type").delete()
|
||||
```
|
||||
|
||||
## モデルとの結合 {#model-coupling}
|
||||
|
||||
スキーマ構築は意図的にモデルから分離されています。クエリビルディングとは異なり、スキーマビルディングはキーパスを使用せず、完全に文字列型です。これは重要です。なぜなら、特にマイグレーション用に書かれたスキーマ定義は、もはや存在しないモデルプロパティを参照する必要がある場合があるからです。
|
||||
|
||||
これをよりよく理解するために、次のマイグレーションの例を見てみましょう。
|
||||
|
||||
```swift
|
||||
struct UserMigration: AsyncMigration {
|
||||
func prepare(on database: Database) async throws {
|
||||
try await database.schema("users")
|
||||
.field("id", .uuid, .identifier(auto: false))
|
||||
.field("name", .string, .required)
|
||||
.create()
|
||||
}
|
||||
|
||||
func revert(on database: Database) async throws {
|
||||
try await database.schema("users").delete()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
このマイグレーションがすでに本番環境にプッシュされていると仮定しましょう。次に、Userモデルに次の変更を加える必要があると仮定します。
|
||||
|
||||
```diff
|
||||
- @Field(key: "name")
|
||||
- var name: String
|
||||
+ @Field(key: "first_name")
|
||||
+ var firstName: String
|
||||
+
|
||||
+ @Field(key: "last_name")
|
||||
+ var lastName: String
|
||||
```
|
||||
|
||||
次のマイグレーションで必要なデータベーススキーマの調整を行うことができます。
|
||||
|
||||
```swift
|
||||
struct UserNameMigration: AsyncMigration {
|
||||
func prepare(on database: Database) async throws {
|
||||
try await database.schema("users")
|
||||
.field("first_name", .string, .required)
|
||||
.field("last_name", .string, .required)
|
||||
.update()
|
||||
|
||||
// 現在、カスタムSQLを使用せずにこの更新を表現することはできません。
|
||||
// また、名前を姓と名に分割する処理は行いません。
|
||||
// これにはデータベース固有の構文が必要だからです。
|
||||
try await User.query(on: database)
|
||||
.set(["first_name": .sql(embed: "name")])
|
||||
.run()
|
||||
|
||||
try await database.schema("users")
|
||||
.deleteField("name")
|
||||
.update()
|
||||
}
|
||||
|
||||
func revert(on database: Database) async throws {
|
||||
try await database.schema("users")
|
||||
.field("name", .string, .required)
|
||||
.update()
|
||||
try await User.query(on: database)
|
||||
.set(["name": .sql(embed: "concat(first_name, ' ', last_name)")])
|
||||
.run()
|
||||
try await database.schema("users")
|
||||
.deleteField("first_name")
|
||||
.deleteField("last_name")
|
||||
.update()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
このマイグレーションが機能するためには、削除された`name`フィールドと新しい`firstName`および`lastName`フィールドの両方を同時に参照できる必要があることに注意してください。さらに、元の`UserMigration`は引き続き有効である必要があります。これはキーパスでは不可能でした。
|
||||
|
||||
## モデルスペースの設定 {#setting-model-space}
|
||||
|
||||
[モデルのスペース](model.md#database-space)を定義するには、テーブルを作成するときに`schema(_:space:)`にスペースを渡します。例:
|
||||
|
||||
```swift
|
||||
try await db.schema("planets", space: "mirror_universe")
|
||||
.id()
|
||||
// ...
|
||||
.create()
|
||||
```
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# トランザクション {#transactions}
|
||||
|
||||
トランザクションを使用すると、データベースにデータを保存する前に、複数の操作が正常に完了することを保証できます。
|
||||
トランザクションが開始されると、通常通りFluentクエリを実行できます。ただし、トランザクションが完了するまでデータはデータベースに保存されません。
|
||||
トランザクション中のいずれかの時点でエラーがスローされた場合(あなたまたはデータベースによって)、変更は一切反映されません。
|
||||
|
||||
トランザクションを実行するには、データベースに接続できるものへのアクセスが必要です。これは通常、受信HTTPリクエストです。これには、`req.db.transaction(_ :)`を使用します:
|
||||
```swift
|
||||
req.db.transaction { database in
|
||||
// databaseを使用
|
||||
}
|
||||
```
|
||||
トランザクションクロージャ内では、クロージャパラメータで提供されるデータベース(例では`database`という名前)を使用してクエリを実行する必要があります。
|
||||
|
||||
このクロージャが正常に返されると、トランザクションがコミットされます。
|
||||
```swift
|
||||
var sun: Star = ...
|
||||
var sirius: Star = ...
|
||||
|
||||
return req.db.transaction { database in
|
||||
return sun.save(on: database).flatMap { _ in
|
||||
return sirius.save(on: database)
|
||||
}
|
||||
}
|
||||
```
|
||||
上記の例では、トランザクションを完了する前に`sun`を保存し、*その後*`sirius`を保存します。いずれかの星の保存に失敗した場合、どちらも保存されません。
|
||||
|
||||
トランザクションが完了すると、結果を別のfutureに変換できます。例えば、以下のように完了を示すHTTPステータスに変換できます:
|
||||
```swift
|
||||
return req.db.transaction { database in
|
||||
// databaseを使用してトランザクションを実行
|
||||
}.transform(to: HTTPStatus.ok)
|
||||
```
|
||||
|
||||
## `async`/`await`
|
||||
|
||||
`async`/`await`を使用する場合、コードを以下のようにリファクタリングできます:
|
||||
|
||||
```swift
|
||||
try await req.db.transaction { transaction in
|
||||
try await sun.save(on: transaction)
|
||||
try await sirius.save(on: transaction)
|
||||
}
|
||||
return .ok
|
||||
```
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
# Redis
|
||||
|
||||
[Redis](https://redis.io/)は、キャッシュやメッセージブローカーとして一般的に使用される、最も人気のあるインメモリデータ構造ストアの1つです。
|
||||
|
||||
このライブラリは、VaporとRedisとの通信を行う基盤ドライバーである[**RediStack**](https://github.com/swift-server/RediStack)との統合です。
|
||||
|
||||
!!! note
|
||||
Redisの機能のほとんどは**RediStack**によって提供されています。
|
||||
そのドキュメントに精通することを強くお勧めします。
|
||||
|
||||
_適切な箇所にリンクを提供しています。_
|
||||
|
||||
## パッケージ {#package}
|
||||
|
||||
Redisを使用する最初のステップは、Swiftパッケージマニフェストでプロジェクトの依存関係として追加することです。
|
||||
|
||||
> この例は既存のパッケージ用です。新しいプロジェクトの開始に関するヘルプについては、メインの[Getting Started](../getting-started/hello-world.md)ガイドを参照してください。
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
// ...
|
||||
.package(url: "https://github.com/vapor/redis.git", from: "4.0.0")
|
||||
]
|
||||
// ...
|
||||
targets: [
|
||||
.target(name: "App", dependencies: [
|
||||
// ...
|
||||
.product(name: "Redis", package: "redis")
|
||||
])
|
||||
]
|
||||
```
|
||||
|
||||
## 設定 {#configure}
|
||||
|
||||
Vaporは[`RedisConnection`](https://swiftpackageindex.com/swift-server/RediStack/main/documentation/redistack/redisconnection)インスタンスのプーリング戦略を採用しており、個々の接続とプール自体を設定するためのいくつかのオプションがあります。
|
||||
|
||||
Redisを設定するために必要な最小限の要件は、接続するURLを提供することです:
|
||||
|
||||
```swift
|
||||
let app = Application()
|
||||
|
||||
app.redis.configuration = try RedisConfiguration(hostname: "localhost")
|
||||
```
|
||||
|
||||
### Redis設定 {#redis-configuration}
|
||||
|
||||
> APIドキュメント:[`RedisConfiguration`](https://api.vapor.codes/redis/documentation/redis/redisconfiguration)
|
||||
|
||||
#### serverAddresses
|
||||
|
||||
Redisインスタンスのクラスターなど、複数のRedisエンドポイントがある場合は、代わりにイニシャライザに渡す[`[SocketAddress]`](https://swiftpackageindex.com/apple/swift-nio/main/documentation/niocore/socketaddress)コレクションを作成する必要があります。
|
||||
|
||||
`SocketAddress`を作成する最も一般的な方法は、[`makeAddressResolvingHost(_:port:)`](https://swiftpackageindex.com/apple/swift-nio/main/documentation/niocore/socketaddress/makeaddressresolvinghost(_:port:))静的メソッドを使用することです。
|
||||
|
||||
```swift
|
||||
let serverAddresses: [SocketAddress] = [
|
||||
try .makeAddressResolvingHost("localhost", port: RedisConnection.Configuration.defaultPort)
|
||||
]
|
||||
```
|
||||
|
||||
単一のRedisエンドポイントの場合、`SocketAddress`の作成を処理してくれるため、便利なイニシャライザを使用する方が簡単です:
|
||||
|
||||
- [`.init(url:pool)`](https://api.vapor.codes/redis/documentation/redis/redisconfiguration/init(url:tlsconfiguration:pool:)-o9lf) (`String`または[`Foundation.URL`](https://developer.apple.com/documentation/foundation/url)を使用)
|
||||
- [`.init(hostname:port:password:database:pool:)`](https://api.vapor.codes/redis/documentation/redis/redisconfiguration/init(hostname:port:password:tlsconfiguration:database:pool:))
|
||||
|
||||
#### password
|
||||
|
||||
Redisインスタンスがパスワードで保護されている場合は、`password`引数として渡す必要があります。
|
||||
|
||||
各接続は作成時にパスワードを使用して認証されます。
|
||||
|
||||
#### database
|
||||
|
||||
これは、各接続が作成されるときに選択したいデータベースインデックスです。
|
||||
|
||||
これにより、自分でRedisに`SELECT`コマンドを送信する必要がなくなります。
|
||||
|
||||
!!! warning
|
||||
データベースの選択は維持されません。自分で`SELECT`コマンドを送信する際は注意してください。
|
||||
|
||||
### 接続プールオプション {#connection-pool-options}
|
||||
|
||||
> APIドキュメント:[`RedisConfiguration.PoolOptions`](https://api.vapor.codes/redis/documentation/redis/redisconfiguration/pooloptions)
|
||||
|
||||
!!! note
|
||||
ここでは最も一般的に変更されるオプションのみを強調しています。すべてのオプションについては、APIドキュメントを参照してください。
|
||||
|
||||
#### minimumConnectionCount
|
||||
|
||||
これは、各プールが常に維持したい接続数を設定する値です。
|
||||
|
||||
値が`0`の場合、何らかの理由で接続が失われても、プールは必要になるまで再作成しません。
|
||||
|
||||
これは「コールドスタート」接続として知られており、最小接続数を維持するよりもオーバーヘッドがあります。
|
||||
|
||||
#### maximumConnectionCount
|
||||
|
||||
このオプションは、最大接続数がどのように維持されるかの動作を決定します。
|
||||
|
||||
!!! seealso
|
||||
どのようなオプションが利用可能かについては、`RedisConnectionPoolSize` APIを参照してください。
|
||||
|
||||
## コマンドの送信 {#sending-a-command}
|
||||
|
||||
[`Application`](https://api.vapor.codes/vapor/documentation/vapor/application)または[`Request`](https://api.vapor.codes/vapor/documentation/vapor/request)インスタンスの`.redis`プロパティを使用してコマンドを送信できます。これにより、[`RedisClient`](https://swiftpackageindex.com/swift-server/RediStack/main/documentation/redistack/redisclient)にアクセスできます。
|
||||
|
||||
すべての`RedisClient`には、さまざまな[Redisコマンド](https://redis.io/commands)用の拡張機能があります。
|
||||
|
||||
```swift
|
||||
let value = try app.redis.get("my_key", as: String.self).wait()
|
||||
print(value)
|
||||
// Optional("my_value")
|
||||
|
||||
// または
|
||||
|
||||
let value = try await app.redis.get("my_key", as: String.self)
|
||||
print(value)
|
||||
// Optional("my_value")
|
||||
```
|
||||
|
||||
### サポートされていないコマンド {#unsupported-commands}
|
||||
|
||||
**RediStack**が拡張メソッドでコマンドをサポートしていない場合でも、手動で送信できます。
|
||||
|
||||
```swift
|
||||
// コマンドの後の各値は、Redisが期待する位置引数です
|
||||
try app.redis.send(command: "PING", with: ["hello"])
|
||||
.map {
|
||||
print($0)
|
||||
}
|
||||
.wait()
|
||||
// "hello"
|
||||
|
||||
// または
|
||||
|
||||
let res = try await app.redis.send(command: "PING", with: ["hello"])
|
||||
print(res)
|
||||
// "hello"
|
||||
```
|
||||
|
||||
## Pub/Subモード {#pubsub-mode}
|
||||
|
||||
Redisは、接続が特定の「チャンネル」をリッスンし、購読したチャンネルが「メッセージ」(何らかのデータ値)をパブリッシュしたときに特定のクロージャを実行できる[「Pub/Sub」モード](https://redis.io/topics/pubsub)をサポートしています。
|
||||
|
||||
サブスクリプションには定義されたライフサイクルがあります:
|
||||
|
||||
1. **subscribe**:サブスクリプションが最初に開始されたときに1回呼び出されます
|
||||
1. **message**:購読したチャンネルにメッセージがパブリッシュされるたびに0回以上呼び出されます
|
||||
1. **unsubscribe**:リクエストによるか接続が失われたかにより、サブスクリプションが終了したときに1回呼び出されます
|
||||
|
||||
サブスクリプションを作成するときは、購読したチャンネルによってパブリッシュされたすべてのメッセージを処理するために、少なくとも[`messageReceiver`](https://swiftpackageindex.com/swift-server/RediStack/main/documentation/redistack/redissubscriptionmessagereceiver)を提供する必要があります。
|
||||
|
||||
オプションで、それぞれのライフサイクルイベントを処理するために、`onSubscribe`と`onUnsubscribe`用の`RedisSubscriptionChangeHandler`を提供できます。
|
||||
|
||||
```swift
|
||||
// 指定された各チャンネルに対して1つずつ、2つのサブスクリプションを作成します
|
||||
app.redis.subscribe
|
||||
to: "channel_1", "channel_2",
|
||||
messageReceiver: { channel, message in
|
||||
switch channel {
|
||||
case "channel_1": // メッセージで何か処理を行う
|
||||
default: break
|
||||
}
|
||||
},
|
||||
onUnsubscribe: { channel, subscriptionCount in
|
||||
print("unsubscribed from \(channel)")
|
||||
print("subscriptions remaining: \(subscriptionCount)")
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
# Redis & セッション {#redis-sessions}
|
||||
|
||||
Redisは[セッションデータ](../advanced/sessions.md#session-data)(ユーザー認証情報など)をキャッシュするためのストレージプロバイダーとして機能します。
|
||||
|
||||
カスタムの[`RedisSessionsDelegate`](https://api.vapor.codes/redis/documentation/redis/redissessionsdelegate)が提供されない場合、デフォルトのものが使用されます。
|
||||
|
||||
## デフォルトの動作 {#default-behavior}
|
||||
|
||||
### SessionIDの作成 {#sessionid-creation}
|
||||
|
||||
[独自の`RedisSessionsDelegate`](#redissessionsdelegate)で[`makeNewID()`](https://api.vapor.codes/redis/documentation/redis/redissessionsdelegate/makenewid()-3hyne)メソッドを実装しない限り、すべての[`SessionID`](https://api.vapor.codes/vapor/documentation/vapor/sessionid)値は以下の手順で作成されます:
|
||||
|
||||
1. 32バイトのランダムな文字を生成
|
||||
1. その値をbase64エンコード
|
||||
|
||||
例:`Hbxozx8rTj+XXGWAzOhh1npZFXaGLpTWpWCaXuo44xQ=`
|
||||
|
||||
### SessionDataの保存 {#sessiondata-storage}
|
||||
|
||||
`RedisSessionsDelegate`のデフォルト実装は、[`SessionData`](https://api.vapor.codes/vapor/documentation/vapor/sessiondata)を`Codable`を使用してシンプルなJSON文字列値として保存します。
|
||||
|
||||
独自の`RedisSessionsDelegate`で[`makeRedisKey(for:)`](https://api.vapor.codes/redis/documentation/redis/redissessionsdelegate/makerediskey(for:)-5nfge)メソッドを実装しない限り、`SessionData`は`SessionID`に`vrs-`(**V**apor **R**edis **S**essions)というプレフィックスを付けたキーでRedisに保存されます。
|
||||
|
||||
例:`vrs-Hbxozx8rTj+XXGWAzOhh1npZFXaGLpTWpWCaXuo44xQ=`
|
||||
|
||||
## カスタムデリゲートの登録 {#registering-a-custom-delegate}
|
||||
|
||||
Redisへのデータの読み書き方法をカスタマイズするには、独自の`RedisSessionsDelegate`オブジェクトを以下のように登録します:
|
||||
|
||||
```swift
|
||||
import Redis
|
||||
|
||||
struct CustomRedisSessionsDelegate: RedisSessionsDelegate {
|
||||
// 実装
|
||||
}
|
||||
|
||||
app.sessions.use(.redis(delegate: CustomRedisSessionsDelegate()))
|
||||
```
|
||||
|
||||
## RedisSessionsDelegate {#redissessionsdelegate}
|
||||
|
||||
> APIドキュメント:[`RedisSessionsDelegate`](https://api.vapor.codes/redis/documentation/redis/redissessionsdelegate)
|
||||
|
||||
このプロトコルに準拠するオブジェクトを使用して、`SessionData`がRedisに保存される方法を変更できます。
|
||||
|
||||
プロトコルに準拠する型が実装する必要があるメソッドは2つのみです:[`redis(_:store:with:)`](https://api.vapor.codes/redis/documentation/redis/redissessionsdelegate/redis(_:store:with:))と[`redis(_:fetchDataFor:)`](https://api.vapor.codes/redis/documentation/redis/redissessionsdelegate/redis(_:fetchdatafor:))。
|
||||
|
||||
セッションデータをRedisに書き込む方法のカスタマイズは、Redisからデータを読み取る方法と本質的に関連しているため、両方とも必須です。
|
||||
|
||||
### RedisSessionsDelegateハッシュの例 {#redissessionsdelegate-hash-example}
|
||||
|
||||
例えば、セッションデータを[Redisの**ハッシュ**](https://redis.io/topics/data-types-intro#redis-hashes)として保存したい場合、以下のような実装を行います:
|
||||
|
||||
```swift
|
||||
func redis<Client: RedisClient>(
|
||||
_ client: Client,
|
||||
store data: SessionData,
|
||||
with key: RedisKey
|
||||
) -> EventLoopFuture<Void> {
|
||||
// 各データフィールドを個別のハッシュフィールドとして保存
|
||||
return client.hmset(data.snapshot, in: key)
|
||||
}
|
||||
func redis<Client: RedisClient>(
|
||||
_ client: Client,
|
||||
fetchDataFor key: RedisKey
|
||||
) -> EventLoopFuture<SessionData?> {
|
||||
return client
|
||||
.hgetall(from: key)
|
||||
.map { hash in
|
||||
// hashは[String: RESPValue]なので、値を文字列として
|
||||
// アンラップして各値をデータコンテナに保存する必要があります
|
||||
return hash.reduce(into: SessionData()) { result, next in
|
||||
guard let value = next.value.string else { return }
|
||||
result[next.key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
# Vapor リリースノート {#vapor-release-notes}
|
||||
|
||||
ドキュメントを常に最新の状態に保つことは困難、あるいは不可能であるため、ここでは Vapor エコシステムに関連する様々なパッケージのリリースノートをご覧いただけます。
|
||||
|
||||
## vapor
|
||||
|
||||
## fluent
|
||||
|
||||
## fluent-kit
|
||||
|
||||
## leaf
|
||||
|
||||
## leaf-kit
|
||||
|
||||
## fluent-postgres-driver
|
||||
|
||||
## fluent-mysql-driver
|
||||
|
||||
## fluent-sqlite-driver
|
||||
|
||||
## fluent-mongo-driver
|
||||
|
||||
## postgres-nio
|
||||
|
||||
## mysql-nio
|
||||
|
||||
## sqlite-nio
|
||||
|
||||
## postgres-kit
|
||||
|
||||
## mysql-kit
|
||||
|
||||
## sqlite-kit
|
||||
|
||||
## sql-kit
|
||||
|
||||
## apns
|
||||
|
||||
## queues
|
||||
|
||||
## queues-redis-driver
|
||||
|
||||
## redis
|
||||
|
||||
## jwt
|
||||
|
||||
## jwt-kit
|
||||
|
||||
## websocket-kit
|
||||
|
||||
## routing-kit
|
||||
|
||||
## console-kit
|
||||
|
||||
## async-kit
|
||||
|
||||
## multipart-kit
|
||||
|
||||
## toolbox
|
||||
|
||||
## core
|
||||
|
||||
## swift-codecov-action
|
||||
|
||||
## api-docs
|
||||
|
|
@ -0,0 +1,894 @@
|
|||
# 認証 {#authentication}
|
||||
|
||||
認証は、ユーザーの身元を確認する行為です。これは、ユーザー名とパスワードまたは一意のトークンのような認証情報の検証を通じて行われます。認証(auth/cとも呼ばれる)は、以前に認証されたユーザーが特定のタスクを実行する権限を確認する行為である認可(auth/z)とは異なります。
|
||||
|
||||
## はじめに {#introduction}
|
||||
|
||||
VaporのAuthentication APIは、[Basic](https://tools.ietf.org/html/rfc7617)および[Bearer](https://tools.ietf.org/html/rfc6750)を使用して、`Authorization`ヘッダーを介したユーザー認証をサポートします。また、[Content](../basics/content.md) APIからデコードされたデータを介したユーザー認証もサポートしています。
|
||||
|
||||
認証は、検証ロジックを含む`Authenticator`を作成することで実装されます。オーセンティケータは、個々のルートグループまたはアプリ全体を保護するために使用できます。Vaporには以下のオーセンティケータヘルパーが付属しています:
|
||||
|
||||
|プロトコル|説明|
|
||||
|-|-|
|
||||
|`RequestAuthenticator`/`AsyncRequestAuthenticator`|ミドルウェアを作成できる基本オーセンティケータ。|
|
||||
|[`BasicAuthenticator`/`AsyncBasicAuthenticator`](#basic)|Basic認証ヘッダーを認証します。|
|
||||
|[`BearerAuthenticator`/`AsyncBearerAuthenticator`](#bearer)|Bearer認証ヘッダーを認証します。|
|
||||
|`CredentialsAuthenticator`/`AsyncCredentialsAuthenticator`|リクエストボディから認証情報のペイロードを認証します。|
|
||||
|
||||
認証が成功した場合、オーセンティケータは検証されたユーザーを`req.auth`に追加します。このユーザーは、オーセンティケータによって保護されているルートで`req.auth.get(_:)`を使用してアクセスできます。認証が失敗した場合、ユーザーは`req.auth`に追加されず、アクセスしようとしても失敗します。
|
||||
|
||||
## Authenticatable {#authenticatable}
|
||||
|
||||
Authentication APIを使用するには、まず`Authenticatable`に準拠するユーザータイプが必要です。これは`struct`、`class`、またはFluentの`Model`でも構いません。以下の例では、`name`という1つのプロパティを持つシンプルな`User`構造体を想定しています。
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
struct User: Authenticatable {
|
||||
var name: String
|
||||
}
|
||||
```
|
||||
|
||||
以下の各例では、作成したオーセンティケータのインスタンスを使用します。これらの例では、`UserAuthenticator`と呼んでいます。
|
||||
|
||||
### ルート {#route}
|
||||
|
||||
オーセンティケータはミドルウェアであり、ルートの保護に使用できます。
|
||||
|
||||
```swift
|
||||
let protected = app.grouped(UserAuthenticator())
|
||||
protected.get("me") { req -> String in
|
||||
try req.auth.require(User.self).name
|
||||
}
|
||||
```
|
||||
|
||||
`req.auth.require`は認証された`User`を取得するために使用されます。認証が失敗した場合、このメソッドはエラーをスローし、ルートを保護します。
|
||||
|
||||
### ガードミドルウェア {#guard-middleware}
|
||||
|
||||
ルートグループで`GuardMiddleware`を使用して、ルートハンドラに到達する前にユーザーが認証されていることを確認することもできます。
|
||||
|
||||
```swift
|
||||
let protected = app.grouped(UserAuthenticator())
|
||||
.grouped(User.guardMiddleware())
|
||||
```
|
||||
|
||||
認証の要求は、オーセンティケータの構成を可能にするため、オーセンティケータミドルウェアによって行われません。[構成](#composition)の詳細については以下をお読みください。
|
||||
|
||||
## Basic {#basic}
|
||||
|
||||
Basic認証は、`Authorization`ヘッダーでユーザー名とパスワードを送信します。ユーザー名とパスワードはコロン(例:`test:secret`)で連結され、base-64エンコードされ、`"Basic "`でプレフィックスされます。次の例のリクエストは、ユーザー名`test`とパスワード`secret`をエンコードしています。
|
||||
|
||||
```http
|
||||
GET /me HTTP/1.1
|
||||
Authorization: Basic dGVzdDpzZWNyZXQ=
|
||||
```
|
||||
|
||||
Basic認証は通常、ユーザーのログインとトークンの生成に一度だけ使用されます。これにより、ユーザーの機密パスワードを送信する頻度を最小限に抑えます。プレーンテキストまたは未検証のTLS接続でBasic認証を送信しないでください。
|
||||
|
||||
アプリでBasic認証を実装するには、`BasicAuthenticator`に準拠する新しいオーセンティケータを作成します。以下は、上記のリクエストを検証するためにハードコードされた例のオーセンティケータです。
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
struct UserAuthenticator: BasicAuthenticator {
|
||||
typealias User = App.User
|
||||
|
||||
func authenticate(
|
||||
basic: BasicAuthorization,
|
||||
for request: Request
|
||||
) -> EventLoopFuture<Void> {
|
||||
if basic.username == "test" && basic.password == "secret" {
|
||||
request.auth.login(User(name: "Vapor"))
|
||||
}
|
||||
return request.eventLoop.makeSucceededFuture(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`async`/`await`を使用している場合は、代わりに`AsyncBasicAuthenticator`を使用できます:
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
struct UserAuthenticator: AsyncBasicAuthenticator {
|
||||
typealias User = App.User
|
||||
|
||||
func authenticate(
|
||||
basic: BasicAuthorization,
|
||||
for request: Request
|
||||
) async throws {
|
||||
if basic.username == "test" && basic.password == "secret" {
|
||||
request.auth.login(User(name: "Vapor"))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
このプロトコルでは、着信リクエストに`Authorization: Basic ...`ヘッダーが含まれているときに呼び出される`authenticate(basic:for:)`を実装する必要があります。ユーザー名とパスワードを含む`BasicAuthorization`構造体がメソッドに渡されます。
|
||||
|
||||
このテストオーセンティケータでは、ユーザー名とパスワードはハードコードされた値と照合されます。実際のオーセンティケータでは、データベースや外部APIと照合する可能性があります。これが`authenticate`メソッドがフューチャーを返すことができる理由です。
|
||||
|
||||
!!! tip
|
||||
パスワードはプレーンテキストでデータベースに保存しないでください。比較には常にパスワードハッシュを使用してください。
|
||||
|
||||
認証パラメータが正しい場合(この場合はハードコードされた値と一致)、Vaporという名前の`User`がログインされます。認証パラメータが一致しない場合、ユーザーはログインされず、認証が失敗したことを示します。
|
||||
|
||||
このオーセンティケータをアプリに追加し、上記で定義したルートをテストすると、ログインが成功した場合に名前`"Vapor"`が返されるはずです。認証情報が正しくない場合は、`401 Unauthorized`エラーが表示されるはずです。
|
||||
|
||||
## Bearer {#bearer}
|
||||
|
||||
Bearer認証は、`Authorization`ヘッダーでトークンを送信します。トークンには`"Bearer "`がプレフィックスされます。次の例のリクエストはトークン`foo`を送信しています。
|
||||
|
||||
```http
|
||||
GET /me HTTP/1.1
|
||||
Authorization: Bearer foo
|
||||
```
|
||||
|
||||
Bearer認証は、一般的にAPIエンドポイントの認証に使用されます。ユーザーは通常、ユーザー名とパスワードなどの認証情報をログインエンドポイントに送信してBearerトークンをリクエストします。このトークンは、アプリケーションのニーズに応じて数分から数日間有効です。
|
||||
|
||||
トークンが有効である限り、ユーザーは認証情報の代わりにそれを使用してAPIに対して認証できます。トークンが無効になった場合、ログインエンドポイントを使用して新しいトークンを生成できます。
|
||||
|
||||
アプリでBearer認証を実装するには、`BearerAuthenticator`に準拠する新しいオーセンティケータを作成します。以下は、上記のリクエストを検証するためにハードコードされた例のオーセンティケータです。
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
struct UserAuthenticator: BearerAuthenticator {
|
||||
typealias User = App.User
|
||||
|
||||
func authenticate(
|
||||
bearer: BearerAuthorization,
|
||||
for request: Request
|
||||
) -> EventLoopFuture<Void> {
|
||||
if bearer.token == "foo" {
|
||||
request.auth.login(User(name: "Vapor"))
|
||||
}
|
||||
return request.eventLoop.makeSucceededFuture(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`async`/`await`を使用している場合は、代わりに`AsyncBearerAuthenticator`を使用できます:
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
struct UserAuthenticator: AsyncBearerAuthenticator {
|
||||
typealias User = App.User
|
||||
|
||||
func authenticate(
|
||||
bearer: BearerAuthorization,
|
||||
for request: Request
|
||||
) async throws {
|
||||
if bearer.token == "foo" {
|
||||
request.auth.login(User(name: "Vapor"))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
このプロトコルでは、着信リクエストに`Authorization: Bearer ...`ヘッダーが含まれているときに呼び出される`authenticate(bearer:for:)`を実装する必要があります。トークンを含む`BearerAuthorization`構造体がメソッドに渡されます。
|
||||
|
||||
このテストオーセンティケータでは、トークンはハードコードされた値と照合されます。実際のオーセンティケータでは、データベースと照合したり、JWTで行われるような暗号化手段を使用してトークンを検証する可能性があります。これが`authenticate`メソッドがフューチャーを返すことができる理由です。
|
||||
|
||||
!!! tip
|
||||
トークン検証を実装する際は、水平方向のスケーラビリティを考慮することが重要です。アプリケーションが多くのユーザーを同時に処理する必要がある場合、認証が潜在的なボトルネックになる可能性があります。同時に実行される複数のアプリケーションインスタンスにわたって設計がどのようにスケールするかを検討してください。
|
||||
|
||||
認証パラメータが正しい場合(この場合はハードコードされた値と一致)、Vaporという名前の`User`がログインされます。認証パラメータが一致しない場合、ユーザーはログインされず、認証が失敗したことを示します。
|
||||
|
||||
このオーセンティケータをアプリに追加し、上記で定義したルートをテストすると、ログインが成功した場合に名前`"Vapor"`が返されるはずです。認証情報が正しくない場合は、`401 Unauthorized`エラーが表示されるはずです。
|
||||
|
||||
## 構成 {#composition}
|
||||
|
||||
複数のオーセンティケータを構成(組み合わせ)して、より複雑なエンドポイント認証を作成できます。オーセンティケータミドルウェアは認証が失敗してもリクエストを拒否しないため、これらのミドルウェアの複数を連鎖させることができます。オーセンティケータは2つの主要な方法で構成できます。
|
||||
|
||||
### メソッドの構成 {#composing-methods}
|
||||
|
||||
認証構成の最初の方法は、同じユーザータイプに対して複数のオーセンティケータを連鎖させることです。次の例を見てください:
|
||||
|
||||
```swift
|
||||
app.grouped(UserPasswordAuthenticator())
|
||||
.grouped(UserTokenAuthenticator())
|
||||
.grouped(User.guardMiddleware())
|
||||
.post("login")
|
||||
{ req in
|
||||
let user = try req.auth.require(User.self)
|
||||
// ユーザーで何かを行う。
|
||||
}
|
||||
```
|
||||
|
||||
この例では、両方とも`User`を認証する2つのオーセンティケータ`UserPasswordAuthenticator`と`UserTokenAuthenticator`を想定しています。これらのオーセンティケータの両方がルートグループに追加されます。最後に、`User`が正常に認証されたことを要求するため、オーセンティケータの後に`GuardMiddleware`が追加されます。
|
||||
|
||||
このオーセンティケータの構成により、パスワードまたはトークンのいずれかでアクセスできるルートが作成されます。このようなルートは、ユーザーがログインしてトークンを生成し、その後そのトークンを使用して新しいトークンを生成し続けることができます。
|
||||
|
||||
### ユーザーの構成 {#composing-users}
|
||||
|
||||
認証構成の2番目の方法は、異なるユーザータイプのオーセンティケータを連鎖させることです。次の例を見てください:
|
||||
|
||||
```swift
|
||||
app.grouped(AdminAuthenticator())
|
||||
.grouped(UserAuthenticator())
|
||||
.get("secure")
|
||||
{ req in
|
||||
guard req.auth.has(Admin.self) || req.auth.has(User.self) else {
|
||||
throw Abort(.unauthorized)
|
||||
}
|
||||
// 何かを行う。
|
||||
}
|
||||
```
|
||||
|
||||
この例では、それぞれ`Admin`と`User`を認証する2つのオーセンティケータ`AdminAuthenticator`と`UserAuthenticator`を想定しています。これらのオーセンティケータの両方がルートグループに追加されます。`GuardMiddleware`を使用する代わりに、ルートハンドラに`Admin`または`User`のいずれかが認証されたかどうかを確認するチェックが追加されます。そうでない場合は、エラーがスローされます。
|
||||
|
||||
このオーセンティケータの構成により、潜在的に異なる認証方法を持つ2つの異なるタイプのユーザーがアクセスできるルートが作成されます。このようなルートは、通常のユーザー認証を許可しながら、スーパーユーザーへのアクセスも提供できます。
|
||||
|
||||
## 手動 {#manual}
|
||||
|
||||
`req.auth`を使用して認証を手動で処理することもできます。これは特にテストに便利です。
|
||||
|
||||
ユーザーを手動でログインするには、`req.auth.login(_:)`を使用します。任意の`Authenticatable`ユーザーをこのメソッドに渡すことができます。
|
||||
|
||||
```swift
|
||||
req.auth.login(User(name: "Vapor"))
|
||||
```
|
||||
|
||||
認証されたユーザーを取得するには、`req.auth.require(_:)`を使用します
|
||||
|
||||
```swift
|
||||
let user: User = try req.auth.require(User.self)
|
||||
print(user.name) // String
|
||||
```
|
||||
|
||||
認証が失敗したときに自動的にエラーをスローしたくない場合は、`req.auth.get(_:)`を使用することもできます。
|
||||
|
||||
```swift
|
||||
let user = req.auth.get(User.self)
|
||||
print(user?.name) // String?
|
||||
```
|
||||
|
||||
ユーザーの認証を解除するには、ユーザータイプを`req.auth.logout(_:)`に渡します。
|
||||
|
||||
```swift
|
||||
req.auth.logout(User.self)
|
||||
```
|
||||
|
||||
## Fluent {#fluent}
|
||||
|
||||
[Fluent](../fluent/overview.md)は、既存のモデルに追加できる`ModelAuthenticatable`と`ModelTokenAuthenticatable`の2つのプロトコルを定義しています。モデルをこれらのプロトコルに準拠させることで、エンドポイントを保護するためのオーセンティケータを作成できます。
|
||||
|
||||
`ModelTokenAuthenticatable`はBearerトークンで認証します。これは、ほとんどのエンドポイントを保護するために使用するものです。`ModelAuthenticatable`はユーザー名とパスワードで認証し、トークンを生成するための単一のエンドポイントで使用されます。
|
||||
|
||||
このガイドは、Fluentに精通しており、データベースを使用するようにアプリを正常に設定していることを前提としています。Fluentを初めて使用する場合は、[概要](../fluent/overview.md)から始めてください。
|
||||
|
||||
### ユーザー {#user}
|
||||
|
||||
開始するには、認証されるユーザーを表すモデルが必要です。このガイドでは、次のモデルを使用しますが、既存のモデルを自由に使用できます。
|
||||
|
||||
```swift
|
||||
import Fluent
|
||||
import Vapor
|
||||
|
||||
final class User: Model, Content {
|
||||
static let schema = "users"
|
||||
|
||||
@ID(key: .id)
|
||||
var id: UUID?
|
||||
|
||||
@Field(key: "name")
|
||||
var name: String
|
||||
|
||||
@Field(key: "email")
|
||||
var email: String
|
||||
|
||||
@Field(key: "password_hash")
|
||||
var passwordHash: String
|
||||
|
||||
init() { }
|
||||
|
||||
init(id: UUID? = nil, name: String, email: String, passwordHash: String) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.email = email
|
||||
self.passwordHash = passwordHash
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
モデルは、この場合はメールアドレスであるユーザー名とパスワードハッシュを保存できる必要があります。また、重複ユーザーを避けるために、`email`を一意のフィールドに設定します。この例のモデルに対応するマイグレーションは次のとおりです:
|
||||
|
||||
```swift
|
||||
import Fluent
|
||||
import Vapor
|
||||
|
||||
extension User {
|
||||
struct Migration: AsyncMigration {
|
||||
var name: String { "CreateUser" }
|
||||
|
||||
func prepare(on database: Database) async throws {
|
||||
try await database.schema("users")
|
||||
.id()
|
||||
.field("name", .string, .required)
|
||||
.field("email", .string, .required)
|
||||
.field("password_hash", .string, .required)
|
||||
.unique(on: "email")
|
||||
.create()
|
||||
}
|
||||
|
||||
func revert(on database: Database) async throws {
|
||||
try await database.schema("users").delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`app.migrations`にマイグレーションを追加することを忘れないでください。
|
||||
|
||||
```swift
|
||||
app.migrations.add(User.Migration())
|
||||
```
|
||||
|
||||
!!! tip
|
||||
メールアドレスは大文字小文字を区別しないため、データベースに保存する前にメールアドレスを小文字に強制する[`Middleware`](../fluent/model.md#lifecycle)を追加することをお勧めします。ただし、`ModelAuthenticatable`は大文字小文字を区別する比較を使用するため、これを行う場合は、クライアントでの大文字小文字の強制、またはカスタムオーセンティケータを使用して、ユーザーの入力がすべて小文字であることを確認する必要があります。
|
||||
|
||||
最初に必要なのは、新しいユーザーを作成するためのエンドポイントです。`POST /users`を使用しましょう。このエンドポイントが期待するデータを表す[Content](../basics/content.md)構造体を作成します。
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
extension User {
|
||||
struct Create: Content {
|
||||
var name: String
|
||||
var email: String
|
||||
var password: String
|
||||
var confirmPassword: String
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
必要に応じて、この構造体を[Validatable](../basics/validation.md)に準拠させて、検証要件を追加できます。
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
extension User.Create: Validatable {
|
||||
static func validations(_ validations: inout Validations) {
|
||||
validations.add("name", as: String.self, is: !.empty)
|
||||
validations.add("email", as: String.self, is: .email)
|
||||
validations.add("password", as: String.self, is: .count(8...))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
これで`POST /users`エンドポイントを作成できます。
|
||||
|
||||
```swift
|
||||
app.post("users") { req async throws -> User in
|
||||
try User.Create.validate(content: req)
|
||||
let create = try req.content.decode(User.Create.self)
|
||||
guard create.password == create.confirmPassword else {
|
||||
throw Abort(.badRequest, reason: "Passwords did not match")
|
||||
}
|
||||
let user = try User(
|
||||
name: create.name,
|
||||
email: create.email,
|
||||
passwordHash: Bcrypt.hash(create.password)
|
||||
)
|
||||
try await user.save(on: req.db)
|
||||
return user
|
||||
}
|
||||
```
|
||||
|
||||
このエンドポイントは、着信リクエストを検証し、`User.Create`構造体をデコードし、パスワードが一致することを確認します。次に、デコードされたデータを使用して新しい`User`を作成し、データベースに保存します。プレーンテキストのパスワードは、データベースに保存する前に`Bcrypt`を使用してハッシュされます。
|
||||
|
||||
プロジェクトをビルドして実行し、最初にデータベースをマイグレートしてから、次のリクエストを使用して新しいユーザーを作成します。
|
||||
|
||||
```http
|
||||
POST /users HTTP/1.1
|
||||
Content-Length: 97
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Vapor",
|
||||
"email": "test@vapor.codes",
|
||||
"password": "secret42",
|
||||
"confirmPassword": "secret42"
|
||||
}
|
||||
```
|
||||
|
||||
#### Model Authenticatable {#model-authenticatable}
|
||||
|
||||
これで、ユーザーモデルと新しいユーザーを作成するためのエンドポイントができたので、モデルを`ModelAuthenticatable`に準拠させましょう。これにより、モデルをユーザー名とパスワードを使用して認証できるようになります。
|
||||
|
||||
```swift
|
||||
import Fluent
|
||||
import Vapor
|
||||
|
||||
extension User: ModelAuthenticatable {
|
||||
static let usernameKey = \User.$email
|
||||
static let passwordHashKey = \User.$passwordHash
|
||||
|
||||
func verify(password: String) throws -> Bool {
|
||||
try Bcrypt.verify(password, created: self.passwordHash)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
この拡張は`User`に`ModelAuthenticatable`準拠を追加します。最初の2つのプロパティは、ユーザー名とパスワードハッシュをそれぞれ保存するために使用するフィールドを指定します。`\`記法は、Fluentがそれらにアクセスするために使用できるフィールドへのキーパスを作成します。
|
||||
|
||||
最後の要件は、Basic認証ヘッダーで送信されたプレーンテキストパスワードを検証するメソッドです。サインアップ中にパスワードをハッシュするためにBcryptを使用しているため、Bcryptを使用して、提供されたパスワードが保存されたパスワードハッシュと一致することを検証します。
|
||||
|
||||
`User`が`ModelAuthenticatable`に準拠したので、ログインルートを保護するためのオーセンティケータを作成できます。
|
||||
|
||||
```swift
|
||||
let passwordProtected = app.grouped(User.authenticator())
|
||||
passwordProtected.post("login") { req -> User in
|
||||
try req.auth.require(User.self)
|
||||
}
|
||||
```
|
||||
|
||||
`ModelAuthenticatable`は、オーセンティケータを作成するための静的メソッド`authenticator`を追加します。
|
||||
|
||||
次のリクエストを送信して、このルートが機能することをテストします。
|
||||
|
||||
```http
|
||||
POST /login HTTP/1.1
|
||||
Authorization: Basic dGVzdEB2YXBvci5jb2RlczpzZWNyZXQ0Mg==
|
||||
```
|
||||
|
||||
このリクエストは、Basic認証ヘッダーを介してユーザー名`test@vapor.codes`とパスワード`secret42`を渡します。以前に作成したユーザーが返されるはずです。
|
||||
|
||||
理論的にはBasic認証を使用してすべてのエンドポイントを保護できますが、代わりに別のトークンを使用することをお勧めします。これにより、ユーザーの機密パスワードをインターネット経由で送信する頻度が最小限に抑えられます。また、パスワードハッシュはログイン中にのみ実行する必要があるため、認証がはるかに高速になります。
|
||||
|
||||
### ユーザートークン {#user-token}
|
||||
|
||||
ユーザートークンを表す新しいモデルを作成します。
|
||||
|
||||
```swift
|
||||
import Fluent
|
||||
import Vapor
|
||||
|
||||
final class UserToken: Model, Content {
|
||||
static let schema = "user_tokens"
|
||||
|
||||
@ID(key: .id)
|
||||
var id: UUID?
|
||||
|
||||
@Field(key: "value")
|
||||
var value: String
|
||||
|
||||
@Parent(key: "user_id")
|
||||
var user: User
|
||||
|
||||
init() { }
|
||||
|
||||
init(id: UUID? = nil, value: String, userID: User.IDValue) {
|
||||
self.id = id
|
||||
self.value = value
|
||||
self.$user.id = userID
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
このモデルには、トークンの一意の文字列を保存するための`value`フィールドが必要です。また、ユーザーモデルへの[親関係](../fluent/overview.md#parent)も必要です。有効期限などの追加のプロパティを必要に応じてこのトークンに追加できます。
|
||||
|
||||
次に、このモデルのマイグレーションを作成します。
|
||||
|
||||
```swift
|
||||
import Fluent
|
||||
|
||||
extension UserToken {
|
||||
struct Migration: AsyncMigration {
|
||||
var name: String { "CreateUserToken" }
|
||||
|
||||
func prepare(on database: Database) async throws {
|
||||
try await database.schema("user_tokens")
|
||||
.id()
|
||||
.field("value", .string, .required)
|
||||
.field("user_id", .uuid, .required, .references("users", "id"))
|
||||
.unique(on: "value")
|
||||
.create()
|
||||
}
|
||||
|
||||
func revert(on database: Database) async throws {
|
||||
try await database.schema("user_tokens").delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
このマイグレーションは`value`フィールドを一意にすることに注意してください。また、`user_id`フィールドとusersテーブル間の外部キー参照も作成します。
|
||||
|
||||
`app.migrations`にマイグレーションを追加することを忘れないでください。
|
||||
|
||||
```swift
|
||||
app.migrations.add(UserToken.Migration())
|
||||
```
|
||||
|
||||
最後に、新しいトークンを生成するメソッドを`User`に追加します。このメソッドはログイン中に使用されます。
|
||||
|
||||
```swift
|
||||
extension User {
|
||||
func generateToken() throws -> UserToken {
|
||||
try .init(
|
||||
value: [UInt8].random(count: 16).base64,
|
||||
userID: self.requireID()
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
ここでは、`[UInt8].random(count:)`を使用してランダムなトークン値を生成しています。この例では、16バイト(128ビット)のランダムデータを使用しています。必要に応じてこの数値を調整できます。ランダムデータは、HTTPヘッダーで簡単に送信できるようにbase-64エンコードされます。
|
||||
|
||||
ユーザートークンを生成できるようになったので、`POST /login`ルートを更新してトークンを作成して返すようにします。
|
||||
|
||||
```swift
|
||||
let passwordProtected = app.grouped(User.authenticator())
|
||||
passwordProtected.post("login") { req async throws -> UserToken in
|
||||
let user = try req.auth.require(User.self)
|
||||
let token = try user.generateToken()
|
||||
try await token.save(on: req.db)
|
||||
return token
|
||||
}
|
||||
```
|
||||
|
||||
上記と同じログインリクエストを使用して、このルートが機能することをテストします。ログイン時に次のようなトークンを取得するはずです:
|
||||
|
||||
```
|
||||
8gtg300Jwdhc/Ffw784EXA==
|
||||
```
|
||||
|
||||
後で使用するので、取得したトークンを保持してください。
|
||||
|
||||
#### Model Token Authenticatable {#model-token-authenticatable}
|
||||
|
||||
`UserToken`を`ModelTokenAuthenticatable`に準拠させます。これにより、トークンが`User`モデルを認証できるようになります。
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
import Fluent
|
||||
|
||||
extension UserToken: ModelTokenAuthenticatable {
|
||||
static var valueKey: KeyPath<UserToken, Field<String>> { \.$value }
|
||||
static var userKey: KeyPath<UserToken, Parent<User>> { \.$user }
|
||||
|
||||
var isValid: Bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
最初のプロトコル要件は、トークンの一意の値を保存するフィールドを指定します。これは、Bearer認証ヘッダーで送信される値です。2番目の要件は、`User`モデルへの親関係を指定します。これは、Fluentが認証されたユーザーを検索する方法です。
|
||||
|
||||
最後の要件は`isValid`ブール値です。これが`false`の場合、トークンはデータベースから削除され、ユーザーは認証されません。簡単にするために、これを`true`にハードコードしてトークンを永続的にします。
|
||||
|
||||
トークンが`ModelTokenAuthenticatable`に準拠したので、ルートを保護するためのオーセンティケータを作成できます。
|
||||
|
||||
現在認証されているユーザーを取得するための新しいエンドポイント`GET /me`を作成します。
|
||||
|
||||
```swift
|
||||
let tokenProtected = app.grouped(UserToken.authenticator())
|
||||
tokenProtected.get("me") { req -> User in
|
||||
try req.auth.require(User.self)
|
||||
}
|
||||
```
|
||||
|
||||
`User`と同様に、`UserToken`にはオーセンティケータを生成できる静的な`authenticator()`メソッドがあります。オーセンティケータは、Bearer認証ヘッダーで提供された値を使用して一致する`UserToken`を見つけようとします。一致するものが見つかった場合、関連する`User`を取得して認証します。
|
||||
|
||||
`POST /login`リクエストから保存した値をトークンとして使用して、次のHTTPリクエストを送信してこのルートが機能することをテストします。
|
||||
|
||||
```http
|
||||
GET /me HTTP/1.1
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
認証された`User`が返されるはずです。
|
||||
|
||||
## セッション {#session}
|
||||
|
||||
Vaporの[Session API](../advanced/sessions.md)を使用して、リクエスト間でユーザー認証を自動的に永続化できます。これは、ログインに成功した後、ユーザーの一意の識別子をリクエストのセッションデータに保存することで機能します。後続のリクエストでは、ユーザーの識別子がセッションから取得され、ルートハンドラを呼び出す前にユーザーを認証するために使用されます。
|
||||
|
||||
セッションは、Webブラウザに直接HTMLを提供するVaporで構築されたフロントエンドWebアプリケーションに最適です。APIの場合、リクエスト間でユーザーデータを永続化するために、ステートレスなトークンベースの認証を使用することをお勧めします。
|
||||
|
||||
### Session Authenticatable {#session-authenticatable}
|
||||
|
||||
セッションベースの認証を使用するには、`SessionAuthenticatable`に準拠するタイプが必要です。この例では、シンプルな構造体を使用します。
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
struct User {
|
||||
var email: String
|
||||
}
|
||||
```
|
||||
|
||||
`SessionAuthenticatable`に準拠するには、`sessionID`を指定する必要があります。これは、セッションデータに保存される値であり、ユーザーを一意に識別する必要があります。
|
||||
|
||||
```swift
|
||||
extension User: SessionAuthenticatable {
|
||||
var sessionID: String {
|
||||
self.email
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
シンプルな`User`タイプの場合、セッションの一意の識別子としてメールアドレスを使用します。
|
||||
|
||||
### セッションオーセンティケータ {#session-authenticator}
|
||||
|
||||
次に、永続化されたセッション識別子からUserのインスタンスを解決する処理を行う`SessionAuthenticator`が必要です。
|
||||
|
||||
```swift
|
||||
struct UserSessionAuthenticator: SessionAuthenticator {
|
||||
typealias User = App.User
|
||||
func authenticate(sessionID: String, for request: Request) -> EventLoopFuture<Void> {
|
||||
let user = User(email: sessionID)
|
||||
request.auth.login(user)
|
||||
return request.eventLoop.makeSucceededFuture(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`async`/`await`を使用している場合は、`AsyncSessionAuthenticator`を使用できます:
|
||||
|
||||
```swift
|
||||
struct UserSessionAuthenticator: AsyncSessionAuthenticator {
|
||||
typealias User = App.User
|
||||
func authenticate(sessionID: String, for request: Request) async throws {
|
||||
let user = User(email: sessionID)
|
||||
request.auth.login(user)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
例の`User`を初期化するために必要なすべての情報がセッション識別子に含まれているため、ユーザーを同期的に作成してログインできます。実際のアプリケーションでは、セッション識別子を使用してデータベース検索やAPIリクエストを実行し、認証する前に残りのユーザーデータを取得する可能性があります。
|
||||
|
||||
次に、初期認証を実行するためのシンプルなベアラーオーセンティケータを作成しましょう。
|
||||
|
||||
```swift
|
||||
struct UserBearerAuthenticator: AsyncBearerAuthenticator {
|
||||
func authenticate(bearer: BearerAuthorization, for request: Request) async throws {
|
||||
if bearer.token == "test" {
|
||||
let user = User(email: "hello@vapor.codes")
|
||||
request.auth.login(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
このオーセンティケータは、ベアラートークン`test`が送信されたときに、メール`hello@vapor.codes`を持つユーザーを認証します。
|
||||
|
||||
最後に、これらのすべての部分をアプリケーションで組み合わせましょう。
|
||||
|
||||
```swift
|
||||
// ユーザー認証を必要とする保護されたルートグループを作成します。
|
||||
let protected = app.routes.grouped([
|
||||
app.sessions.middleware,
|
||||
UserSessionAuthenticator(),
|
||||
UserBearerAuthenticator(),
|
||||
User.guardMiddleware(),
|
||||
])
|
||||
|
||||
// ユーザーのメールを読み取るためのGET /meルートを追加します。
|
||||
protected.get("me") { req -> String in
|
||||
try req.auth.require(User.self).email
|
||||
}
|
||||
```
|
||||
|
||||
`SessionsMiddleware`が最初に追加され、アプリケーションでセッションサポートが有効になります。セッションの設定に関する詳細は、[Session API](../advanced/sessions.md)セクションにあります。
|
||||
|
||||
次に、`SessionAuthenticator`が追加されます。これは、セッションがアクティブな場合にユーザーを認証する処理を行います。
|
||||
|
||||
認証がまだセッションに永続化されていない場合、リクエストは次のオーセンティケータに転送されます。`UserBearerAuthenticator`はベアラートークンをチェックし、それが`"test"`と等しい場合にユーザーを認証します。
|
||||
|
||||
最後に、`User.guardMiddleware()`は、`User`が前のミドルウェアのいずれかによって認証されたことを確認します。ユーザーが認証されていない場合、エラーがスローされます。
|
||||
|
||||
このルートをテストするには、まず次のリクエストを送信します:
|
||||
|
||||
```http
|
||||
GET /me HTTP/1.1
|
||||
authorization: Bearer test
|
||||
```
|
||||
|
||||
これにより、`UserBearerAuthenticator`がユーザーを認証します。認証されると、`UserSessionAuthenticator`はユーザーの識別子をセッションストレージに永続化し、クッキーを生成します。レスポンスからのクッキーを使用して、ルートへの2番目のリクエストを行います。
|
||||
|
||||
```http
|
||||
GET /me HTTP/1.1
|
||||
cookie: vapor_session=123
|
||||
```
|
||||
|
||||
今回は、`UserSessionAuthenticator`がユーザーを認証し、再びユーザーのメールが返されるはずです。
|
||||
|
||||
### Model Session Authenticatable {#model-session-authenticatable}
|
||||
|
||||
Fluentモデルは、`ModelSessionAuthenticatable`に準拠することで`SessionAuthenticator`を生成できます。これは、モデルの一意の識別子をセッション識別子として使用し、セッションからモデルを復元するためのデータベース検索を自動的に実行します。
|
||||
|
||||
```swift
|
||||
import Fluent
|
||||
|
||||
final class User: Model { ... }
|
||||
|
||||
// このモデルをセッションに永続化できるようにします。
|
||||
extension User: ModelSessionAuthenticatable { }
|
||||
```
|
||||
|
||||
`ModelSessionAuthenticatable`を既存のモデルに空の準拠として追加できます。追加されると、そのモデルの`SessionAuthenticator`を作成するための新しい静的メソッドが利用可能になります。
|
||||
|
||||
```swift
|
||||
User.sessionAuthenticator()
|
||||
```
|
||||
|
||||
これは、ユーザーを解決するためにアプリケーションのデフォルトデータベースを使用します。データベースを指定するには、識別子を渡します。
|
||||
|
||||
```swift
|
||||
User.sessionAuthenticator(.sqlite)
|
||||
```
|
||||
|
||||
## ウェブサイト認証 {#website-authentication}
|
||||
|
||||
ウェブサイトは、ブラウザの使用により認証情報をブラウザに添付する方法が制限されるため、認証の特殊なケースです。これにより、2つの異なる認証シナリオが発生します:
|
||||
|
||||
* フォームを介した初回ログイン
|
||||
* セッションクッキーで認証される後続の呼び出し
|
||||
|
||||
VaporとFluentは、これをシームレスにするためのいくつかのヘルパーを提供します。
|
||||
|
||||
### セッション認証 {#session-authentication}
|
||||
|
||||
セッション認証は上記で説明したとおりに機能します。ユーザーがアクセスするすべてのルートにセッションミドルウェアとセッションオーセンティケータを適用する必要があります。これには、保護されたルート、ログインしている場合にユーザーにアクセスしたいパブリックルート(アカウントボタンを表示するためなど)、**および**ログインルートが含まれます。
|
||||
|
||||
**configure.swift**でアプリにグローバルに有効にできます:
|
||||
|
||||
```swift
|
||||
app.middleware.use(app.sessions.middleware)
|
||||
app.middleware.use(User.sessionAuthenticator())
|
||||
```
|
||||
|
||||
これらのミドルウェアは次のことを行います:
|
||||
|
||||
* セッションミドルウェアは、リクエストで提供されたセッションクッキーを取得し、セッションに変換します
|
||||
* セッションオーセンティケータはセッションを取得し、そのセッションに認証されたユーザーがいるかどうかを確認します。いる場合、ミドルウェアはリクエストを認証します。レスポンスでは、セッションオーセンティケータはリクエストに認証されたユーザーがいるかどうかを確認し、次のリクエストで認証されるようにセッションに保存します。
|
||||
|
||||
!!! note
|
||||
セッションクッキーはデフォルトで`secure`と`httpOnly`に設定されていません。クッキーの設定方法の詳細については、Vaporの[Session API](../advanced/sessions.md#configuration)を確認してください。
|
||||
|
||||
### ルートの保護 {#protecting-routes}
|
||||
|
||||
APIのルートを保護する場合、従来はリクエストが認証されていない場合に**401 Unauthorized**などのステータスコードでHTTPレスポンスを返します。しかし、これはブラウザを使用している人にとって良いユーザーエクスペリエンスではありません。Vaporは、このシナリオで使用する任意の`Authenticatable`タイプ用の`RedirectMiddleware`を提供します:
|
||||
|
||||
```swift
|
||||
let protectedRoutes = app.grouped(User.redirectMiddleware(path: "/login?loginRequired=true"))
|
||||
```
|
||||
|
||||
`RedirectMiddleware`オブジェクトは、高度なURL処理のために作成時にリダイレクトパスを`String`として返すクロージャを渡すこともサポートしています。例えば、状態管理のためにリダイレクト元のパスをリダイレクト先のクエリパラメータとして含めることができます。
|
||||
|
||||
```swift
|
||||
let redirectMiddleware = User.redirectMiddleware { req -> String in
|
||||
return "/login?authRequired=true&next=\(req.url.path)"
|
||||
}
|
||||
```
|
||||
|
||||
これは`GuardMiddleware`と同様に機能します。`protectedRoutes`に登録されたルートへの認証されていないリクエストは、提供されたパスにリダイレクトされます。これにより、単に**401 Unauthorized**を提供するのではなく、ユーザーにログインするよう指示できます。
|
||||
|
||||
`RedirectMiddleware`を実行する前に認証されたユーザーが読み込まれるように、`RedirectMiddleware`の前にセッションオーセンティケータを含めてください。
|
||||
|
||||
```swift
|
||||
let protectedRoutes = app.grouped([User.sessionAuthenticator(), redirectMiddleware])
|
||||
```
|
||||
|
||||
### フォームログイン {#form-log-in}
|
||||
|
||||
ユーザーとセッションでの将来のリクエストを認証するには、ユーザーをログインする必要があります。Vaporは、フォームを介したログインを処理する`ModelCredentialsAuthenticatable`プロトコルを提供します。まず、`User`をこのプロトコルに準拠させます:
|
||||
|
||||
```swift
|
||||
extension User: ModelCredentialsAuthenticatable {
|
||||
static let usernameKey = \User.$email
|
||||
static let passwordHashKey = \User.$password
|
||||
|
||||
func verify(password: String) throws -> Bool {
|
||||
try Bcrypt.verify(password, created: self.password)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
これは`ModelAuthenticatable`と同じであり、すでにそれに準拠している場合は何もする必要はありません。次に、この`ModelCredentialsAuthenticator`ミドルウェアをログインフォームのPOSTリクエストに適用します:
|
||||
|
||||
```swift
|
||||
let credentialsProtectedRoute = sessionRoutes.grouped(User.credentialsAuthenticator())
|
||||
credentialsProtectedRoute.post("login", use: loginPostHandler)
|
||||
```
|
||||
|
||||
これは、ログインルートを保護するためにデフォルトの認証情報オーセンティケータを使用します。POSTリクエストで`username`と`password`を送信する必要があります。次のようにフォームを設定できます:
|
||||
|
||||
```html
|
||||
<form method="POST" action="/login">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" placeholder="Username" name="username" autocomplete="username" required autofocus>
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" placeholder="Password" name="password" autocomplete="current-password" required>
|
||||
<input type="submit" value="Sign In">
|
||||
</form>
|
||||
```
|
||||
|
||||
`CredentialsAuthenticator`は、リクエストボディから`username`と`password`を抽出し、ユーザー名からユーザーを見つけ、パスワードを検証します。パスワードが有効な場合、ミドルウェアはリクエストを認証します。その後、`SessionAuthenticator`が後続のリクエストのためにセッションを認証します。
|
||||
|
||||
## JWT {#jwt}
|
||||
|
||||
[JWT](jwt.md)は、着信リクエストでJSON Web Tokenを認証するために使用できる`JWTAuthenticator`を提供します。JWTを初めて使用する場合は、[概要](jwt.md)を確認してください。
|
||||
|
||||
まず、JWTペイロードを表すタイプを作成します。
|
||||
|
||||
```swift
|
||||
// 例のJWTペイロード。
|
||||
struct SessionToken: Content, Authenticatable, JWTPayload {
|
||||
|
||||
// 定数
|
||||
let expirationTime: TimeInterval = 60 * 15
|
||||
|
||||
// トークンデータ
|
||||
var expiration: ExpirationClaim
|
||||
var userId: UUID
|
||||
|
||||
init(userId: UUID) {
|
||||
self.userId = userId
|
||||
self.expiration = ExpirationClaim(value: Date().addingTimeInterval(expirationTime))
|
||||
}
|
||||
|
||||
init(with user: User) throws {
|
||||
self.userId = try user.requireID()
|
||||
self.expiration = ExpirationClaim(value: Date().addingTimeInterval(expirationTime))
|
||||
}
|
||||
|
||||
func verify(using algorithm: some JWTAlgorithm) throws {
|
||||
try expiration.verifyNotExpired()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
次に、ログインレスポンスの成功時に含まれるデータの表現を定義できます。今のところ、レスポンスには署名されたJWTを表す文字列であるプロパティが1つだけあります。
|
||||
|
||||
```swift
|
||||
struct ClientTokenResponse: Content {
|
||||
var token: String
|
||||
}
|
||||
```
|
||||
|
||||
JWTトークンとレスポンスのモデルを使用して、`ClientTokenResponse`を返し、署名された`SessionToken`を含むパスワード保護されたログインルートを使用できます。
|
||||
|
||||
```swift
|
||||
let passwordProtected = app.grouped(User.authenticator(), User.guardMiddleware())
|
||||
passwordProtected.post("login") { req async throws -> ClientTokenResponse in
|
||||
let user = try req.auth.require(User.self)
|
||||
let payload = try SessionToken(with: user)
|
||||
return ClientTokenResponse(token: try await req.jwt.sign(payload))
|
||||
}
|
||||
```
|
||||
|
||||
また、オーセンティケータを使用したくない場合は、次のようなものを使用できます。
|
||||
```swift
|
||||
app.post("login") { req async throws -> ClientTokenResponse in
|
||||
// ユーザーの提供された認証情報を検証
|
||||
// 提供されたユーザーのuserIdを取得
|
||||
let payload = try SessionToken(userId: userId)
|
||||
return ClientTokenResponse(token: try await req.jwt.sign(payload))
|
||||
}
|
||||
```
|
||||
|
||||
ペイロードを`Authenticatable`と`JWTPayload`に準拠させることで、`authenticator()`メソッドを使用してルートオーセンティケータを生成できます。ルートグループにこれを追加して、ルートが呼び出される前にJWTを自動的に取得して検証します。
|
||||
|
||||
```swift
|
||||
// SessionToken JWTを必要とするルートグループを作成します。
|
||||
let secure = app.grouped(SessionToken.authenticator(), SessionToken.guardMiddleware())
|
||||
```
|
||||
|
||||
オプションの[ガードミドルウェア](#guard-middleware)を追加すると、認証が成功したことが必要になります。
|
||||
|
||||
保護されたルート内では、`req.auth`を使用して認証されたJWTペイロードにアクセスできます。
|
||||
|
||||
```swift
|
||||
// ユーザーが提供したトークンが有効な場合、okレスポンスを返します。
|
||||
secure.post("validateLoggedInUser") { req -> HTTPStatus in
|
||||
let sessionToken = try req.auth.require(SessionToken.self)
|
||||
print(sessionToken.userId)
|
||||
return .ok
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
# 暗号 {#crypto}
|
||||
|
||||
Vapor には [SwiftCrypto](https://github.com/apple/swift-crypto/) が含まれており、これは Apple の CryptoKit ライブラリの Linux 互換ポートです。SwiftCrypto がまだサポートしていない [Bcrypt](https://ja.wikipedia.org/wiki/Bcrypt) や [TOTP](https://ja.wikipedia.org/wiki/Time-based_One-time_Password) のような追加の暗号 API も公開されています。
|
||||
|
||||
## SwiftCrypto {#swiftcrypto}
|
||||
|
||||
Swift の `Crypto` ライブラリは Apple の CryptoKit API を実装しています。そのため、[CryptoKit ドキュメント](https://developer.apple.com/documentation/cryptokit) と [WWDC トーク](https://developer.apple.com/videos/play/wwdc2019/709) は API を学ぶための優れたリソースです。
|
||||
|
||||
これらの API は Vapor をインポートすると自動的に利用可能になります。
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
let digest = SHA256.hash(data: Data("hello".utf8))
|
||||
print(digest)
|
||||
```
|
||||
|
||||
CryptoKit には以下のサポートが含まれています:
|
||||
|
||||
- ハッシュ化:`SHA512`、`SHA384`、`SHA256`
|
||||
- メッセージ認証コード:`HMAC`
|
||||
- 暗号:`AES`、`ChaChaPoly`
|
||||
- 公開鍵暗号:`Curve25519`、`P521`、`P384`、`P256`
|
||||
- 安全でないハッシュ化:`SHA1`、`MD5`
|
||||
|
||||
## Bcrypt {#bcrypt}
|
||||
|
||||
Bcrypt はランダム化されたソルトを使用して、同じパスワードを複数回ハッシュ化しても同じダイジェストにならないようにするパスワードハッシュアルゴリズムです。
|
||||
|
||||
Vapor はパスワードのハッシュ化と比較のための `Bcrypt` 型を提供しています。
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
let digest = try Bcrypt.hash("test")
|
||||
```
|
||||
|
||||
Bcrypt はソルトを使用するため、パスワードハッシュを直接比較することはできません。平文パスワードと既存のダイジェストの両方を一緒に検証する必要があります。
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
let pass = try Bcrypt.verify("test", created: digest)
|
||||
if pass {
|
||||
// パスワードとダイジェストが一致します。
|
||||
} else {
|
||||
// パスワードが間違っています。
|
||||
}
|
||||
```
|
||||
|
||||
Bcrypt パスワードでのログインは、まずメールアドレスまたはユーザー名でデータベースからユーザーのパスワードダイジェストを取得することで実装できます。その後、既知のダイジェストを提供された平文パスワードに対して検証できます。
|
||||
|
||||
## OTP {#otp}
|
||||
|
||||
Vapor は HOTP と TOTP の両方のワンタイムパスワードをサポートしています。OTP は SHA-1、SHA-256、SHA-512 ハッシュ関数で動作し、6 桁、7 桁、または 8 桁の出力を提供できます。OTP は、単一使用の人間が読めるパスワードを生成することで認証を提供します。これを行うために、両当事者はまず対称鍵に合意し、生成されたパスワードのセキュリティを維持するために常に秘密にしておく必要があります。
|
||||
|
||||
#### HOTP {#hotp}
|
||||
|
||||
HOTP は HMAC 署名に基づく OTP です。対称鍵に加えて、両当事者はパスワードの一意性を提供する数値であるカウンターにも合意します。各生成試行後、カウンターは増加します。
|
||||
|
||||
```swift
|
||||
let key = SymmetricKey(size: .bits128)
|
||||
let hotp = HOTP(key: key, digest: .sha256, digits: .six)
|
||||
let code = hotp.generate(counter: 25)
|
||||
|
||||
// または静的 generate 関数を使用
|
||||
HOTP.generate(key: key, digest: .sha256, digits: .six, counter: 25)
|
||||
```
|
||||
|
||||
#### TOTP {#totp}
|
||||
|
||||
TOTP は HOTP の時間ベースのバリエーションです。ほとんど同じように動作しますが、単純なカウンターの代わりに、現在の時刻を使用して一意性を生成します。非同期クロック、ネットワーク遅延、ユーザーの遅延、およびその他の混乱要因によって導入される避けられないずれを補償するために、生成された TOTP コードは指定された時間間隔(最も一般的には 30 秒)にわたって有効のままです。
|
||||
|
||||
```swift
|
||||
let key = SymmetricKey(size: .bits128)
|
||||
let totp = TOTP(key: key, digest: .sha256, digits: .six, interval: 60)
|
||||
let code = totp.generate(time: Date())
|
||||
|
||||
// または静的 generate 関数を使用
|
||||
TOTP.generate(key: key, digest: .sha256, digits: .six, interval: 60, time: Date())
|
||||
```
|
||||
|
||||
#### 範囲 {#range}
|
||||
|
||||
OTP は検証での余裕と同期外れカウンターを提供するのに非常に便利です。両方の OTP 実装には、エラーのマージンを持って OTP を生成する能力があります。
|
||||
|
||||
```swift
|
||||
let key = SymmetricKey(size: .bits128)
|
||||
let hotp = HOTP(key: key, digest: .sha256, digits: .six)
|
||||
|
||||
// 正しいカウンターのウィンドウを生成
|
||||
let codes = hotp.generate(counter: 25, range: 2)
|
||||
```
|
||||
|
||||
上記の例では 2 のマージンを許可しており、これは HOTP がカウンター値 `23 ... 27` に対して計算され、これらのコードすべてが返されることを意味します。
|
||||
|
||||
!!! warning "警告"
|
||||
注意:使用するエラーマージンが大きくなるほど、攻撃者が行動するための時間と自由度が増え、アルゴリズムのセキュリティが低下します。
|
||||
|
|
@ -0,0 +1,437 @@
|
|||
# JWT
|
||||
|
||||
JSON Web Token (JWT) は、JSON オブジェクトとして当事者間で情報を安全に送信するための、コンパクトで自己完結型の方法を定義するオープンスタンダード ([RFC 7519](https://tools.ietf.org/html/rfc7519)) です。この情報はデジタル署名されているため、検証可能で信頼できます。
|
||||
|
||||
JWT は Web アプリケーションで特に有用で、ステートレスな認証/認可や情報交換によく使用されます。JWT の背後にある理論については、上記のリンク先の仕様書または [jwt.io](https://jwt.io/introduction) で詳しく読むことができます。
|
||||
|
||||
Vapor は `JWT` モジュールを通じて JWT のファーストクラスサポートを提供しています。このモジュールは `JWTKit` ライブラリの上に構築されており、[SwiftCrypto](https://github.com/apple/swift-crypto) に基づく JWT 標準の Swift 実装です。JWTKit は、HMAC、ECDSA、EdDSA、RSA を含むさまざまなアルゴリズムの署名者と検証者を提供します。
|
||||
|
||||
## はじめに {#getting-started}
|
||||
|
||||
Vapor アプリケーションで JWT を使用する最初のステップは、プロジェクトの `Package.swift` ファイルに `JWT` 依存関係を追加することです:
|
||||
|
||||
```swift
|
||||
// swift-tools-version:5.10
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "my-app",
|
||||
dependencies: [
|
||||
// Other dependencies...
|
||||
.package(url: "https://github.com/vapor/jwt.git", from: "5.0.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "App", dependencies: [
|
||||
// Other dependencies...
|
||||
.product(name: "JWT", package: "jwt")
|
||||
]),
|
||||
// Other targets...
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### 設定 {#configuration}
|
||||
|
||||
依存関係を追加した後、アプリケーションで `JWT` モジュールを使い始めることができます。JWT モジュールは `Application` に新しい `jwt` プロパティを追加し、設定に使用されます。その内部は [JWTKit](https://github.com/vapor/jwt-kit) ライブラリによって提供されています。
|
||||
|
||||
#### キーコレクション {#key-collection}
|
||||
|
||||
`jwt` オブジェクトには `keys` プロパティが付属しており、これは JWTKit の `JWTKeyCollection` のインスタンスです。このコレクションは、JWT の署名と検証に使用されるキーの保存と管理に使用されます。`JWTKeyCollection` は `actor` であり、コレクションに対するすべての操作がシリアライズされ、スレッドセーフであることを意味します。
|
||||
|
||||
JWT を署名または検証するには、コレクションにキーを追加する必要があります。これは通常、`configure.swift` ファイルで行われます:
|
||||
|
||||
```swift
|
||||
import JWT
|
||||
|
||||
// Add HMAC with SHA-256 signer.
|
||||
await app.jwt.keys.add(hmac: "secret", digestAlgorithm: .sha256)
|
||||
```
|
||||
|
||||
これにより、SHA-256 をダイジェストアルゴリズムとして使用する HMAC キーがキーチェーンに追加されます(JWA 記法では HS256)。利用可能なアルゴリズムの詳細については、下記の[アルゴリズム](#algorithms)セクションをご覧ください。
|
||||
|
||||
!!! note
|
||||
`"secret"` を実際のシークレットキーに置き換えてください。このキーは安全に保管する必要があり、理想的には設定ファイルまたは環境変数に保存します。
|
||||
|
||||
### 署名 {#signing}
|
||||
|
||||
追加されたキーは JWT の署名に使用できます。これを行うには、まず署名するもの、つまり「ペイロード」が必要です。
|
||||
このペイロードは、送信したいデータを含む単純な JSON オブジェクトです。`JWTPayload` プロトコルに準拠する構造体を作成することで、カスタムペイロードを作成できます:
|
||||
|
||||
```swift
|
||||
// JWT payload structure.
|
||||
struct TestPayload: JWTPayload {
|
||||
// Maps the longer Swift property names to the
|
||||
// shortened keys used in the JWT payload.
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case subject = "sub"
|
||||
case expiration = "exp"
|
||||
case isAdmin = "admin"
|
||||
}
|
||||
|
||||
// The "sub" (subject) claim identifies the principal that is the
|
||||
// subject of the JWT.
|
||||
var subject: SubjectClaim
|
||||
|
||||
// The "exp" (expiration time) claim identifies the expiration time on
|
||||
// or after which the JWT MUST NOT be accepted for processing.
|
||||
var expiration: ExpirationClaim
|
||||
|
||||
// Custom data.
|
||||
// If true, the user is an admin.
|
||||
var isAdmin: Bool
|
||||
|
||||
// Run any additional verification logic beyond
|
||||
// signature verification here.
|
||||
// Since we have an ExpirationClaim, we will
|
||||
// call its verify method.
|
||||
func verify(using algorithm: some JWTAlgorithm) async throws {
|
||||
try self.expiration.verifyNotExpired()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
ペイロードの署名は、例えばルートハンドラ内で `JWT` モジュールの `sign` メソッドを呼び出すことで行われます:
|
||||
|
||||
```swift
|
||||
app.post("login") { req async throws -> [String: String] in
|
||||
let payload = TestPayload(
|
||||
subject: "vapor",
|
||||
expiration: .init(value: .distantFuture),
|
||||
isAdmin: true
|
||||
)
|
||||
return try await ["token": req.jwt.sign(payload)]
|
||||
}
|
||||
```
|
||||
|
||||
このエンドポイントにリクエストが送信されると、レスポンスボディに署名された JWT を `String` として返し、すべてが計画通りに進んだ場合、次のようなものが表示されます:
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo"
|
||||
}
|
||||
```
|
||||
|
||||
[`jwt.io` デバッガー](https://jwt.io/#debugger)を使用して、このトークンをデコードして検証できます。デバッガーは JWT のペイロード(先ほど指定したデータであるはずです)とヘッダーを表示し、JWT の署名に使用したシークレットキーを使用して署名を検証できます。
|
||||
|
||||
### 検証 {#verifying}
|
||||
|
||||
トークンがアプリケーションに_送信された_場合、`JWT` モジュールの `verify` メソッドを呼び出すことで、トークンの真正性を検証できます:
|
||||
|
||||
```swift
|
||||
// Fetch and verify JWT from incoming request.
|
||||
app.get("me") { req async throws -> HTTPStatus in
|
||||
let payload = try await req.jwt.verify(as: TestPayload.self)
|
||||
print(payload)
|
||||
return .ok
|
||||
}
|
||||
```
|
||||
|
||||
`req.jwt.verify` ヘルパーは、`Authorization` ヘッダーでベアラートークンをチェックします。存在する場合、JWT を解析し、その署名とクレームを検証します。これらのステップのいずれかが失敗した場合、401 Unauthorized エラーがスローされます。
|
||||
|
||||
次の HTTP リクエストを送信してルートをテストします:
|
||||
|
||||
```http
|
||||
GET /me HTTP/1.1
|
||||
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo
|
||||
```
|
||||
|
||||
すべてが正常に動作した場合、`200 OK` レスポンスが返され、ペイロードが出力されます:
|
||||
|
||||
```swift
|
||||
TestPayload(
|
||||
subject: "vapor",
|
||||
expiration: 4001-01-01 00:00:00 +0000,
|
||||
isAdmin: true
|
||||
)
|
||||
```
|
||||
|
||||
完全な認証フローは [認証 → JWT](authentication.md#jwt) で確認できます。
|
||||
|
||||
## アルゴリズム {#algorithms}
|
||||
|
||||
JWT はさまざまなアルゴリズムを使用して署名できます。
|
||||
|
||||
キーチェーンにキーを追加するには、次の各アルゴリズムに対して `add` メソッドのオーバーロードが利用可能です:
|
||||
|
||||
### HMAC
|
||||
|
||||
HMAC(Hash-based Message Authentication Code)は、JWT の署名と検証にシークレットキーを使用する対称アルゴリズムです。Vapor は次の HMAC アルゴリズムをサポートしています:
|
||||
|
||||
- `HS256`:SHA-256 を使用した HMAC
|
||||
- `HS384`:SHA-384 を使用した HMAC
|
||||
- `HS512`:SHA-512 を使用した HMAC
|
||||
|
||||
```swift
|
||||
// Add an HS256 key.
|
||||
await app.jwt.keys.add(hmac: "secret", digestAlgorithm: .sha256)
|
||||
```
|
||||
|
||||
### ECDSA
|
||||
|
||||
ECDSA(Elliptic Curve Digital Signature Algorithm)は、JWT の署名と検証に公開鍵/秘密鍵のペアを使用する非対称アルゴリズムです。楕円曲線に関する数学に基づいています。Vapor は次の ECDSA アルゴリズムをサポートしています:
|
||||
|
||||
- `ES256`:P-256 曲線と SHA-256 を使用した ECDSA
|
||||
- `ES384`:P-384 曲線と SHA-384 を使用した ECDSA
|
||||
- `ES512`:P-521 曲線と SHA-512 を使用した ECDSA
|
||||
|
||||
すべてのアルゴリズムは、`ES256PublicKey` と `ES256PrivateKey` のように、公開鍵と秘密鍵の両方を提供します。PEM 形式を使用して ECDSA キーを追加できます:
|
||||
|
||||
```swift
|
||||
let ecdsaPublicKey = """
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2adMrdG7aUfZH57aeKFFM01dPnkx
|
||||
C18ScRb4Z6poMBgJtYlVtd9ly63URv57ZW0Ncs1LiZB7WATb3svu+1c7HQ==
|
||||
-----END PUBLIC KEY-----
|
||||
"""
|
||||
|
||||
// Initialize an ECDSA key with public PEM.
|
||||
let key = try ES256PublicKey(pem: ecdsaPublicKey)
|
||||
```
|
||||
|
||||
またはランダムなキーを生成できます(テストに便利です):
|
||||
|
||||
```swift
|
||||
let key = ES256PrivateKey()
|
||||
```
|
||||
|
||||
キーチェーンにキーを追加するには:
|
||||
|
||||
```swift
|
||||
await app.jwt.keys.add(ecdsa: key)
|
||||
```
|
||||
|
||||
### EdDSA
|
||||
|
||||
EdDSA(Edwards-curve Digital Signature Algorithm)は、JWT の署名と検証に公開鍵/秘密鍵のペアを使用する非対称アルゴリズムです。両方とも DSA アルゴリズムに依存している点で ECDSA に似ていますが、EdDSA は異なる楕円曲線ファミリーである Edwards 曲線に基づいており、わずかにパフォーマンスが向上しています。ただし、より新しいため、広くサポートされていません。Vapor は `Ed25519` 曲線を使用する `EdDSA` アルゴリズムのみをサポートしています。
|
||||
|
||||
EdDSA キーは、その(base-64 エンコードされた `String`)座標を使用して作成できます。公開鍵の場合は `x`、秘密鍵の場合は `d` です:
|
||||
|
||||
```swift
|
||||
let publicKey = try EdDSA.PublicKey(x: "0ZcEvMCSYqSwR8XIkxOoaYjRQSAO8frTMSCpNbUl4lE", curve: .ed25519)
|
||||
|
||||
let privateKey = try EdDSA.PrivateKey(d: "d1H3/dcg0V3XyAuZW2TE5Z3rhY20M+4YAfYu/HUQd8w=", curve: .ed25519)
|
||||
```
|
||||
|
||||
ランダムなキーを生成することもできます:
|
||||
|
||||
```swift
|
||||
let key = EdDSA.PrivateKey(curve: .ed25519)
|
||||
```
|
||||
|
||||
キーチェーンにキーを追加するには:
|
||||
|
||||
```swift
|
||||
await app.jwt.keys.add(eddsa: key)
|
||||
```
|
||||
|
||||
### RSA
|
||||
|
||||
RSA(Rivest-Shamir-Adleman)は、JWT の署名と検証に公開鍵/秘密鍵のペアを使用する非対称アルゴリズムです。
|
||||
|
||||
!!! warning
|
||||
ご覧のとおり、RSA キーは新しいユーザーがそれらを使用することを思いとどまらせるために `Insecure` 名前空間の後ろにゲートされています。これは、RSA が ECDSA および EdDSA よりも安全性が低いと見なされており、互換性の理由でのみ使用すべきだからです。
|
||||
可能であれば、他のアルゴリズムのいずれかを使用してください。
|
||||
|
||||
Vapor は次の RSA アルゴリズムをサポートしています:
|
||||
|
||||
- `RS256`:SHA-256 を使用した RSA
|
||||
- `RS384`:SHA-384 を使用した RSA
|
||||
- `RS512`:SHA-512 を使用した RSA
|
||||
|
||||
PEM 形式を使用して RSA キーを作成できます:
|
||||
|
||||
```swift
|
||||
let rsaPublicKey = """
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0cOtPjzABybjzm3fCg1aCYwnx
|
||||
PmjXpbCkecAWLj/CcDWEcuTZkYDiSG0zgglbbbhcV0vJQDWSv60tnlA3cjSYutAv
|
||||
7FPo5Cq8FkvrdDzeacwRSxYuIq1LtYnd6I30qNaNthntjvbqyMmBulJ1mzLI+Xg/
|
||||
aX4rbSL49Z3dAQn8vQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
"""
|
||||
|
||||
// Initialize an RSA key with public pem.
|
||||
let key = try Insecure.RSA.PublicKey(pem: rsaPublicKey)
|
||||
```
|
||||
|
||||
またはコンポーネントを使用して:
|
||||
|
||||
```swift
|
||||
// Initialize an RSA private key with components.
|
||||
let key = try Insecure.RSA.PrivateKey(
|
||||
modulus: modulus,
|
||||
exponent: publicExponent,
|
||||
privateExponent: privateExponent
|
||||
)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
パッケージは 2048 ビット未満の RSA キーをサポートしていません。
|
||||
|
||||
その後、キーコレクションにキーを追加できます:
|
||||
|
||||
```swift
|
||||
await app.jwt.keys.add(rsa: key, digestAlgorithm: .sha256)
|
||||
```
|
||||
|
||||
### PSS
|
||||
|
||||
RSA-PKCS1v1.5 アルゴリズムに加えて、Vapor は RSA-PSS アルゴリズムもサポートしています。PSS(Probabilistic Signature Scheme)は、RSA 署名のためのより安全なパディングスキームです。可能な場合は、PKCS1v1.5 よりも PSS を使用することが推奨されます。
|
||||
|
||||
アルゴリズムは署名フェーズでのみ異なり、キーは RSA と同じですが、キーコレクションに追加する際にパディングスキームを指定する必要があります:
|
||||
|
||||
```swift
|
||||
await app.jwt.keys.add(pss: key, digestAlgorithm: .sha256)
|
||||
```
|
||||
|
||||
## キー識別子(kid) {#key-identifier-kid}
|
||||
|
||||
キーコレクションにキーを追加する際に、キー識別子(kid)を指定することもできます。これは、コレクション内でキーを検索するために使用できるキーの一意の識別子です。
|
||||
|
||||
```swift
|
||||
// Add HMAC with SHA-256 key named "a".
|
||||
await app.jwt.keys.add(hmac: "foo", digestAlgorithm: .sha256, kid: "a")
|
||||
```
|
||||
|
||||
`kid` を指定しない場合、キーはデフォルトキーとして割り当てられます。
|
||||
|
||||
!!! note
|
||||
`kid` なしで別のキーを追加すると、デフォルトキーは上書きされます。
|
||||
|
||||
JWT に署名する際に、使用する `kid` を指定できます:
|
||||
|
||||
```swift
|
||||
let token = try await req.jwt.sign(payload, kid: "a")
|
||||
```
|
||||
|
||||
一方、検証時には、`kid` は JWT ヘッダーから自動的に抽出され、コレクション内のキーを検索するために使用されます。また、`kid` が見つからない場合にコレクション内のすべてのキーを反復処理するかどうかを指定できる `iteratingKeys` パラメータが verify メソッドにあります。
|
||||
|
||||
## クレーム {#claims}
|
||||
|
||||
Vapor の JWT パッケージには、一般的な [JWT クレーム](https://tools.ietf.org/html/rfc7519#section-4.1)を実装するためのいくつかのヘルパーが含まれています。
|
||||
|
||||
|クレーム|型|検証メソッド|
|
||||
|---|---|---|
|
||||
|`aud`|`AudienceClaim`|`verifyIntendedAudience(includes:)`|
|
||||
|`exp`|`ExpirationClaim`|`verifyNotExpired(currentDate:)`|
|
||||
|`jti`|`IDClaim`|n/a|
|
||||
|`iat`|`IssuedAtClaim`|n/a|
|
||||
|`iss`|`IssuerClaim`|n/a|
|
||||
|`locale`|`LocaleClaim`|n/a|
|
||||
|`nbf`|`NotBeforeClaim`|`verifyNotBefore(currentDate:)`|
|
||||
|`sub`|`SubjectClaim`|n/a|
|
||||
|
||||
すべてのクレームは `JWTPayload.verify` メソッドで検証する必要があります。クレームに特別な検証メソッドがある場合は、それを使用できます。それ以外の場合は、`value` を使用してクレームの値にアクセスし、それが有効であることを確認してください。
|
||||
|
||||
## JWK
|
||||
|
||||
JSON Web Key(JWK)は、暗号化キーを表す JSON データ構造です([RFC7517](https://datatracker.ietf.org/doc/html/rfc7517))。これらは一般的に、JWT を検証するためのキーをクライアントに提供するために使用されます。
|
||||
|
||||
例えば、Apple は Sign in with Apple JWKS を次の URL でホストしています。
|
||||
|
||||
```http
|
||||
GET https://appleid.apple.com/auth/keys
|
||||
```
|
||||
|
||||
Vapor は JWK をキーコレクションに追加するためのユーティリティを提供します:
|
||||
|
||||
```swift
|
||||
let privateKey = """
|
||||
{
|
||||
"kty": "RSA",
|
||||
"d": "\(rsaPrivateExponent)",
|
||||
"e": "AQAB",
|
||||
"use": "sig",
|
||||
"kid": "1234",
|
||||
"alg": "RS256",
|
||||
"n": "\(rsaModulus)"
|
||||
}
|
||||
"""
|
||||
|
||||
let jwk = try JWK(json: privateKey)
|
||||
try await app.jwt.keys.use(jwk: jwk)
|
||||
```
|
||||
|
||||
これにより、JWK がキーコレクションに追加され、他のキーと同様に JWT の署名と検証に使用できます。
|
||||
|
||||
### JWKs
|
||||
|
||||
複数の JWK がある場合は、同様に追加できます:
|
||||
|
||||
```swift
|
||||
let json = """
|
||||
{
|
||||
"keys": [
|
||||
{"kty": "RSA", "alg": "RS256", "kid": "a", "n": "\(rsaModulus)", "e": "AQAB"},
|
||||
{"kty": "RSA", "alg": "RS512", "kid": "b", "n": "\(rsaModulus)", "e": "AQAB"},
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
try await app.jwt.keys.use(jwksJSON: json)
|
||||
```
|
||||
|
||||
## ベンダー {#vendors}
|
||||
|
||||
Vapor は、以下の人気のある発行者からの JWT を処理するための API を提供します。
|
||||
|
||||
### Apple
|
||||
|
||||
まず、Apple アプリケーション識別子を設定します。
|
||||
|
||||
```swift
|
||||
// Configure Apple app identifier.
|
||||
app.jwt.apple.applicationIdentifier = "..."
|
||||
```
|
||||
|
||||
次に、`req.jwt.apple` ヘルパーを使用して Apple JWT を取得して検証します。
|
||||
|
||||
```swift
|
||||
// Fetch and verify Apple JWT from Authorization header.
|
||||
app.get("apple") { req async throws -> HTTPStatus in
|
||||
let token = try await req.jwt.apple.verify()
|
||||
print(token) // AppleIdentityToken
|
||||
return .ok
|
||||
}
|
||||
```
|
||||
|
||||
### Google
|
||||
|
||||
まず、Google アプリケーション識別子と G Suite ドメイン名を設定します。
|
||||
|
||||
```swift
|
||||
// Configure Google app identifier and domain name.
|
||||
app.jwt.google.applicationIdentifier = "..."
|
||||
app.jwt.google.gSuiteDomainName = "..."
|
||||
```
|
||||
|
||||
次に、`req.jwt.google` ヘルパーを使用して Google JWT を取得して検証します。
|
||||
|
||||
```swift
|
||||
// Fetch and verify Google JWT from Authorization header.
|
||||
app.get("google") { req async throws -> HTTPStatus in
|
||||
let token = try await req.jwt.google.verify()
|
||||
print(token) // GoogleIdentityToken
|
||||
return .ok
|
||||
}
|
||||
```
|
||||
|
||||
### Microsoft
|
||||
|
||||
まず、Microsoft アプリケーション識別子を設定します。
|
||||
|
||||
```swift
|
||||
// Configure Microsoft app identifier.
|
||||
app.jwt.microsoft.applicationIdentifier = "..."
|
||||
```
|
||||
|
||||
次に、`req.jwt.microsoft` ヘルパーを使用して Microsoft JWT を取得して検証します。
|
||||
|
||||
```swift
|
||||
// Fetch and verify Microsoft JWT from Authorization header.
|
||||
app.get("microsoft") { req async throws -> HTTPStatus in
|
||||
let token = try await req.jwt.microsoft.verify()
|
||||
print(token) // MicrosoftIdentityToken
|
||||
return .ok
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
# パスワード {#passwords}
|
||||
|
||||
Vaporには、パスワードを安全に保存・検証するためのパスワードハッシュAPIが含まれています。このAPIは環境に基づいて設定可能で、非同期ハッシュ化をサポートしています。
|
||||
|
||||
## 設定 {#configuration}
|
||||
|
||||
アプリケーションのパスワードハッシャーを設定するには、`app.passwords`を使用します。
|
||||
|
||||
```swift
|
||||
import Vapor
|
||||
|
||||
app.passwords.use(...)
|
||||
```
|
||||
|
||||
### Bcrypt
|
||||
|
||||
パスワードハッシュ化にVaporの[Bcrypt API](crypto.md#bcrypt)を使用するには、`.bcrypt`を指定します。これがデフォルトです。
|
||||
|
||||
```swift
|
||||
app.passwords.use(.bcrypt)
|
||||
```
|
||||
|
||||
Bcryptは、特に指定しない限りコスト12を使用します。`cost`パラメータを渡すことで、これを設定できます。
|
||||
|
||||
```swift
|
||||
app.passwords.use(.bcrypt(cost: 8))
|
||||
```
|
||||
|
||||
### Plaintext
|
||||
|
||||
Vaporには、パスワードを平文として保存・検証する安全でないパスワードハッシャーが含まれています。これは本番環境では使用すべきではありませんが、テストには便利です。
|
||||
|
||||
```swift
|
||||
switch app.environment {
|
||||
case .testing:
|
||||
app.passwords.use(.plaintext)
|
||||
default: break
|
||||
}
|
||||
```
|
||||
|
||||
## ハッシュ化 {#hashing}
|
||||
|
||||
パスワードをハッシュ化するには、`Request`で利用可能な`password`ヘルパーを使用します。
|
||||
|
||||
```swift
|
||||
let digest = try req.password.hash("vapor")
|
||||
```
|
||||
|
||||
パスワードダイジェストは、`verify`メソッドを使用して平文パスワードと照合できます。
|
||||
|
||||
```swift
|
||||
let bool = try req.password.verify("vapor", created: digest)
|
||||
```
|
||||
|
||||
同じAPIは、起動時に使用するために`Application`でも利用可能です。
|
||||
|
||||
```swift
|
||||
let digest = try app.password.hash("vapor")
|
||||
```
|
||||
|
||||
### Async
|
||||
|
||||
パスワードハッシュアルゴリズムは、遅くCPU集約的になるように設計されています。このため、パスワードをハッシュ化する際にイベントループをブロックしないようにしたい場合があります。Vaporは、ハッシュ化をバックグラウンドスレッドプールにディスパッチする非同期パスワードハッシュAPIを提供します。非同期APIを使用するには、パスワードハッシャーの`async`プロパティを使用します。
|
||||
|
||||
```swift
|
||||
req.password.async.hash("vapor").map { digest in
|
||||
// ダイジェストを処理
|
||||
}
|
||||
|
||||
// または
|
||||
|
||||
let digest = try await req.password.async.hash("vapor")
|
||||
```
|
||||
|
||||
ダイジェストの検証も同様に機能します:
|
||||
|
||||
```swift
|
||||
req.password.async.verify("vapor", created: digest).map { bool in
|
||||
// 結果を処理
|
||||
}
|
||||
|
||||
// または
|
||||
|
||||
let result = try await req.password.async.verify("vapor", created: digest)
|
||||
```
|
||||
|
||||
バックグラウンドスレッドでハッシュを計算することで、アプリケーションのイベントループを解放し、より多くの受信リクエストを処理できるようになります。
|
||||
|
|
@ -0,0 +1,804 @@
|
|||
# 4.0へのアップグレード {#upgrading-to-4.0}
|
||||
|
||||
このガイドでは、既存のVapor 3.xプロジェクトを4.xにアップグレードする方法を説明します。このガイドでは、Vaporの公式パッケージに加え、よく使用されるプロバイダーについても網羅します。不足している内容があれば、[Vaporのチームチャット](https://discord.gg/vapor)で質問するのがおすすめです。IssuesやPull Requestも歓迎です。
|
||||
|
||||
## 依存関係 {#dependencies}
|
||||
|
||||
Vapor 4を使用するには、Xcode 11.4およびmacOS 10.15以上が必要です。
|
||||
|
||||
ドキュメントのインストールセクションで依存関係のインストールについて説明しています。
|
||||
|
||||
## Package.swift
|
||||
|
||||
Vapor 4へのアップグレードの最初のステップは、パッケージの依存関係を更新することです。以下は更新されたPackage.swiftファイルの例です。更新された[テンプレートPackage.swift](https://github.com/vapor/template/blob/main/Package.swift)も確認できます。
|
||||
|
||||
```diff
|
||||
-// swift-tools-version:4.0
|
||||
+// swift-tools-version:5.2
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "api",
|
||||
+ platforms: [
|
||||
+ .macOS(.v10_15),
|
||||
+ ],
|
||||
dependencies: [
|
||||
- .package(url: "https://github.com/vapor/fluent-postgresql.git", from: "1.0.0"),
|
||||
+ .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
|
||||
+ .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),
|
||||
- .package(url: "https://github.com/vapor/jwt.git", from: "3.0.0"),
|
||||
+ .package(url: "https://github.com/vapor/jwt.git", from: "4.0.0"),
|
||||
- .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
|
||||
+ .package(url: "https://github.com/vapor/vapor.git", from: "4.3.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "App", dependencies: [
|
||||
- "FluentPostgreSQL",
|
||||
+ .product(name: "Fluent", package: "fluent"),
|
||||
+ .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
|
||||
- "Vapor",
|
||||
+ .product(name: "Vapor", package: "vapor"),
|
||||
- "JWT",
|
||||
+ .product(name: "JWT", package: "jwt"),
|
||||
]),
|
||||
- .target(name: "Run", dependencies: ["App"]),
|
||||
- .testTarget(name: "AppTests", dependencies: ["App"])
|
||||
+ .target(name: "Run", dependencies: [
|
||||
+ .target(name: "App"),
|
||||
+ ]),
|
||||
+ .testTarget(name: "AppTests", dependencies: [
|
||||
+ .target(name: "App"),
|
||||
+ ])
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
Vapor 4向けにアップグレードされたすべてのパッケージは、メジャーバージョン番号が1つ増加します。
|
||||
|
||||
!!! warning
|
||||
Vapor 4の一部のパッケージはまだ正式にリリースされていないため、`-rc`プレリリース識別子が使用されています。
|
||||
|
||||
### 廃止されたパッケージ {#old-packages}
|
||||
|
||||
いくつかのVapor 3パッケージは非推奨となりました:
|
||||
|
||||
- `vapor/auth`: Vaporに含まれるようになりました。
|
||||
- `vapor/core`: いくつかのモジュールに吸収されました。
|
||||
- `vapor/crypto`: SwiftCryptoに置き換えられました(Vaporに含まれています)。
|
||||
- `vapor/multipart`: Vaporに含まれるようになりました。
|
||||
- `vapor/url-encoded-form`: Vaporに含まれるようになりました。
|
||||
- `vapor-community/vapor-ext`: Vaporに含まれるようになりました。
|
||||
- `vapor-community/pagination`: Fluentの一部になりました。
|
||||
- `IBM-Swift/LoggerAPI`: SwiftLogに置き換えられました。
|
||||
|
||||
### Fluent依存関係 {#fluent-dependency}
|
||||
|
||||
`vapor/fluent`は、依存関係リストとターゲットに個別の依存関係として追加する必要があります。すべてのデータベース固有のパッケージには、`vapor/fluent`への依存関係を明確にするために`-driver`が付けられています。
|
||||
|
||||
```diff
|
||||
- .package(url: "https://github.com/vapor/fluent-postgresql.git", from: "1.0.0"),
|
||||
+ .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
|
||||
+ .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),
|
||||
```
|
||||
|
||||
### プラットフォーム {#platforms}
|
||||
|
||||
Vaporのパッケージマニフェストは、macOS 10.15以上を明示的にサポートするようになりました。これにより、あなたのパッケージもプラットフォームサポートを指定する必要があります。
|
||||
|
||||
```diff
|
||||
+ platforms: [
|
||||
+ .macOS(.v10_15),
|
||||
+ ],
|
||||
```
|
||||
|
||||
将来的にVaporは追加のサポートプラットフォームを追加する可能性があります。あなたのパッケージは、バージョン番号がVaporの最小バージョン要件以上である限り、これらのプラットフォームの任意のサブセットをサポートできます。
|
||||
|
||||
### Xcode
|
||||
|
||||
Vapor 4はXcode 11のネイティブSPMサポートを利用しています。これにより、`.xcodeproj`ファイルを生成する必要がなくなりました。Xcodeでプロジェクトのフォルダーを開くと、自動的にSPMが認識され、依存関係が取得されます。
|
||||
|
||||
`vapor xcode`または`open Package.swift`を使用して、Xcodeでプロジェクトをネイティブに開くことができます。
|
||||
|
||||
Package.swiftを更新したら、Xcodeを閉じてルートディレクトリから以下のフォルダーを削除する必要があるかもしれません:
|
||||
|
||||
- `Package.resolved`
|
||||
- `.build`
|
||||
- `.swiftpm`
|
||||
- `*.xcodeproj`
|
||||
|
||||
更新されたパッケージが正常に解決されると、コンパイラエラーが表示されるはずです--おそらくかなりの数です。心配しないでください!修正方法をお見せします。
|
||||
|
||||
## Run
|
||||
|
||||
最初に行うべきことは、Runモジュールの`main.swift`ファイルを新しい形式に更新することです。
|
||||
|
||||
```swift
|
||||
import App
|
||||
import Vapor
|
||||
|
||||
var env = try Environment.detect()
|
||||
try LoggingSystem.bootstrap(from: &env)
|
||||
let app = Application(env)
|
||||
defer { app.shutdown() }
|
||||
try configure(app)
|
||||
try app.run()
|
||||
```
|
||||
|
||||
`main.swift`ファイルの内容はAppモジュールの`app.swift`を置き換えるため、そのファイルは削除できます。
|
||||
|
||||
## App
|
||||
|
||||
基本的なAppモジュール構造の更新方法を見てみましょう。
|
||||
|
||||
### configure.swift
|
||||
|
||||
`configure`メソッドは`Application`のインスタンスを受け入れるように変更する必要があります。
|
||||
|
||||
```diff
|
||||
- public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws
|
||||
+ public func configure(_ app: Application) throws
|
||||
```
|
||||
|
||||
以下は更新されたconfigureメソッドの例です。
|
||||
|
||||
```swift
|
||||
import Fluent
|
||||
import FluentSQLiteDriver
|
||||
import Vapor
|
||||
|
||||
// アプリケーションが初期化される前に呼び出されます。
|
||||
public func configure(_ app: Application) throws {
|
||||
// `Public/`ディレクトリからファイルを提供
|
||||
// app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
|
||||
// SQLiteデータベースを設定
|
||||
app.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite)
|
||||
|
||||
// マイグレーションを設定
|
||||
app.migrations.add(CreateTodo())
|
||||
|
||||
try routes(app)
|
||||
}
|
||||
```
|
||||
|
||||
ルーティング、ミドルウェア、Fluentなどの設定に関する構文の変更は以下で説明します。
|
||||
|
||||
### boot.swift
|
||||
|
||||
`boot`の内容は、アプリケーションインスタンスを受け入れるようになったため、`configure`メソッドに配置できます。
|
||||
|
||||
### routes.swift
|
||||
|
||||
`routes`メソッドは`Application`のインスタンスを受け入れるように変更する必要があります。
|
||||
|
||||
```diff
|
||||
- public func routes(_ router: Router, _ container: Container) throws
|
||||
+ public func routes(_ app: Application) throws
|
||||
```
|
||||
|
||||
ルーティング構文の変更に関する詳細は以下で説明します。
|
||||
|
||||
## サービス {#services}
|
||||
|
||||
Vapor 4のサービスAPIは、サービスの発見と使用を容易にするために簡素化されました。サービスは`Application`と`Request`のメソッドとプロパティとして公開されるようになり、コンパイラがそれらの使用を支援できます。
|
||||
|
||||
これをよりよく理解するために、いくつかの例を見てみましょう。
|
||||
|
||||
```diff
|
||||
// サーバーのデフォルトポートを8281に変更
|
||||
- services.register { container -> NIOServerConfig in
|
||||
- return .default(port: 8281)
|
||||
- }
|
||||
+ app.http.server.configuration.port = 8281
|
||||
```
|
||||
|
||||
`NIOServerConfig`をサービスに登録する代わりに、サーバー設定はApplicationの単純なプロパティとして公開され、オーバーライドできます。
|
||||
|
||||
```diff
|
||||
// CORSミドルウェアを登録
|
||||
let corsConfiguration = CORSMiddleware.Configuration(
|
||||
allowedOrigin: .all,
|
||||
allowedMethods: [.POST, .GET, .PATCH, .PUT, .DELETE, .OPTIONS]
|
||||
)
|
||||
let corsMiddleware = CORSMiddleware(configuration: corsConfiguration)
|
||||
- var middlewares = MiddlewareConfig() // _空の_ミドルウェア設定を作成
|
||||
- middlewares.use(corsMiddleware)
|
||||
- services.register(middlewares)
|
||||
+ app.middleware.use(corsMiddleware)
|
||||
```
|
||||
|
||||
`MiddlewareConfig`を作成してサービスに登録する代わりに、ミドルウェアはApplicationのプロパティとして公開され、追加できます。
|
||||
|
||||
```diff
|
||||
// ルートハンドラー内でリクエストを行う。
|
||||
- try req.make(Client.self).get("https://vapor.codes")
|
||||
+ req.client.get("https://vapor.codes")
|
||||
```
|
||||
|
||||
Applicationと同様に、Requestもサービスを単純なプロパティとメソッドとして公開します。ルートクロージャ内では、常にRequest固有のサービスを使用する必要があります。
|
||||
|
||||
この新しいサービスパターンは、Vapor 3の`Container`、`Service`、および`Config`タイプを置き換えます。
|
||||
|
||||
### プロバイダー {#providers}
|
||||
|
||||
サードパーティパッケージを設定するためにプロバイダーは必要なくなりました。各パッケージは代わりにApplicationとRequestを新しいプロパティとメソッドで拡張して設定します。
|
||||
|
||||
Vapor 4でLeafがどのように設定されるか見てみましょう。
|
||||
|
||||
```diff
|
||||
// ビューレンダリングにLeafを使用。
|
||||
- try services.register(LeafProvider())
|
||||
- config.prefer(LeafRenderer.self, for: ViewRenderer.self)
|
||||
+ app.views.use(.leaf)
|
||||
```
|
||||
|
||||
Leafを設定するには、`app.leaf`プロパティを使用します。
|
||||
|
||||
```diff
|
||||
// Leafビューキャッシュを無効化。
|
||||
- services.register { container -> LeafConfig in
|
||||
- return LeafConfig(tags: ..., viewsDir: ..., shouldCache: false)
|
||||
- }
|
||||
+ app.leaf.cache.isEnabled = false
|
||||
```
|
||||
|
||||
### 環境 {#environment}
|
||||
|
||||
現在の環境(production、developmentなど)は`app.environment`でアクセスできます。
|
||||
|
||||
### カスタムサービス {#custom-services}
|
||||
|
||||
Vapor 3で`Service`プロトコルに準拠し、コンテナに登録されていたカスタムサービスは、ApplicationまたはRequestの拡張として表現できるようになりました。
|
||||
|
||||
```diff
|
||||
struct MyAPI {
|
||||
let client: Client
|
||||
func foo() { ... }
|
||||
}
|
||||
- extension MyAPI: Service { }
|
||||
- services.register { container -> MyAPI in
|
||||
- return try MyAPI(client: container.make())
|
||||
- }
|
||||
+ extension Request {
|
||||
+ var myAPI: MyAPI {
|
||||
+ .init(client: self.client)
|
||||
+ }
|
||||
+ }
|
||||
```
|
||||
|
||||
このサービスは`make`の代わりに拡張を使用してアクセスできます。
|
||||
|
||||
```diff
|
||||
- try req.make(MyAPI.self).foo()
|
||||
+ req.myAPI.foo()
|
||||
```
|
||||
|
||||
### カスタムプロバイダー {#custom-providers}
|
||||
|
||||
ほとんどのカスタムサービスは、前のセクションで示したように拡張を使用して実装できます。ただし、一部の高度なプロバイダーは、アプリケーションのライフサイクルにフックしたり、保存されたプロパティを使用したりする必要があるかもしれません。
|
||||
|
||||
Applicationの新しい`Lifecycle`ヘルパーを使用してライフサイクルハンドラーを登録できます。
|
||||
|
||||
```swift
|
||||
struct PrintHello: LifecycleHandler {
|
||||
func willBoot(_ app: Application) throws {
|
||||
print("Hello!")
|
||||
}
|
||||
}
|
||||
|
||||
app.lifecycle.use(PrintHello())
|
||||
```
|
||||
|
||||
Applicationに値を保存するには、新しい`Storage`ヘルパーを使用できます。
|
||||
|
||||
```swift
|
||||
struct MyNumber: StorageKey {
|
||||
typealias Value = Int
|
||||
}
|
||||
app.storage[MyNumber.self] = 5
|
||||
print(app.storage[MyNumber.self]) // 5
|
||||
```
|
||||
|
||||
`app.storage`へのアクセスは、簡潔なAPIを作成するために設定可能な計算プロパティでラップできます。
|
||||
|
||||
```swift
|
||||
extension Application {
|
||||
var myNumber: Int? {
|
||||
get { self.storage[MyNumber.self] }
|
||||
set { self.storage[MyNumber.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
app.myNumber = 42
|
||||
print(app.myNumber) // 42
|
||||
```
|
||||
|
||||
## NIO
|
||||
|
||||
Vapor 4はSwiftNIOの非同期APIを直接公開するようになり、`map`や`flatMap`のようなメソッドをオーバーロードしたり、`EventLoopFuture`のようなタイプをエイリアスしたりしようとしなくなりました。Vapor 3は、SwiftNIOが存在する前にリリースされた初期ベータバージョンとの下位互換性のためにオーバーロードとエイリアスを提供していました。これらは、他のSwiftNIO互換パッケージとの混乱を減らし、SwiftNIOのベストプラクティスの推奨事項により良く従うために削除されました。
|
||||
|
||||
### 非同期の名前変更 {#async-naming-changes}
|
||||
|
||||
最も明白な変更は、`EventLoopFuture`の`Future`タイプエイリアスが削除されたことです。これは検索と置換で簡単に修正できます。
|
||||
|
||||
さらに、NIOはVapor 3が追加した`to:`ラベルをサポートしていません。Swift 5.2の改善された型推論により、`to:`はそれほど必要ではなくなりました。
|
||||
|
||||
```diff
|
||||
- futureA.map(to: String.self) { ... }
|
||||
+ futureA.map { ... }
|
||||
```
|
||||
|
||||
`newPromise`のように`new`で始まるメソッドは、Swiftスタイルに合わせて`make`に変更されました。
|
||||
|
||||
```diff
|
||||
- let promise = eventLoop.newPromise(String.self)
|
||||
+ let promise = eventLoop.makePromise(of: String.self)
|
||||
```
|
||||
|
||||
`catchMap`は利用できなくなりましたが、NIOの`mapError`や`flatMapErrorThrowing`のようなメソッドが代わりに機能します。
|
||||
|
||||
複数のフューチャーを組み合わせるためのVapor 3のグローバル`flatMap`メソッドは利用できなくなりました。これは、NIOの`and`メソッドを使用して多くのフューチャーを組み合わせることで置き換えることができます。
|
||||
|
||||
```diff
|
||||
- flatMap(futureA, futureB) { a, b in
|
||||
+ futureA.and(futureB).flatMap { (a, b) in
|
||||
// aとbで何かを行う。
|
||||
}
|
||||
```
|
||||
|
||||
### ByteBuffer
|
||||
|
||||
以前は`Data`を使用していた多くのメソッドとプロパティは、NIOの`ByteBuffer`を使用するようになりました。このタイプは、より強力で高性能なバイトストレージタイプです。APIの詳細については、[SwiftNIOのByteBufferドキュメント](https://swiftpackageindex.com/apple/swift-nio/main/documentation/niocore/bytebuffer)を参照してください。
|
||||
|
||||
`ByteBuffer`を`Data`に戻すには:
|
||||
|
||||
```swift
|
||||
Data(buffer.readableBytesView)
|
||||
```
|
||||
|
||||
### map / flatMapのスロー {#throwing-map-flatmap}
|
||||
|
||||
最も難しい変更は、`map`と`flatMap`がもはやスローできないことです。`map`には(やや紛らわしいことに)`flatMapThrowing`という名前のスローバージョンがあります。しかし、`flatMap`にはスローする対応物がありません。これにより、いくつかの非同期コードの再構築が必要になる場合があります。
|
||||
|
||||
スローしないmapは引き続き正常に動作するはずです。
|
||||
|
||||
```swift
|
||||
// スローしないmap。
|
||||
futureA.map { a in
|
||||
return b
|
||||
}
|
||||
```
|
||||
|
||||
スローするmapは`flatMapThrowing`に名前を変更する必要があります。
|
||||
|
||||
```diff
|
||||
- futureA.map { a in
|
||||
+ futureA.flatMapThrowing { a in
|
||||
if ... {
|
||||
throw SomeError()
|
||||
} else {
|
||||
return b
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
スローしないflat-mapは引き続き正常に動作するはずです。
|
||||
|
||||
```swift
|
||||
// スローしないflatMap。
|
||||
futureA.flatMap { a in
|
||||
return futureB
|
||||
}
|
||||
```
|
||||
|
||||
flat-map内でエラーをスローする代わりに、フューチャーエラーを返します。エラーが他のスローメソッドから発生する場合、エラーはdo / catchでキャッチしてフューチャーとして返すことができます。
|
||||
|
||||
```swift
|
||||
// キャッチしたエラーをフューチャーとして返す。
|
||||
futureA.flatMap { a in
|
||||
do {
|
||||
try doSomething()
|
||||
return futureB
|
||||
} catch {
|
||||
return eventLoop.makeFailedFuture(error)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
スローメソッド呼び出しは、`flatMapThrowing`にリファクタリングし、タプルを使用してチェーンすることもできます。
|
||||
|
||||
```swift
|
||||
// タプルチェーンを使用してflatMapThrowingにリファクタリングされたスローメソッド。
|
||||
futureA.flatMapThrowing { a in
|
||||
try (a, doSomeThing())
|
||||
}.flatMap { (a, result) in
|
||||
// resultはdoSomethingの値です。
|
||||
return futureB
|
||||
}
|
||||
```
|
||||
|
||||
## ルーティング {#routing}
|
||||
|
||||
ルートはApplicationに直接登録されるようになりました。
|
||||
|
||||
```swift
|
||||
app.get("hello") { req in
|
||||
return "Hello, world"
|
||||
}
|
||||
```
|
||||
|
||||
これは、ルーターをサービスに登録する必要がなくなったことを意味します。`routes`メソッドにアプリケーションを渡してルートを追加し始めるだけです。`RoutesBuilder`で利用可能なすべてのメソッドは`Application`で利用可能です。
|
||||
|
||||
### 同期コンテンツ {#synchronous-content}
|
||||
|
||||
リクエストコンテンツのデコードは同期的になりました。
|
||||
|
||||
```swift
|
||||
let payload = try req.content.decode(MyPayload.self)
|
||||
print(payload) // MyPayload
|
||||
```
|
||||
|
||||
この動作は、`.stream`ボディコレクション戦略を使用してルートを登録することでオーバーライドできます。
|
||||
|
||||
```swift
|
||||
app.on(.POST, "streaming", body: .stream) { req in
|
||||
// リクエストボディは非同期になりました。
|
||||
req.body.collect().map { buffer in
|
||||
HTTPStatus.ok
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### カンマ区切りのパス {#comma-separated-paths}
|
||||
|
||||
一貫性のため、パスはカンマ区切りである必要があり、`/`を含んではいけません。
|
||||
|
||||
```diff
|
||||
- router.get("v1/users/", "posts", "/comments") { req in
|
||||
+ app.get("v1", "users", "posts", "comments") { req in
|
||||
// リクエストを処理。
|
||||
}
|
||||
```
|
||||
|
||||
### ルートパラメータ {#route-parameters}
|
||||
|
||||
`Parameter`プロトコルは、明示的に名前付きパラメータを支持して削除されました。これにより、重複するパラメータの問題と、ミドルウェアとルートハンドラーでのパラメータの順不同の取得が防止されます。
|
||||
|
||||
```diff
|
||||
- router.get("planets", String.parameter) { req in
|
||||
- let id = req.parameters.next(String.self)
|
||||
+ app.get("planets", ":id") { req in
|
||||
+ let id = req.parameters.get("id")
|
||||
return "Planet id: \(id)"
|
||||
}
|
||||
```
|
||||
|
||||
モデルを使用したルートパラメータの使用については、Fluentセクションで説明します。
|
||||
|
||||
## ミドルウェア {#middleware}
|
||||
|
||||
`MiddlewareConfig`は`MiddlewareConfiguration`に名前が変更され、Applicationのプロパティになりました。`app.middleware`を使用してアプリにミドルウェアを追加できます。
|
||||
|
||||
```diff
|
||||
let corsMiddleware = CORSMiddleware(configuration: ...)
|
||||
- var middleware = MiddlewareConfig()
|
||||
- middleware.use(corsMiddleware)
|
||||
+ app.middleware.use(corsMiddleware)
|
||||
- services.register(middlewares)
|
||||
```
|
||||
|
||||
ミドルウェアはタイプ名で登録できなくなりました。登録する前にミドルウェアを初期化してください。
|
||||
|
||||
```diff
|
||||
- middleware.use(ErrorMiddleware.self)
|
||||
+ app.middleware.use(ErrorMiddleware.default(environment: app.environment))
|
||||
```
|
||||
|
||||
すべてのデフォルトミドルウェアを削除するには、`app.middleware`を空の設定に設定します:
|
||||
|
||||
```swift
|
||||
app.middleware = .init()
|
||||
```
|
||||
|
||||
## Fluent
|
||||
|
||||
FluentのAPIはデータベースに依存しなくなりました。`Fluent`だけをインポートできます。
|
||||
|
||||
```diff
|
||||
- import FluentMySQL
|
||||
+ import Fluent
|
||||
```
|
||||
|
||||
### モデル {#models}
|
||||
|
||||
すべてのモデルは`Model`プロトコルを使用し、クラスである必要があります。
|
||||
|
||||
```diff
|
||||
- struct Planet: MySQLModel {
|
||||
+ final class Planet: Model {
|
||||
```
|
||||
|
||||
すべてのフィールドは`@Field`または`@OptionalField`プロパティラッパーを使用して宣言されます。
|
||||
|
||||
```diff
|
||||
+ @Field(key: "name")
|
||||
var name: String
|
||||
|
||||
+ @OptionalField(key: "age")
|
||||
var age: Int?
|
||||
```
|
||||
|
||||
モデルのIDは`@ID`プロパティラッパーを使用して定義する必要があります。
|
||||
|
||||
```diff
|
||||
+ @ID(key: .id)
|
||||
var id: UUID?
|
||||
```
|
||||
|
||||
カスタムキーまたはタイプの識別子を使用するモデルは`@ID(custom:)`を使用する必要があります。
|
||||
|
||||
すべてのモデルは、テーブルまたはコレクション名を静的に定義する必要があります。
|
||||
|
||||
```diff
|
||||
final class Planet: Model {
|
||||
+ static let schema = "Planet"
|
||||
}
|
||||
```
|
||||
|
||||
すべてのモデルには空のイニシャライザが必要です。すべてのプロパティがプロパティラッパーを使用するため、これは空にできます。
|
||||
|
||||
```diff
|
||||
final class Planet: Model {
|
||||
+ init() { }
|
||||
}
|
||||
```
|
||||
|
||||
モデルの`save`、`update`、`create`は、モデルインスタンスを返さなくなりました。
|
||||
|
||||
```diff
|
||||
- model.save(on: ...)
|
||||
+ model.save(on: ...).map { model }
|
||||
```
|
||||
|
||||
モデルはルートパスコンポーネントとして使用できなくなりました。代わりに`find`と`req.parameters.get`を使用してください。
|
||||
|
||||
```diff
|
||||
- try req.parameters.next(ServerSize.self)
|
||||
+ ServerSize.find(req.parameters.get("size"), on: req.db)
|
||||
+ .unwrap(or: Abort(.notFound))
|
||||
```
|
||||
|
||||
`Model.ID`は`Model.IDValue`に名前が変更されました。
|
||||
|
||||
モデルのタイムスタンプは`@Timestamp`プロパティラッパーを使用して宣言されるようになりました。
|
||||
|
||||
```diff
|
||||
- static var createdAtKey: TimestampKey? = \.createdAt
|
||||
+ @Timestamp(key: "createdAt", on: .create)
|
||||
var createdAt: Date?
|
||||
```
|
||||
|
||||
### リレーション {#relations}
|
||||
|
||||
リレーションはプロパティラッパーを使用して定義されるようになりました。
|
||||
|
||||
親リレーションは`@Parent`プロパティラッパーを使用し、フィールドプロパティを内部に含みます。`@Parent`に渡されるキーは、データベース内の識別子を格納するフィールドの名前である必要があります。
|
||||
|
||||
```diff
|
||||
- var serverID: Int
|
||||
- var server: Parent<App, Server> {
|
||||
- parent(\.serverID)
|
||||
- }
|
||||
+ @Parent(key: "serverID")
|
||||
+ var server: Server
|
||||
```
|
||||
|
||||
子リレーションは、関連する`@Parent`へのキーパスを持つ`@Children`プロパティラッパーを使用します。
|
||||
|
||||
```diff
|
||||
- var apps: Children<Server, App> {
|
||||
- children(\.serverID)
|
||||
- }
|
||||
+ @Children(for: \.$server)
|
||||
+ var apps: [App]
|
||||
```
|
||||
|
||||
兄弟リレーションは、ピボットモデルへのキーパスを持つ`@Siblings`プロパティラッパーを使用します。
|
||||
|
||||
```diff
|
||||
- var users: Siblings<Company, User, Permission> {
|
||||
- siblings()
|
||||
- }
|
||||
+ @Siblings(through: Permission.self, from: \.$user, to: \.$company)
|
||||
+ var companies: [Company]
|
||||
```
|
||||
|
||||
ピボットは、2つの`@Parent`リレーションと0個以上の追加フィールドを持つ`Model`に準拠する通常のモデルになりました。
|
||||
|
||||
### クエリ {#query}
|
||||
|
||||
データベースコンテキストは、ルートハンドラー内で`req.db`を介してアクセスされるようになりました。
|
||||
|
||||
```diff
|
||||
- Planet.query(on: req)
|
||||
+ Planet.query(on: req.db)
|
||||
```
|
||||
|
||||
`DatabaseConnectable`は`Database`に名前が変更されました。
|
||||
|
||||
フィールドへのキーパスは、フィールド値の代わりにプロパティラッパーを指定するために`$`で始まるようになりました。
|
||||
|
||||
```diff
|
||||
- filter(\.foo == ...)
|
||||
+ filter(\.$foo == ...)
|
||||
```
|
||||
|
||||
### マイグレーション {#migrations}
|
||||
|
||||
モデルはリフレクションベースの自動マイグレーションをサポートしなくなりました。すべてのマイグレーションは手動で記述する必要があります。
|
||||
|
||||
```diff
|
||||
- extension Planet: Migration { }
|
||||
+ struct CreatePlanet: Migration {
|
||||
+ ...
|
||||
+}
|
||||
```
|
||||
|
||||
マイグレーションは文字列型になり、モデルから切り離されて`Migration`プロトコルを使用するようになりました。
|
||||
|
||||
```diff
|
||||
- struct CreateGalaxy: <#Database#>Migration {
|
||||
+ struct CreateGalaxy: Migration {
|
||||
```
|
||||
|
||||
`prepare`および`revert`メソッドは静的ではなくなりました。
|
||||
|
||||
```diff
|
||||
- static func prepare(on conn: <#Database#>Connection) -> Future<Void> {
|
||||
+ func prepare(on database: Database) -> EventLoopFuture<Void>
|
||||
```
|
||||
|
||||
スキーマビルダーの作成は、`Database`のインスタンスメソッドを介して行われます。
|
||||
|
||||
```diff
|
||||
- <#Database#>Database.create(Galaxy.self, on: conn) { builder in
|
||||
- // ビルダーを使用。
|
||||
- }
|
||||
+ var builder = database.schema("Galaxy")
|
||||
+ // ビルダーを使用。
|
||||
```
|
||||
|
||||
`create`、`update`、および`delete`メソッドは、クエリビルダーと同様にスキーマビルダーで呼び出されるようになりました。
|
||||
|
||||
フィールド定義は文字列型になり、次のパターンに従います:
|
||||
|
||||
```swift
|
||||
field(<name>, <type>, <constraints>)
|
||||
```
|
||||
|
||||
以下の例を参照してください。
|
||||
|
||||
```diff
|
||||
- builder.field(for: \.name)
|
||||
+ builder.field("name", .string, .required)
|
||||
```
|
||||
|
||||
スキーマビルドはクエリビルダーのようにチェーンできるようになりました。
|
||||
|
||||
```swift
|
||||
database.schema("Galaxy")
|
||||
.id()
|
||||
.field("name", .string, .required)
|
||||
.create()
|
||||
```
|
||||
|
||||
### Fluent設定 {#fluent-configuration}
|
||||
|
||||
`DatabasesConfig`は`app.databases`に置き換えられました。
|
||||
|
||||
```swift
|
||||
try app.databases.use(.postgres(url: "postgres://..."), as: .psql)
|
||||
```
|
||||
|
||||
`MigrationsConfig`は`app.migrations`に置き換えられました。
|
||||
|
||||
```swift
|
||||
app.migrations.use(CreatePlanet(), on: .psql)
|
||||
```
|
||||
|
||||
### リポジトリ {#repositories}
|
||||
|
||||
Vapor 4でのサービスの動作方法が変更されたため、データベースリポジトリの実装方法も変更されました。`UserRepository`のようなプロトコルは引き続き必要ですが、そのプロトコルに準拠する`final class`を作成する代わりに、`struct`を作成する必要があります。
|
||||
|
||||
```diff
|
||||
- final class DatabaseUserRepository: UserRepository {
|
||||
+ struct DatabaseUserRepository: UserRepository {
|
||||
let database: Database
|
||||
func all() -> EventLoopFuture<[User]> {
|
||||
return User.query(on: database).all()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
また、Vapor 4にはもはや存在しないため、`ServiceType`への準拠も削除する必要があります。
|
||||
```diff
|
||||
- extension DatabaseUserRepository {
|
||||
- static let serviceSupports: [Any.Type] = [Athlete.self]
|
||||
- static func makeService(for worker: Container) throws -> Self {
|
||||
- return .init()
|
||||
- }
|
||||
- }
|
||||
```
|
||||
|
||||
代わりに`UserRepositoryFactory`を作成する必要があります:
|
||||
```swift
|
||||
struct UserRepositoryFactory {
|
||||
var make: ((Request) -> UserRepository)?
|
||||
mutating func use(_ make: @escaping ((Request) -> UserRepository)) {
|
||||
self.make = make
|
||||
}
|
||||
}
|
||||
```
|
||||
このファクトリーは`Request`に対して`UserRepository`を返す責任があります。
|
||||
|
||||
次のステップは、ファクトリーを指定するために`Application`に拡張を追加することです:
|
||||
```swift
|
||||
extension Application {
|
||||
private struct UserRepositoryKey: StorageKey {
|
||||
typealias Value = UserRepositoryFactory
|
||||
}
|
||||
|
||||
var users: UserRepositoryFactory {
|
||||
get {
|
||||
self.storage[UserRepositoryKey.self] ?? .init()
|
||||
}
|
||||
set {
|
||||
self.storage[UserRepositoryKey.self] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`Request`内で実際のリポジトリを使用するには、`Request`にこの拡張を追加します:
|
||||
```swift
|
||||
extension Request {
|
||||
var users: UserRepository {
|
||||
self.application.users.make!(self)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
最後のステップは、`configure.swift`内でファクトリーを指定することです
|
||||
```swift
|
||||
app.users.use { req in
|
||||
DatabaseUserRepository(database: req.db)
|
||||
}
|
||||
```
|
||||
|
||||
これで、ルートハンドラー内で`req.users.all()`を使用してリポジトリにアクセスでき、テスト内でファクトリーを簡単に置き換えることができます。
|
||||
テスト内でモックされたリポジトリを使用したい場合は、まず`TestUserRepository`を作成します
|
||||
```swift
|
||||
final class TestUserRepository: UserRepository {
|
||||
var users: [User]
|
||||
let eventLoop: EventLoop
|
||||
|
||||
init(users: [User] = [], eventLoop: EventLoop) {
|
||||
self.users = users
|
||||
self.eventLoop = eventLoop
|
||||
}
|
||||
|
||||
func all() -> EventLoopFuture<[User]> {
|
||||
eventLoop.makeSuccededFuture(self.users)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
このモックされたリポジトリをテスト内で次のように使用できます:
|
||||
```swift
|
||||
final class MyTests: XCTestCase {
|
||||
func test() throws {
|
||||
let users: [User] = []
|
||||
app.users.use { TestUserRepository(users: users, eventLoop: $0.eventLoop) }
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# リダイレクト中...
|
||||
|
||||
<meta http-equiv="refresh" content="0; url=https://legacy.docs.vapor.codes/">
|
||||
Loading…
Reference in New Issue