Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get HummingbirdFluent working with HB 2.0 #11

Merged
merged 14 commits into from
Jan 5, 2024
2 changes: 1 addition & 1 deletion .github/workflows/api-breakage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
linux:
runs-on: ubuntu-latest
container:
image: swift:5.8
image: swift:5.9
steps:
- name: Checkout
uses: actions/checkout@v3
Expand Down
12 changes: 5 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ on:
push:
branches:
- main
- 2.x.x
paths:
- '**.swift'
- '**.yml'
pull_request:
branches:
- main
paths:
- '**.swift'
- '**.yml'
Expand All @@ -21,15 +20,14 @@ jobs:
strategy:
matrix:
image:
- 'swift:5.6'
- 'swift:5.7'
- 'swift:5.8'
- 'swift:5.9'
- 'swiftlang/swift:nightly-5.10-jammy'

container:
image: ${{ matrix.image }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Build
run: |
swift build
swift test
14 changes: 8 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,31 +1,33 @@
// swift-tools-version:5.6
// swift-tools-version:5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "hummingbird-fluent",
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13)],
platforms: [.macOS(.v14), .iOS(.v17), .tvOS(.v17)],
products: [
.library(name: "HummingbirdFluent", targets: ["HummingbirdFluent"]),
],
dependencies: [
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-async-algorithms.git", from: "1.0.0"),
.package(url: "https://github.com/hummingbird-project/hummingbird.git", branch: "2.x.x"),
.package(url: "https://github.com/vapor/fluent-kit.git", from: "1.17.0"),
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.0.0"),
// used in tests
.package(url: "https://github.com/vapor/fluent-sqlite-driver.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/fluent-mysql-driver.git", from: "4.0.0"),
],
targets: [
.target(name: "HummingbirdFluent", dependencies: [
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
.product(name: "FluentKit", package: "fluent-kit"),
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle"),
]),
.testTarget(name: "HummingbirdFluentTests", dependencies: [
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
// .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
// .product(name: "FluentMySQLDriver", package: "fluent-mysql-driver"),
.byName(name: "HummingbirdFluent"),
.product(name: "HummingbirdFoundation", package: "hummingbird"),
.product(name: "HummingbirdXCT", package: "hummingbird"),
Expand Down
40 changes: 24 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Hummingbird interface to the [Fluent](https://github.com/vapor/fluent-kit) database ORM.

Hummingbird doesn't come with any database drivers or ORM. This library provides a connection to Vapor's database ORM. The Vapor guys have been generous and forward thinking enough to ensure Fluent-kit can be used independent of Vapor. They have a small library that links Vapor to Fluent, this library does pretty much the same thing for Hummingbird.
Hummingbird doesn't come with any database drivers or ORM. This library provides a connection to Vapor's database ORM. The Vapor guys have been generous and forward thinking enough to ensure Fluent-kit can be used independent of Vapor. This package collates the fluent features into one. It also provides a driver for the Hummingbird Persist framework.

## Usage

Expand All @@ -12,30 +12,38 @@ The following initializes an SQLite database and adds a single migration `Create
import FluentSQLiteDriver
import HummingbirdFluent

let app = HBApplication()
// add Fluent
app.addFluent()
let logger = Logger(label: "MyApp")
let fluent = HBFluent(logger: logger)
// add sqlite database
app.fluent.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite)
// add migrations
app.fluent.migrations.add(CreateTodo())
fluent.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite)
// add migration
await fluent.migrations.add(CreateTodo())
// migrate
if arguments.migrate {
try app.fluent.migrate().wait()
try fluent.migrate().wait()
}
```
In general the interface to Fluent follows the same pattern as Vapor, except the `db` and `migrations` objects are only accessible from within the `fluent` object, and you need to call `HBApplication.addFluent()` at initialization.

Fluent can be used from a route as follows. The database is accessible via `HBRequest.db`.
Fluent can be used from a route as follows.

```swift
app.router
.endpoint("todos")
.get(":id") { request in
guard let id = request.parameters.get("id", as: UUID.self) else { return request.failure(HBHTTPError(.badRequest)) }
return Todo.find(id, on: request.db)
let router = HBRouter()
router
.group("todos")
.get(":id") { request, context in
guard let id = context.parameters.get("id", as: UUID.self) else { return request.failure(HBHTTPError(.badRequest)) }
return Todo.find(id, on: fluent.db())
}
```
Here we are returning a `Todo` with an id specified in the path.
Here we are returning a `Todo` with an id specified in the request URI.

You can then bring this together by creating an application that uses the router and adding fluent to its list of services

```swift
var app = HBApplication(router: router)
// add the fluent service to the application so it can manage shutdown correctly
app.addServices(fluent)
try await app.runService()
```

You can find more documentation on Fluent [here](https://docs.vapor.codes/4.0/fluent/overview/).
47 changes: 0 additions & 47 deletions Sources/HummingbirdFluent/Application+fluent.swift

This file was deleted.

122 changes: 41 additions & 81 deletions Sources/HummingbirdFluent/Fluent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,85 +14,57 @@

import FluentKit
import Hummingbird
import ServiceLifecycle

@MainActor
public struct MainActorBox<Value>: Sendable {
public let value: Value
}

extension DatabaseID: @unchecked Sendable {}

/// Manage fluent databases and migrations
///
/// You can either create this separate from `HBApplication` or add it to your application
/// using `HBApplication.addFluent`.
public struct HBFluent {
/// Fluent history management
public class History {
/// Is history recording enabled
public private(set) var enabled: Bool
// History of queries to Fluent
public private(set) var history: QueryHistory?

init() {
self.enabled = false
self.history = nil
}

/// Start recording history
public func start() {
self.enabled = true
self.history = .init()
}

/// Stop recording history
public func stop() {
self.enabled = false
}

/// Clear history
public func clear() {
self.history = .init()
}
}

/// Databases attached
public let databases: Databases
/// List of migrations
public let migrations: Migrations
/// Event loop group used by migrator
public struct HBFluent: Sendable, Service {
/// Event loop group
public let eventLoopGroup: EventLoopGroup
/// Logger
public let logger: Logger
/// Fluent history setup
public let history: History
/// List of migrations. Only accessible from the main actor
@MainActor
public var migrations: Migrations { self._migrations.value }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm currently using @MainActor here. @Joannis what are your thoughts on setting up a Hummingbird global actor in Hummingbird and using that instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you foresee other places where you'd want/need this? What's the "domain" of the actor? What type of information do you scope it to?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point I time probably not so we can keep as @MainActor. The migration stuff should only be setup during initialisation.

/// Databases attached
public var databases: Databases { self._databases.wrappedValue }

/// Initialize HBFluent
/// - Parameter application: application to get NIOThreadPool, EventLoopGroup and Logger from
init(application: HBApplication) {
self.databases = Databases(threadPool: application.threadPool, on: application.eventLoopGroup)
self.migrations = .init()
self.eventLoopGroup = application.eventLoopGroup
self.logger = application.logger
self.history = .init()
}
private let _databases: UnsafeTransfer<Databases>
private let _migrations: MainActorBox<Migrations>

/// Initialize HBFluent
/// - Parameters:
/// - eventLoopGroup: EventLoopGroup used by databases
/// - threadPool: NIOThreadPool used by databases
/// - logger: Logger used by databases
public init(
eventLoopGroup: EventLoopGroup,
threadPool: NIOThreadPool,
eventLoopGroupProvider: EventLoopGroupProvider = .singleton,
threadPool: NIOThreadPool = .singleton,
logger: Logger
) {
self.databases = Databases(threadPool: threadPool, on: eventLoopGroup)
self.migrations = .init()
let eventLoopGroup = eventLoopGroupProvider.eventLoopGroup
self._databases = .init(Databases(threadPool: threadPool, on: eventLoopGroup))
self._migrations = .init(value: .init())
self.eventLoopGroup = eventLoopGroup
self.logger = logger
self.history = .init()
}

/// Shutdown databases
public func shutdown() {
self.databases.shutdown()
public func run() async throws {
await GracefulShutdownWaiter().wait()
self._databases.wrappedValue.shutdown()
}

/// fluent migrator
@MainActor
public var migrator: Migrator {
Migrator(
databases: self.databases,
Expand All @@ -103,46 +75,34 @@ public struct HBFluent {
}

/// Run migration if needed
public func migrate() -> EventLoopFuture<Void> {
self.migrator.setupIfNeeded().flatMap {
self.migrator.prepareBatch()
}
@MainActor
public func migrate() async throws {
try await self.migrator.setupIfNeeded().get()
try await self.migrator.prepareBatch().get()
}

/// Run revert if needed
public func revert() -> EventLoopFuture<Void> {
self.migrator.setupIfNeeded().flatMap {
self.migrator.revertAllBatches()
}
@MainActor
public func revert() async throws {
try await self.migrator.setupIfNeeded().get()
try await self.migrator.revertAllBatches().get()
}

/// Return Database connection
///
/// - Parameters:
/// - id: ID of database
/// - eventLoop: Eventloop database connection is running on
/// - history: Query history storage
/// - pageSizeLimit: Set page size limit to avoid server overload
/// - Returns: Database connection
public func db(_ id: DatabaseID? = nil, on eventLoop: EventLoop) -> Database {
self.databases
public func db(_ id: DatabaseID? = nil, history: QueryHistory? = nil, pageSizeLimit: Int? = nil) -> Database {
self._databases.wrappedValue
.database(
id,
logger: self.logger,
on: eventLoop,
history: self.history.enabled ? self.history.history : nil
on: self.eventLoopGroup.any(),
history: history,
pageSizeLimit: pageSizeLimit
)!
}
}

/// async/await
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
extension HBFluent {
/// Run migration if needed
public func migrate() async throws {
try await self.migrate().get()
}

/// Run revert if needed
public func revert() async throws {
try await self.revert().get()
}
}
Loading
Loading