Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test-template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ on:
permissions:
contents: read
env:
SWIFT_IMAGE: 'swift:6.3-noble'
SWIFT_IMAGE: 'swiftlang/swift:nightly-6.4.x-noble'

jobs:

Expand Down Expand Up @@ -66,7 +66,7 @@ jobs:
fail-fast: false
matrix:
swift-image:
- 'swift:6.3-noble'
- 'swiftlang/swift:nightly-6.4.x-noble'
fluentflags:
- '--no-fluent'
- '--fluent.db mysql'
Expand Down
2 changes: 1 addition & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"recommendations": ["swiftlang.swift-vscode", "Vapor.vapor-vscode"]
}
}
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ================================
# Build image
# ================================
FROM swift:6.3-noble AS build
FROM swiftlang/swift:nightly-6.4.x-noble AS build

# Install OS updates
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
Expand Down
38 changes: 17 additions & 21 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,33 +1,26 @@
// swift-tools-version:6.3
// swift-tools-version:6.4
import PackageDescription

let package = Package(
name: "{{name}}",
platforms: [
.macOS(.v13)
.macOS("26.2")
],
dependencies: [
// 💧 A server-side Swift web framework.
.package(url: "https://github.com/vapor/vapor.git", from: "4.121.4"),{{#fluent}}
.package(url: "https://github.com/vapor/vapor.git", branch: "main", traits: ["bcrypt"]),{{#fluent}}
// 🗄 An ORM for SQL and NoSQL databases.
.package(url: "https://github.com/vapor/fluent.git", from: "4.13.0"),
.package(url: "https://github.com/vapor/fluent-kit.git", from: "1.56.0"),
// {{fluent.db.emoji}} Fluent driver for {{fluent.db.module}}.
.package(url: "https://github.com/vapor/fluent-{{fluent.db.url}}-driver.git", from: "{{fluent.db.version}}"),{{/fluent}}{{#leaf}}
// 🍃 An expressive, performant, and extensible templating language built for Swift.
.package(url: "https://github.com/vapor/leaf.git", from: "4.5.1"),{{/leaf}}
// 🔵 Non-blocking, event-driven networking for Swift. Used for custom executors
.package(url: "https://github.com/apple/swift-nio.git", from: "2.101.0"),
.package(url: "https://github.com/vapor/fluent-{{fluent.db.url}}-driver.git", from: "{{fluent.db.version}}"),{{/fluent}}
],
targets: [
.executableTarget(
name: "{{name}}",
dependencies: [{{#fluent}}
.product(name: "Fluent", package: "fluent"),
.product(name: "Fluent{{fluent.db.module}}Driver", package: "fluent-{{fluent.db.url}}-driver"),{{/fluent}}{{#leaf}}
.product(name: "Leaf", package: "leaf"),{{/leaf}}
.product(name: "FluentKit", package: "fluent-kit"),
.product(name: "Fluent{{fluent.db.module}}Driver", package: "fluent-{{fluent.db.url}}-driver"),{{/fluent}}
.product(name: "Vapor", package: "vapor"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
],
swiftSettings: swiftSettings
),
Expand All @@ -42,10 +35,13 @@ let package = Package(
]
)

var swiftSettings: [SwiftSetting] { [
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("InternalImportsByDefault"),
.enableUpcomingFeature("MemberImportVisibility"),
.enableUpcomingFeature("InferIsolatedConformances"),
.enableUpcomingFeature("ImmutableWeakCaptures"),
] }
var swiftSettings: [SwiftSetting] {
[
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("InternalImportsByDefault"),
.enableUpcomingFeature("MemberImportVisibility"),
.enableUpcomingFeature("InferIsolatedConformances"),
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.enableExperimentalFeature("SuppressedAssociatedTypesWithDefaults"),
]
}
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
[![Team Chat](https://design.vapor.codes/images/discordchat.svg)](https://discord.gg/vapor)
[![MIT License](https://design.vapor.codes/images/mitlicense.svg)](./LICENSE)
[![Continuous Integration](https://img.shields.io/github/actions/workflow/status/vapor/template/test-template.yml?event=push&style=plastic&logo=github&label=tests&logoColor=ccc)](https://github.com/vapor/template/actions/workflows/test-template.yml)
[![Swift 6.3+](https://design.vapor.codes/images/swift63up.svg)](https://swift.org)
[![Swift 6.4+](https://design.vapor.codes/images/swift64up.svg)](https://swift.org)

</div>

Expand All @@ -29,4 +29,4 @@ You can then move into the project directory:
cd <ProjectName>
```

To build and run the project, see the [Getting Started](https://docs.vapor.codes/getting-started/hello-world/#build-run) guide.
To build and run the project, see the [Getting Started](https://docs.vapor.codes/getting-started/hello-world/#build-run) guide.
29 changes: 23 additions & 6 deletions Sources/App/Controllers/TodoController.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import Fluent
import FluentKit
import HTTPTypes
import RoutingKit
import Vapor

#if canImport(FoundationEssentials)
import struct FoundationEssentials.UUID
#else
import struct Foundation.UUID
#endif

struct TodoController: RouteCollection {
let db: any Database

init(database: any Database) {
self.db = database
}

func boot(routes: any RoutesBuilder) throws {
let todos = routes.grouped("todos")

Expand All @@ -14,24 +28,27 @@ struct TodoController: RouteCollection {

@Sendable
func index(req: Request) async throws -> [TodoDTO] {
try await Todo.query(on: req.db).all().map { $0.toDTO() }
try await Todo.query(on: db).all().map { $0.toDTO() }
}

@Sendable
func create(req: Request) async throws -> TodoDTO {
let todo = try req.content.decode(TodoDTO.self).toModel()
let todo = try await req.content.decode(TodoDTO.self).toModel()

try await todo.save(on: req.db)
try await todo.save(on: db)
return todo.toDTO()
}

@Sendable
func delete(req: Request) async throws -> HTTPStatus {
guard let todo = try await Todo.find(req.parameters.get("todoID"), on: req.db) else {
guard let id = req.parameters.get("todoID").flatMap(UUID.init(uuidString:)) else {
throw Abort(.badRequest)
}
guard let todo = try await Todo.find(id, on: db) else {
throw Abort(.notFound)
}

try await todo.delete(on: req.db)
try await todo.delete(on: db)
return .noContent
}
}
12 changes: 9 additions & 3 deletions Sources/App/DTOs/TodoDTO.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import Fluent
import FluentKit
import Vapor

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

struct TodoDTO: Content {
var id: UUID?
var title: String?

func toModel() -> Todo {
let model = Todo()

model.id = self.id
if let title = self.title {
model.title = title
Expand Down
39 changes: 39 additions & 0 deletions Sources/App/Extensions/Databases+Vapor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import FluentKit
import Logging
import NIOCore
import NIOPosix
import Vapor

extension Databases {
enum Error: Swift.Error {
case noDatabaseConfigured
}

func database(for request: Request) throws(Error) -> any Database {
try self.database(logger: request.logger)
}

func database(for application: Application) throws(Error) -> any Database {
try self.database(logger: application.logger)
}

private func database(logger: Logger) throws(Error) -> any Database {
guard let database = self.database(logger: logger, on: MultiThreadedEventLoopGroup.singleton.any()) else {
throw Error.noDatabaseConfigured
}
return database
}

func revert(migrations: any Migration..., on application: Application) async throws {
let container = Migrations()
container.add(migrations)

let migrator = Migrator(
databases: self,
migrations: container,
logger: application.logger,
on: MultiThreadedEventLoopGroup.singleton.any()
)
try await migrator.revertAllBatches().get()
}
}
28 changes: 28 additions & 0 deletions Sources/App/LifecycleHandlers/MigrateLifecycleHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import FluentKit
import NIOCore
import NIOPosix
import Vapor

struct MigrateLifecycleHandler: LifecycleHandler {
let databases: Databases
let migrations: [any Migration]

init(databases: Databases, migrations: any Migration...) {
self.databases = databases
self.migrations = migrations
}

func willBoot(_ application: Application) async throws {
let migrations = Migrations()
migrations.add(self.migrations)

let migrator = Migrator(
databases: databases,
migrations: migrations,
logger: application.logger,
on: MultiThreadedEventLoopGroup.singleton.any()
)
try await migrator.setupIfNeeded().get()
try await migrator.prepareBatch().get()
}
}
2 changes: 1 addition & 1 deletion Sources/App/Migrations/CreateTodo.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Fluent
import FluentKit

struct CreateTodo: AsyncMigration {
func prepare(on database: any Database) async throws {
Expand Down
13 changes: 9 additions & 4 deletions Sources/App/Models/Todo.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import Fluent
import struct Foundation.UUID
import FluentKit

#if canImport(FoundationEssentials)
import struct FoundationEssentials.UUID
#else
import struct Foundation.UUID
#endif

/// Property wrappers interact poorly with `Sendable` checking, causing a warning for the `@ID` property
/// It is recommended you write your model with sendability checking on and then suppress the warning
/// afterwards with `@unchecked Sendable`.
final class Todo: Model, @unchecked Sendable {
static let schema = "todos"

@ID(key: .id)
var id: UUID?

Expand All @@ -19,7 +24,7 @@ final class Todo: Model, @unchecked Sendable {
self.id = id
self.title = title
}

func toDTO() -> TodoDTO {
.init(
id: self.id,
Expand Down
22 changes: 12 additions & 10 deletions Sources/App/configure.swift
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
{{#fluent}}import NIOSSL
import Fluent
import FluentKit
import Fluent{{fluent.db.module}}Driver
{{/fluent}}{{#leaf}}import Leaf
{{/leaf}}import Vapor

{{/fluent}}import Vapor
/// configures your application
func configure(_ app: Application) async throws {
// uncomment to serve files from /Public folder
// app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)){{#fluent}}

{{#fluent.db.is_postgres}}app.databases.use(DatabaseConfigurationFactory.postgres(configuration: .init(
let databases = Databases(threadPool: .singleton, on: .singletonMultiThreadedEventLoopGroup)

{{#fluent.db.is_postgres}}databases.use(DatabaseConfigurationFactory.postgres(configuration: .init(
hostname: Environment.get("DATABASE_HOST") ?? "localhost",
port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? SQLPostgresConfiguration.ianaPortNumber,
username: Environment.get("DATABASE_USERNAME") ?? "vapor_username",
password: Environment.get("DATABASE_PASSWORD") ?? "vapor_password",
database: Environment.get("DATABASE_NAME") ?? "vapor_database",
tls: .prefer(try .init(configuration: .clientDefault)))
), as: .psql){{/fluent.db.is_postgres}}{{#fluent.db.is_mysql}}app.databases.use(DatabaseConfigurationFactory.mysql(
), as: .psql){{/fluent.db.is_postgres}}{{#fluent.db.is_mysql}}databases.use(DatabaseConfigurationFactory.mysql(
hostname: Environment.get("DATABASE_HOST") ?? "localhost",
port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? MySQLConfiguration.ianaPortNumber,
username: Environment.get("DATABASE_USERNAME") ?? "vapor_username",
password: Environment.get("DATABASE_PASSWORD") ?? "vapor_password",
database: Environment.get("DATABASE_NAME") ?? "vapor_database"
), as: .mysql){{/fluent.db.is_mysql}}{{#fluent.db.is_sqlite}}app.databases.use(DatabaseConfigurationFactory.sqlite(.file("db.sqlite")), as: .sqlite){{/fluent.db.is_sqlite}}
), as: .mysql){{/fluent.db.is_mysql}}{{#fluent.db.is_sqlite}}databases.use(DatabaseConfigurationFactory.sqlite(.file("db.sqlite")), as: .sqlite){{/fluent.db.is_sqlite}}

app.migrations.add(CreateTodo()){{/fluent}}{{#leaf}}
// close database connections cleanly on shutdown
app.addService(databases)

app.views.use(.leaf){{/leaf}}
// run migrations before the app boots
app.lifecycle.use(MigrateLifecycleHandler(databases: databases, migrations: CreateTodo())){{/fluent}}

// register routes
try routes(app)
try await routes(app{{#fluent}}, database: databases.database(for: app){{/fluent}})
}
36 changes: 17 additions & 19 deletions Sources/App/entrypoint.swift
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
import Vapor
import Configuration
import ConsoleLogger
import Logging
import NIOCore
import NIOPosix
import Vapor

@main
enum Entrypoint {
struct Entrypoint {
static func main() async throws {
var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)

let app = try await Application.make(env)
let config = ConfigReader(providers: [
CommandLineArgumentsProvider(),
EnvironmentVariablesProvider(),
])
ConsoleLogger.bootstrap(config: config)

let services = Application.ServiceConfiguration(
logger: .provided(.init(label: "{{name}}.logger"))
)

// This attempts to install NIO as the Swift Concurrency global executor.
// You can enable it if you'd like to reduce the amount of context switching between NIO and Swift Concurrency.
// Note: this has caused issues with some libraries that use `.wait()` and cleanly shutting down.
// If enabled, you should be careful about calling async functions before this point as it can cause assertion failures.
// let executorTakeoverSuccess = NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor()
// app.logger.debug("Tried to install SwiftNIO's EventLoopGroup as Swift's global concurrency executor", metadata: ["success": .stringConvertible(executorTakeoverSuccess)])

let app = try await Application(configReader: config, services: services)
do {
try await configure(app)
try await app.execute()
try await app.run()
try await app.shutdown()
} catch {
app.logger.report(error: error)
try? await app.asyncShutdown()
try? await app.shutdown()
throw error
}
try await app.asyncShutdown()
}
}
Loading
Loading