Skip to content

Commit

Permalink
Get HummingbirdFluent working with HB 2.0 (#11)
Browse files Browse the repository at this point in the history
* Get fluent code compiling

* Get tests running

* Update CI

* Cleanup, remove history management code

* HBFluentPersistDriver doesnt need a Clock

* Use @mainactor for accessing migrations

* Make HBFluent Sendable

Wrap migrations in a MainActorBox
Wrap Databases in a UnsafeTransfer

* Include 2.x.x branch in CI

* Add Service.run override

* Fix persist tests

* Update for addServices api change

* Restructure tests slightly

* Update readme

* Change prints to debug logging
  • Loading branch information
adam-fowler authored Jan 5, 2024
1 parent f99dc93 commit a6f6454
Show file tree
Hide file tree
Showing 11 changed files with 408 additions and 597 deletions.
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 }
/// 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

0 comments on commit a6f6454

Please sign in to comment.