From 11a3cc1598d5a31861e9d90d415b96a5a1aa6971 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Mon, 6 Jul 2026 11:14:28 +0200 Subject: [PATCH 1/3] Update template for Vapor 5 --- .vscode/extensions.json | 2 +- Package.swift | 38 +++---- Sources/App/Controllers/TodoController.swift | 29 +++-- Sources/App/DTOs/TodoDTO.swift | 12 ++- Sources/App/Extensions/Databases+Vapor.swift | 39 +++++++ .../MigrateLifecycleHandler.swift | 28 +++++ Sources/App/Migrations/CreateTodo.swift | 2 +- Sources/App/Models/Todo.swift | 13 ++- Sources/App/configure.swift | 22 ++-- Sources/App/entrypoint.swift | 36 +++---- Sources/App/routes.swift | 15 ++- Tests/AppTests/AppTests.swift | 102 +++++++----------- Tests/AppTests/AppTrait.swift | 73 +++++++++++++ manifest.yml | 29 +++-- 14 files changed, 296 insertions(+), 144 deletions(-) create mode 100644 Sources/App/Extensions/Databases+Vapor.swift create mode 100644 Sources/App/LifecycleHandlers/MigrateLifecycleHandler.swift create mode 100644 Tests/AppTests/AppTrait.swift diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 42783d7c..3e56b7e3 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { "recommendations": ["swiftlang.swift-vscode", "Vapor.vapor-vscode"] -} \ No newline at end of file +} diff --git a/Package.swift b/Package.swift index 70262f55..fe4fddc3 100644 --- a/Package.swift +++ b/Package.swift @@ -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 ), @@ -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"), + ] +} diff --git a/Sources/App/Controllers/TodoController.swift b/Sources/App/Controllers/TodoController.swift index e1e92044..3c9e4c7d 100644 --- a/Sources/App/Controllers/TodoController.swift +++ b/Sources/App/Controllers/TodoController.swift @@ -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") @@ -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 } } diff --git a/Sources/App/DTOs/TodoDTO.swift b/Sources/App/DTOs/TodoDTO.swift index c35ba337..534623bf 100644 --- a/Sources/App/DTOs/TodoDTO.swift +++ b/Sources/App/DTOs/TodoDTO.swift @@ -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 diff --git a/Sources/App/Extensions/Databases+Vapor.swift b/Sources/App/Extensions/Databases+Vapor.swift new file mode 100644 index 00000000..cb2c8618 --- /dev/null +++ b/Sources/App/Extensions/Databases+Vapor.swift @@ -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() + } +} diff --git a/Sources/App/LifecycleHandlers/MigrateLifecycleHandler.swift b/Sources/App/LifecycleHandlers/MigrateLifecycleHandler.swift new file mode 100644 index 00000000..739346d1 --- /dev/null +++ b/Sources/App/LifecycleHandlers/MigrateLifecycleHandler.swift @@ -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() + } +} diff --git a/Sources/App/Migrations/CreateTodo.swift b/Sources/App/Migrations/CreateTodo.swift index 7466919a..34a92986 100644 --- a/Sources/App/Migrations/CreateTodo.swift +++ b/Sources/App/Migrations/CreateTodo.swift @@ -1,4 +1,4 @@ -import Fluent +import FluentKit struct CreateTodo: AsyncMigration { func prepare(on database: any Database) async throws { diff --git a/Sources/App/Models/Todo.swift b/Sources/App/Models/Todo.swift index de1837d5..591072ec 100644 --- a/Sources/App/Models/Todo.swift +++ b/Sources/App/Models/Todo.swift @@ -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? @@ -19,7 +24,7 @@ final class Todo: Model, @unchecked Sendable { self.id = id self.title = title } - + func toDTO() -> TodoDTO { .init( id: self.id, diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index b9b1a650..bc644c2c 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -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}}) } diff --git a/Sources/App/entrypoint.swift b/Sources/App/entrypoint.swift index ad283b4a..3c23ec68 100644 --- a/Sources/App/entrypoint.swift +++ b/Sources/App/entrypoint.swift @@ -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() } } diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index 38a176bc..b9157a44 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -1,16 +1,15 @@ -{{#fluent}}import Fluent -{{/fluent}}import Vapor +{{#fluent}}import FluentKit +{{/fluent}}import RoutingKit +import Vapor -func routes(_ app: Application) throws { - {{#leaf}}app.get { req async throws in - try await req.view.render("index", ["title": "Hello Vapor!"]) - }{{/leaf}}{{^leaf}}app.get { req async in +func routes(_ app: Application{{#fluent}}, database: any Database{{/fluent}}) async throws { + app.get { req async in "It works!" - }{{/leaf}} + } app.get("hello") { req async -> String in "Hello, world!" }{{#fluent}} - try app.register(collection: TodoController()){{/fluent}} + try await app.register(collection: TodoController(database: database)){{/fluent}} } diff --git a/Tests/AppTests/AppTests.swift b/Tests/AppTests/AppTests.swift index 04315dfa..55eafc01 100644 --- a/Tests/AppTests/AppTests.swift +++ b/Tests/AppTests/AppTests.swift @@ -1,86 +1,64 @@ @testable import {{name}} -import VaporTesting +import HTTPTypes import Testing -{{#fluent}}import Fluent +import Vapor +import VaporTesting +{{#fluent}}import FluentKit {{/fluent}} -{{#fluent}}@Suite("App Tests with DB", .serialized) -{{/fluent}}{{^fluent}}@Suite("App Tests") +{{#fluent}}@Suite("App Tests", .serialized, .withApp()) +{{/fluent}}{{^fluent}}@Suite("App Tests", .withApp()) {{/fluent}} struct {{name}}Tests { - {{#fluent}}private func withApp(_ test: (Application) async throws -> ()) async throws { - let app = try await Application.make(.testing) - 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() - } - - {{/fluent}}@Test("Test Hello World Route") + @Test("Test Hello World Route") func helloWorld() async throws { - try await withApp{{^fluent}}(configure: configure){{/fluent}} { app in - try await app.testing().test(.GET, "hello", afterResponse: { res async in - #expect(res.status == .ok) - #expect(res.body.string == "Hello, world!") - }) - } + try await app.testing().test(.get, "hello", afterResponse: { res async in + #expect(res.status == .ok) + #expect(res.body.string == "Hello, world!") + }) }{{#fluent}} - + @Test("Getting all the Todos") func getAllTodos() async throws { - try await withApp { app in - let sampleTodos = [Todo(title: "sample1"), Todo(title: "sample2")] - try await sampleTodos.create(on: app.db) - - try await app.testing().test(.GET, "todos", afterResponse: { res async throws in - #expect(res.status == .ok) - #expect(try - res.content.decode([TodoDTO].self).sorted(by: { ($0.title ?? "") < ($1.title ?? "") }) == - sampleTodos.map { $0.toDTO() }.sorted(by: { ($0.title ?? "") < ($1.title ?? "") }) - ) - }) - } + let sampleTodos = [Todo(title: "sample1"), Todo(title: "sample2")] + try await sampleTodos.create(on: database) + + try await app.testing().test(.get, "todos", afterResponse: { res async throws in + #expect(res.status == .ok) + let todos = try await res.content.decode([TodoDTO].self) + #expect( + todos.sorted { ($0.title ?? "") < ($1.title ?? "") } == + sampleTodos.map { $0.toDTO() }.sorted { ($0.title ?? "") < ($1.title ?? "") } + ) + }) } - + @Test("Creating a Todo") func createTodo() async throws { let newDTO = TodoDTO(id: nil, title: "test") - - try await withApp { app in - 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]) - }) - } + + 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: database).all() + #expect(models.map { $0.toDTO().title } == [newDTO.title]) + }) } - + @Test("Deleting a Todo") func deleteTodo() async throws { let testTodos = [Todo(title: "test1"), Todo(title: "test2")] - - try await withApp { app in - try await testTodos.create(on: app.db) - - try await app.testing().test(.DELETE, "todos/\(testTodos[0].requireID())", afterResponse: { res async throws in - #expect(res.status == .noContent) - let model = try await Todo.find(testTodos[0].id, on: app.db) - #expect(model == nil) - }) - } + try await testTodos.create(on: database) + + try await app.testing().test(.delete, "todos/\(testTodos[0].requireID())", afterResponse: { res async throws in + #expect(res.status == .noContent) + let model = try await Todo.find(testTodos[0].id, on: database) + #expect(model == nil) + }) }{{/fluent}} } {{#fluent}} - extension TodoDTO: Equatable { static func == (lhs: Self, rhs: Self) -> Bool { lhs.id == rhs.id && lhs.title == rhs.title diff --git a/Tests/AppTests/AppTrait.swift b/Tests/AppTests/AppTrait.swift new file mode 100644 index 00000000..c21098de --- /dev/null +++ b/Tests/AppTests/AppTrait.swift @@ -0,0 +1,73 @@ +@testable import {{name}} +import Testing +import Vapor +{{#fluent}}import NIOSSL +import FluentKit +import Fluent{{fluent.db.module}}Driver +{{/fluent}} +@TaskLocal var _application: Application? + +var app: Application { + get throws { try #require(_application) } +} +{{#fluent}} +@TaskLocal var _database: (any Database)? + +var database: any Database { + get throws { try #require(_database) } +} +{{/fluent}} +struct AppTrait: TestTrait, SuiteTrait, TestScoping { + func provideScope( + for test: Test, testCase: Test.Case?, performing function: @Sendable @concurrent () async throws -> Void + ) async throws { + let app = try await Application(.testing){{#fluent}} + + 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}}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}}databases.use(DatabaseConfigurationFactory.sqlite(.file("db.sqlite")), as: .sqlite){{/fluent.db.is_sqlite}} + + do { + try await configure(app) + try await app.run() + try await $_database.withValue(databases.database(for: app)) { + try await $_application.withValue(app) { + try await function() + } + } + try await databases.revert(migrations: CreateTodo(), on: app) + await databases.shutdownAsync() + } catch { + try? await databases.revert(migrations: CreateTodo(), on: app) + await databases.shutdownAsync() + try? await app.shutdown() + throw error + }{{/fluent}}{{^fluent}} + do { + try await configure(app) + try await $_application.withValue(app) { + try await function() + } + } catch { + try? await app.shutdown() + throw error + }{{/fluent}} + try await app.shutdown() + } +} + +extension Trait where Self == AppTrait { + static func withApp() -> Self { .init() } +} diff --git a/manifest.yml b/manifest.yml index 85707423..d2eafbbe 100644 --- a/manifest.yml +++ b/manifest.yml @@ -35,9 +35,9 @@ variables: version: "4.9.0" is_sqlite: true emoji: "\U0001FAB6" - - name: leaf - description: Would you like to use Leaf (templating)? - type: bool + # - name: leaf + # description: Would you like to use Leaf (templating)? + # type: bool files: - file: Package.swift dynamic: true @@ -47,6 +47,7 @@ files: dynamic_name: "{{name}}" files: - file: entrypoint.swift + dynamic: true - file: configure.swift dynamic: true - file: routes.swift @@ -68,6 +69,14 @@ files: - .gitkeep - file: TodoController.swift if: fluent + - folder: LifecycleHandlers + if: fluent + files: + - MigrateLifecycleHandler.swift + - folder: Extensions + if: fluent + files: + - Databases+Vapor.swift - folder: Tests files: - folder: AppTests @@ -76,12 +85,14 @@ files: - file: AppTests.swift dynamic_name: "{{name}}Tests.swift" dynamic: true - - folder: Resources - if: leaf - files: - - folder: Views - files: - - file: index.leaf + - file: AppTrait.swift + dynamic: true + # - folder: Resources + # if: leaf + # files: + # - folder: Views + # files: + # - file: index.leaf - folder: Public files: - .gitkeep From 6f1ce82f72fd59de8093b06576af8988e009bc72 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni Date: Mon, 6 Jul 2026 11:15:08 +0200 Subject: [PATCH 2/3] Use nightly image --- .github/workflows/test-template.yml | 4 ++-- Dockerfile | 2 +- README.md | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-template.yml b/.github/workflows/test-template.yml index e8a47fcd..9a48ce26 100644 --- a/.github/workflows/test-template.yml +++ b/.github/workflows/test-template.yml @@ -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: @@ -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' diff --git a/Dockerfile b/Dockerfile index c7750b71..b6392d00 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 \ diff --git a/README.md b/README.md index 91ad7c5c..f3411d92 100644 --- a/README.md +++ b/README.md @@ -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/swift63up.svg)](https://swift.org) @@ -29,4 +29,4 @@ You can then move into the project directory: cd ``` -To build and run the project, see the [Getting Started](https://docs.vapor.codes/getting-started/hello-world/#build-run) guide. \ No newline at end of file +To build and run the project, see the [Getting Started](https://docs.vapor.codes/getting-started/hello-world/#build-run) guide. From 858e0294f0f7f02441a1e312d12bce02317b00d5 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni <69189821+ptoffy@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:41:07 +0200 Subject: [PATCH 3/3] Update README.md Co-authored-by: Francesco Paolo Severino <96546612+fpseverino@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f3411d92..5fa6c4fe 100644 --- a/README.md +++ b/README.md @@ -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.4+](https://design.vapor.codes/images/swift63up.svg)](https://swift.org) +[![Swift 6.4+](https://design.vapor.codes/images/swift64up.svg)](https://swift.org)