diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..34e6648 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,276 @@ +{ + "originHash" : "928a4b649897cc0b7c73d0862d9400289c0503386d6f44998334e327c5931856", + "pins" : [ + { + "identity" : "async-http-client", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/async-http-client.git", + "state" : { + "revision" : "0a9b72369b9d87ab155ef585ef50700a34abf070", + "version" : "1.23.1" + } + }, + { + "identity" : "async-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/async-kit.git", + "state" : { + "revision" : "e048c8ee94967e8d8a1c2ec0e1156d6f7fa34d31", + "version" : "1.20.0" + } + }, + { + "identity" : "console-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/console-kit.git", + "state" : { + "revision" : "966d89ae64cd71c652a1e981bc971de59d64f13d", + "version" : "4.15.1" + } + }, + { + "identity" : "elementary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sliemeobn/elementary.git", + "state" : { + "revision" : "6d2d244d13f50295277e500db02fe7948d9454c2", + "version" : "0.4.1" + } + }, + { + "identity" : "fluent", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/fluent.git", + "state" : { + "revision" : "223b27d04ab2b51c25503c9922eecbcdf6c12f89", + "version" : "4.12.0" + } + }, + { + "identity" : "fluent-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/fluent-kit.git", + "state" : { + "revision" : "614d3ec27cdef50cfb9fc3cfd382b6a4d9578cff", + "version" : "1.49.0" + } + }, + { + "identity" : "fluent-mysql-driver", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/fluent-mysql-driver.git", + "state" : { + "revision" : "c1422fde19433fa9ded948f83a226291e9ac33a9", + "version" : "4.7.0" + } + }, + { + "identity" : "multipart-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/multipart-kit.git", + "state" : { + "revision" : "a31236f24bfd2ea2f520a74575881f6731d7ae68", + "version" : "4.7.0" + } + }, + { + "identity" : "mysql-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/mysql-kit.git", + "state" : { + "revision" : "ac03d7c3d4be7b6602a73a226b4bfdc97c5b8b11", + "version" : "4.9.0" + } + }, + { + "identity" : "mysql-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/mysql-nio.git", + "state" : { + "revision" : "d60444dc7b1525000eb443baa08bf79f059202ad", + "version" : "1.7.2" + } + }, + { + "identity" : "routing-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/routing-kit.git", + "state" : { + "revision" : "8c9a227476555c55837e569be71944e02a056b72", + "version" : "4.9.1" + } + }, + { + "identity" : "sql-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/sql-kit.git", + "state" : { + "revision" : "e0b35ff07601465dd9f3af19a1c23083acaae3bd", + "version" : "3.32.0" + } + }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "7faebca1ea4f9aaf0cda1cef7c43aecd2311ddf6", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "06dc63c6d8da54ee11ceb268cde1fa68161afc96", + "version" : "3.9.1" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types", + "state" : { + "revision" : "ae67c8178eb46944fd85e4dc6dd970e1f3ed6ccd", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", + "version" : "1.6.1" + } + }, + { + "identity" : "swift-metrics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-metrics.git", + "state" : { + "revision" : "e0165b53d49b413dd987526b641e05e246782685", + "version" : "2.5.0" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "914081701062b11e3bb9e21accc379822621995e", + "version" : "2.76.1" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "2e9746cfc57554f70b650b021b6ae4738abef3e6", + "version" : "1.24.1" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "eaa71bb6ae082eee5a07407b1ad0cbd8f48f9dca", + "version" : "1.34.1" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "c7e95421334b1068490b5d41314a50e70bab23d1", + "version" : "2.29.0" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "bbd5e63cf949b7db0c9edaf7a21e141c52afe214", + "version" : "1.23.0" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "c8a44d836fe7913603e246acab7c528c2e780168", + "version" : "1.4.0" + } + }, + { + "identity" : "vapor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/vapor.git", + "state" : { + "revision" : "9786a424db75c4e9eb53e255ce1268675b680562", + "version" : "4.106.3" + } + }, + { + "identity" : "vapor-elementary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor-community/vapor-elementary.git", + "state" : { + "revision" : "5262cb49ed5a5403477803268624b2a4d963632d", + "version" : "0.2.0" + } + }, + { + "identity" : "websocket-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/websocket-kit.git", + "state" : { + "revision" : "4232d34efa49f633ba61afde365d3896fc7f8740", + "version" : "2.15.0" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift index 513c360..d5a4ce4 100644 --- a/Package.swift +++ b/Package.swift @@ -4,27 +4,33 @@ import PackageDescription let package = Package( name: "website", platforms: [ - .macOS(.v13) + .macOS(.v14) ], dependencies: [ // 💧 A server-side Swift web framework. - .package(url: "https://github.com/vapor/vapor.git", from: "4.99.3"), + .package(url: "https://github.com/vapor/vapor.git", from: "4.106.3"), // 🗄 An ORM for SQL and NoSQL databases. - .package(url: "https://github.com/vapor/fluent.git", from: "4.9.0"), + .package(url: "https://github.com/vapor/fluent.git", from: "4.12.0"), // 🐬 Fluent driver for MySQL. - .package(url: "https://github.com/vapor/fluent-mysql-driver.git", from: "4.4.0"), + .package(url: "https://github.com/vapor/fluent-mysql-driver.git", from: "4.7.0"), // 🔵 Non-blocking, event-driven networking for Swift. Used for custom executors - .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.76.1"), + // + .package(url: "https://github.com/vapor-community/vapor-elementary.git", from: "0.2.0"), ], targets: [ .executableTarget( name: "App", dependencies: [ + // .product(name: "ElementaryHTMX", package: "elementary-htmx"), + // .product(name: "ElementaryHTMXSSE", package: "elementary-htmx"), + // .product(name: "ElementaryHTMXWS", package: "elementary-htmx"), .product(name: "Fluent", package: "fluent"), .product(name: "FluentMySQLDriver", package: "fluent-mysql-driver"), - .product(name: "Vapor", package: "vapor"), .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "Vapor", package: "vapor"), + .product(name: "VaporElementary", package: "vapor-elementary"), ], swiftSettings: swiftSettings ), @@ -35,12 +41,13 @@ let package = Package( .product(name: "XCTVapor", package: "vapor"), ], swiftSettings: swiftSettings - ) + ), ], - swiftLanguageModes: [.v5] + swiftLanguageModes: [.v6] ) -var swiftSettings: [SwiftSetting] { [ - .enableUpcomingFeature("DisableOutwardActorInference"), - .enableExperimentalFeature("StrictConcurrency"), -] } +var swiftSettings: [SwiftSetting] { + [ + .enableExperimentalFeature("StrictConcurrency") + ] +} diff --git a/Sources/App/Controllers/TodoController.swift b/Sources/App/Controllers/TodoController.swift index 63fe661..bed8baf 100644 --- a/Sources/App/Controllers/TodoController.swift +++ b/Sources/App/Controllers/TodoController.swift @@ -5,11 +5,14 @@ struct TodoController: RouteCollection { func boot(routes: RoutesBuilder) throws { let todos = routes.grouped("todos") - todos.get(use: self.index) - todos.post(use: self.create) - todos.group(":todoID") { todo in - todo.delete(use: self.delete) + todos.get(use: index) + todos.post(use: create) + todos.group(":id") { todo in + todo.get(use: show) + todo.put(use: update) + todo.delete(use: delete) } + // todos.delete(":todoID", use: delete) } @Sendable @@ -17,21 +20,63 @@ struct TodoController: RouteCollection { try await Todo.query(on: req.db).all().map { $0.toDTO() } } + @Sendable + func show(req: Request) async throws -> TodoDTO { + guard let uuid = hexToUUID(hex: req.parameters.get("id")!), + let todo = try await Todo.find(uuid, on: req.db) else { + throw Abort(.notFound) + } + + return todo.toDTO() + } + @Sendable func create(req: Request) async throws -> TodoDTO { let todo = try req.content.decode(TodoDTO.self).toModel() try await todo.save(on: req.db) + + return todo.toDTO() + } + + @Sendable + func update(req: Request) async throws -> TodoDTO { + guard let uuid = hexToUUID(hex: req.parameters.get("id")!), + let todo = try await Todo.find(uuid, on: req.db) else { + throw Abort(.notFound) + } + + let updatedTodo = try req.content.decode(Todo.self) + todo.title = updatedTodo.title + try await todo.save(on: req.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 uuid = hexToUUID(hex: req.parameters.get("id")!), + let todo = try await Todo.find(uuid, on: req.db) else { throw Abort(.notFound) } try await todo.delete(on: req.db) + return .noContent } + + // ------------------------------------- + + private func hexToUUID(hex: String) -> UUID? { + var uuid: String = hex.replacingOccurrences(of: "-", with: "") + + guard uuid.count == 32 else { return nil } + + uuid.insert("-", at: uuid.index(uuid.startIndex, offsetBy: 8)) + uuid.insert("-", at: uuid.index(uuid.startIndex, offsetBy: 12 + 1)) + uuid.insert("-", at: uuid.index(uuid.startIndex, offsetBy: 16 + 2)) + uuid.insert("-", at: uuid.index(uuid.startIndex, offsetBy: 20 + 3)) + + return UUID(uuidString: uuid) + } } diff --git a/Sources/App/Middleware/CustomErrorMiddleware.swift b/Sources/App/Middleware/CustomErrorMiddleware.swift new file mode 100644 index 0000000..e93274c --- /dev/null +++ b/Sources/App/Middleware/CustomErrorMiddleware.swift @@ -0,0 +1,91 @@ +import Vapor + +// Modified from Vapor.ErrorMiddleware + +public final class CustomErrorMiddleware: Middleware { + // Default response + public struct ErrorResponse: Codable { + var error: Bool + var reason: String + } + + public init(environment: Environment) { + self.environment = environment + } + + public func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture { + next.respond(to: request).flatMapErrorThrowing { error in + self.makeResponse(with: request, reason: error) + } + } + + // ------------------------------------- + + private func makeResponse(with req: Request, reason error: Error) -> Response { + let reason: String + let status: HTTPResponseStatus + var headers: HTTPHeaders + let source: ErrorSource + + // Inspect the error type and extract what data we can. + switch error { + case let debugAbort as (DebuggableError & AbortError): + (reason, status, headers, source) = (debugAbort.reason, debugAbort.status, debugAbort.headers, debugAbort.source ?? .capture()) + + case let abort as AbortError: + (reason, status, headers, source) = (abort.reason, abort.status, abort.headers, .capture()) + + case let debugErr as DebuggableError: + (reason, status, headers, source) = (debugErr.reason, .internalServerError, [:], debugErr.source ?? .capture()) + + default: + // In debug mode, provide the error description; otherwise hide it to avoid sensitive data disclosure. + reason = environment.isRelease ? "Something went wrong." : String(describing: error) + (status, headers, source) = (.internalServerError, [:], .capture()) + } + + // Report the error + req.logger.report(error: error, file: source.file, function: source.function, line: source.line) + + let body = makeResponseBody(with: req, reason: reason, status: status, headers: &headers) + + // Create a Response with appropriate status + return Response(status: status, headers: headers, body: body) + } + + private func makeResponseBody(with req: Request, reason: String, status: HTTPResponseStatus, + headers: inout HTTPHeaders) -> Response.Body { + let body: Response.Body + + if let acceptHeader = req.headers.first(name: "Accept"), + acceptHeader == "application/json" { + // Attempt to serialize the error to JSON + do { + let encoder = try ContentConfiguration.global.requireEncoder(for: .json) + var byteBuffer = req.byteBufferAllocator.buffer(capacity: 0) + try encoder.encode(ErrorResponse(error: true, reason: reason), to: &byteBuffer, headers: &headers) + + body = .init( + buffer: byteBuffer, + byteBufferAllocator: req.byteBufferAllocator + ) + } catch { + body = .init(string: "Oops: \(String(describing: error))\nWhile encoding error: \(reason)", + byteBufferAllocator: req.byteBufferAllocator) + headers.contentType = .plainText + } + } + else { + // Attempt to render the error to a page + let statusCode = String(status.code) + body = .init(string: MainLayout(title: "Error \(statusCode))") { + ErrorPage(status: statusCode, reason: reason) + }.render()) + headers.contentType = .html + } + + return body + } + + private let environment: Environment +} diff --git a/Sources/App/Views/Pages/ErrorPage.swift b/Sources/App/Views/Pages/ErrorPage.swift new file mode 100644 index 0000000..11f96e5 --- /dev/null +++ b/Sources/App/Views/Pages/ErrorPage.swift @@ -0,0 +1,15 @@ +import Elementary + +struct ErrorPage: HTML { + var status: String + var reason: String + + // ------------------------------------- + + var content: some HTML { + div { + p { "Error "; strong { status }; "." } + p { reason } + } + } +} diff --git a/Sources/App/Views/Pages/IndexPage.swift b/Sources/App/Views/Pages/IndexPage.swift new file mode 100644 index 0000000..1a0db4d --- /dev/null +++ b/Sources/App/Views/Pages/IndexPage.swift @@ -0,0 +1,58 @@ +import Elementary + +struct IndexPage: HTML { + var content: some HTML { + h1 { "Welcome to Our Website" } + p { + "This is a basic Bootstrap page with a sticky navigation bar and content in the middle." + } + p { "The navigation bar will stay at the top of the page as you scroll down." } + p { + """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam tincidunt arcu + sit amet leo rutrum luctus. Sed metus mi, consectetur vitae dui at, sodales + dignissim odio. Curabitur a nisi eros. Suspendisse semper ac justo non gravida. + Vestibulum accumsan interdum varius. Morbi at diam luctus, mattis mi nec, mollis + libero. Pellentesque habitant morbi tristique senectus et netus et malesuada + fames ac turpis egestas. Integer congue, nisl in tempus ultricies, ligula est + laoreet elit, sed elementum erat dui ac nisl. Aenean pulvinar arcu eget urna + venenatis, at dictum arcu ultrices. Etiam hendrerit, purus vitae sagittis + lobortis, nisi sapien vestibulum arcu, ut mattis arcu elit ut arcu. Nulla vitae + sem ac eros ullamcorper efficitur ut id arcu. Vestibulum euismod arcu eget + aliquet tempor. Nullam nec consequat magna. Etiam posuere, ipsum id condimentum + mollis, massa libero efficitur lectus, nec tempor mauris tellus ut ligula. + Quisque viverra diam velit, quis ultricies nisl lacinia vitae. + + Aliquam libero nibh, luctus vel augue at, congue feugiat risus. Mauris volutpat + eget eros ac congue. Duis venenatis, arcu vel sodales accumsan, diam mi posuere + mi, vitae rutrum ex mauris nec libero. Sed eleifend nulla magna, eu lobortis + mauris fringilla nec. In posuere dignissim eros, ut hendrerit quam lacinia eu. + Praesent vestibulum arcu enim, hendrerit convallis risus facilisis eu. Nunc + vitae mauris eu nulla laoreet rhoncus. Nullam ligula tellus, vulputate in + viverra nec, eleifend eu nulla. Praesent suscipit rutrum imperdiet. + + Curabitur in lacus eu diam cursus viverra non eget turpis. Donec a ornare ipsum, + sed egestas orci. Vivamus congue gravida elementum. Pellentesque vitae mauris + magna. Phasellus blandit urna vitae auctor consectetur. Aenean iaculis eget arcu + vitae ultricies. Nunc maximus, massa hendrerit faucibus fringilla, eros quam + consequat enim, sit amet sodales erat quam eget massa. + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum in venenatis + urna. Vestibulum lectus arcu, scelerisque in ipsum vitae, feugiat cursus tortor. + Maecenas aliquam nunc enim, id fringilla est mattis et. Vivamus vitae + ullamcorper erat. Sed quis vehicula felis, quis bibendum nunc. Duis semper + fermentum ante, id fermentum neque varius convallis. Donec dui leo, fringilla + nec massa nec, fermentum molestie purus. Donec eget feugiat velit. Nulla + facilisi. Cras maximus felis eu libero mollis consectetur. Nulla molestie vitae + neque venenatis porta. + + Vestibulum nunc diam, mattis eu bibendum at, sagittis vitae nunc. Duis lacinia + sodales enim, et elementum libero posuere et. Donec ut fringilla orci. Donec at + aliquet ipsum, quis mattis risus. Donec malesuada enim in egestas blandit. Proin + sagittis mauris magna, elementum faucibus metus tempus eu. Sed in sem ut tellus + porta luctus et sed diam. Donec felis ante, euismod a est vitae, mollis + condimentum nulla. + """ + } + } +} diff --git a/Sources/App/Views/Shared/MainLayout.swift b/Sources/App/Views/Shared/MainLayout.swift new file mode 100644 index 0000000..7aec4ac --- /dev/null +++ b/Sources/App/Views/Shared/MainLayout.swift @@ -0,0 +1,63 @@ +import Elementary + +extension MainLayout: Sendable where Body: Sendable {} +struct MainLayout: HTMLDocument { + var title: String + @HTMLBuilder var pageContent: Body // This var name can't be changed! + + // https://www.srihash.org/ + var head: some HTML { + meta(.charset(.utf8)) + meta(.name(.viewport), .content("width=device-width, initial-scale=1.0")) + + // --------------------------------- + // CSS includes + + link( + .rel(.stylesheet), + .href("https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css"), + .integrity("sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"), + .crossorigin(.anonymous)) + link(.rel(.stylesheet), .href("/style.css")) + + style { + """ + body { + padding-top: 56px; + } + """ + } + } + + var body: some HTML { + // --------------------------------- + // Header + + header { + NavMenu() + } + + // --------------------------------- + // Body + + main { + div(.class("cotainer mt-4")) { + div(.class("content px-4 pb-4")) { + pageContent + } + } + } + + // --------------------------------- + // JS includes + + script( + .src( + "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js"), + .integrity("sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"), + .crossorigin(.anonymous) + ) {} + script(.src("/js/site.js")) {} + } + +} diff --git a/Sources/App/Views/Shared/NavMenu.swift b/Sources/App/Views/Shared/NavMenu.swift new file mode 100644 index 0000000..fc403d2 --- /dev/null +++ b/Sources/App/Views/Shared/NavMenu.swift @@ -0,0 +1,34 @@ +import Elementary + +struct NavMenu: HTML { + var content: some HTML { + nav(.class("navbar navbar-expand-lg navbar-light bg-light fixed-top")) { + div(.class("container-fluid")) { + a(.class("navbar-brand"), .href("#")) { "Logo" } + button( + .class("navbar-toggler"), .type(.button), + .data("bs-toggle", value: "collapse"), + .data("bs-target", value: "#navbarNav") + ) { + span(.class("navbar-toggler-icon")) {} + } + div(.class("collapse navbar-collapse"), .id("navbarNav")) { + ul(.class("navbar-nav me-auto mb-2 mb-lg-0")) { + li(.class("nav-item")) { + a(.class("nav-link active"), .href("#")) { "Home" } + } + li(.class("nav-item")) { + a(.class("nav-link"), .href("#")) { "About" } + } + li(.class("nav-item")) { + a(.class("nav-link"), .href("#")) { "Services" } + } + li(.class("nav-item")) { + a(.class("nav-link"), .href("#")) { "Contact" } + } + } + } + } + } + } +} diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index 1731f65..2bf419b 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -3,20 +3,34 @@ import Fluent import FluentMySQLDriver import Vapor -// configures your application +// Configures your application public func configure(_ app: Application) async throws { - // uncomment to serve files from /Public folder - // app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) + app.middleware = .init() + // Error HTML pages or JSON responses + app.middleware.use(CustomErrorMiddleware(environment: app.environment)) + // Serve files from /Public folder + app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) + + // sudo mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql + // sudo systemctl enable mariadb.service + // sudo systemctl start mariadb.service + // sudo mariadb + // > CREATE DATABASE riyyi; + // > GRANT ALL ON riyyi.* TO 'riyyi'@'%' IDENTIFIED BY '123' WITH GRANT OPTION; + // > GRANT ALL ON riyyi.* TO 'riyyi'@'localhost' IDENTIFIED BY '123' WITH GRANT OPTION; // % does NOT match localhost! + // > FLUSH PRIVILEGES; app.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" + username: Environment.get("DATABASE_USERNAME") ?? "riyyi", + password: Environment.get("DATABASE_PASSWORD") ?? "123", + database: Environment.get("DATABASE_NAME") ?? "riyyi", + tlsConfiguration: nil // Local connections dont need encryption ), as: .mysql) app.migrations.add(CreateTodo()) - // register routes + + // Register routes try routes(app) } diff --git a/Sources/App/entrypoint.swift b/Sources/App/entrypoint.swift index 2e85ece..f2553fc 100644 --- a/Sources/App/entrypoint.swift +++ b/Sources/App/entrypoint.swift @@ -8,16 +8,20 @@ enum Entrypoint { static func main() async throws { var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) - + let app = try await Application.make(env) + #if DEBUG + app.logger.logLevel = .debug + #endif + // 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)]) - + do { try await configure(app) } catch { @@ -25,6 +29,7 @@ enum Entrypoint { try? await app.asyncShutdown() throw error } + try await app.execute() try await app.asyncShutdown() } diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index f958df1..4b06e48 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -1,14 +1,41 @@ +import Elementary import Fluent import Vapor +import VaporElementary func routes(_ app: Application) throws { - app.get { req async in - "It works!" + app.routes.caseInsensitive = true + + app.get { req async throws in + + let todo = Todo(title: "Test Todo") + try await todo.save(on: req.db) + + return "It works!" } app.get("hello") { req async -> String in "Hello, world!" } + app.get("test") { _ in + HTMLResponse { + MainLayout(title: "Test123") { + IndexPage() + } + } + } + try app.register(collection: TodoController()) } + +/* + +Closure Expression Syntax + https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures#Closure-Expression-Syntax + +{ (<#parameters#>) -> <#return type#> in + <#statements#> +} + +*/ diff --git a/makefile b/makefile new file mode 100644 index 0000000..808421a --- /dev/null +++ b/makefile @@ -0,0 +1,38 @@ +#------------------------------------------------------------------------------# + +PROGRAM := "App" + +#------------------------------------------------------------------------------# + +.PHONY: all debug build build-release run migrate revert clean clean-all test + +all: build + +build: + @ swift build + +build-release: + @ swift build --configuration release + +run: + @ swift run $(PROGRAM) --auto-migrate + +migrate: + @ swift run $(PROGRAM) migrate --auto-migrate + +revert: + @ swift run $(PROGRAM) migrate --auto-revert + +routes: + @ swift run $(PROGRAM) routes + +clean: + @- echo "Cleaning packages.." ; \ + swift package clean + +clean-all: + @- echo "Cleaning project.." ; \ + rm -rf ./.build + +test: + @ swift test diff --git a/requests b/requests new file mode 100644 index 0000000..035bb43 --- /dev/null +++ b/requests @@ -0,0 +1,61 @@ +# -*- restclient -*- + +# Package restclient-mode, as a Postman alternative. +# Lines starting with # are considered comments AND also act as separators. + +# C-c C-v | restclient-http-send-current-stay-in-window + +# (setq restclient-var-overrides nil) + +# ------------------------------------------ +# Variables + +:id = 00000000-0000-0000-0000-000000000000 + +# ------------------------------------------ + +GET http://localhost:8080/todos +-> jq-set-var :id .[0].id +Accept: application/json + +# -> run-hook (restclient-set-var ":id" (cdr (assq 'id (aref (json-read) 0)))) + +# ------------------------------------------ + +GET http://localhost:8080/todos/:id +Accept: application/json + +# ------------------------------------------ + +POST http://localhost:8080/todos +-> run-hook (restclient-set-var ":id" (cdr (assq 'id (json-read)))) +Accept: application/json +Content-Type: application/json + +{ + "title": "I need to to stuff", +} + +# ------------------------------------------ + +PUT http://localhost:8080/todos/:id +-> run-hook (restclient-set-var ":id" (cdr (assq 'id (json-read)))) +Accept: application/json +Content-Type: application/json + +{ + "title": "I need to to stuff some more", +} + +# ------------------------------------------ + +# The id needs to be according to the UUID spec, hex with spaces +# +# SELECT +# INSERT(INSERT(INSERT(INSERT(HEX(id), 9, 0, '-'), 14, 0, '-'), 19, 0, '-'), 24, 0, '-') AS id, +# title FROM riyyi.todos; + +DELETE http://localhost:8080/todos/:id +Accept: application/json + +# ------------------------------------------