Browse Source

Everywhere: Create basic test page

master
Riyyi 1 month ago
parent
commit
94cef15048
  1. 276
      Package.resolved
  2. 31
      Package.swift
  3. 55
      Sources/App/Controllers/TodoController.swift
  4. 91
      Sources/App/Middleware/CustomErrorMiddleware.swift
  5. 15
      Sources/App/Views/Pages/ErrorPage.swift
  6. 58
      Sources/App/Views/Pages/IndexPage.swift
  7. 63
      Sources/App/Views/Shared/MainLayout.swift
  8. 34
      Sources/App/Views/Shared/NavMenu.swift
  9. 28
      Sources/App/configure.swift
  10. 5
      Sources/App/entrypoint.swift
  11. 31
      Sources/App/routes.swift
  12. 38
      makefile
  13. 61
      requests

276
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
}

31
Package.swift

@ -4,27 +4,33 @@ import PackageDescription
let package = Package( let package = Package(
name: "website", name: "website",
platforms: [ platforms: [
.macOS(.v13) .macOS(.v14)
], ],
dependencies: [ dependencies: [
// 💧 A server-side Swift web framework. // 💧 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. // 🗄 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. // 🐬 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 // 🔵 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: [ targets: [
.executableTarget( .executableTarget(
name: "App", name: "App",
dependencies: [ 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: "Fluent", package: "fluent"),
.product(name: "FluentMySQLDriver", package: "fluent-mysql-driver"), .product(name: "FluentMySQLDriver", package: "fluent-mysql-driver"),
.product(name: "Vapor", package: "vapor"),
.product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"),
.product(name: "Vapor", package: "vapor"),
.product(name: "VaporElementary", package: "vapor-elementary"),
], ],
swiftSettings: swiftSettings swiftSettings: swiftSettings
), ),
@ -35,12 +41,13 @@ let package = Package(
.product(name: "XCTVapor", package: "vapor"), .product(name: "XCTVapor", package: "vapor"),
], ],
swiftSettings: swiftSettings swiftSettings: swiftSettings
) ),
], ],
swiftLanguageModes: [.v5] swiftLanguageModes: [.v6]
) )
var swiftSettings: [SwiftSetting] { [ var swiftSettings: [SwiftSetting] {
.enableUpcomingFeature("DisableOutwardActorInference"), [
.enableExperimentalFeature("StrictConcurrency"), .enableExperimentalFeature("StrictConcurrency")
] } ]
}

55
Sources/App/Controllers/TodoController.swift

@ -5,11 +5,14 @@ struct TodoController: RouteCollection {
func boot(routes: RoutesBuilder) throws { func boot(routes: RoutesBuilder) throws {
let todos = routes.grouped("todos") let todos = routes.grouped("todos")
todos.get(use: self.index) todos.get(use: index)
todos.post(use: self.create) todos.post(use: create)
todos.group(":todoID") { todo in todos.group(":id") { todo in
todo.delete(use: self.delete) todo.get(use: show)
todo.put(use: update)
todo.delete(use: delete)
} }
// todos.delete(":todoID", use: delete)
} }
@Sendable @Sendable
@ -17,21 +20,63 @@ struct TodoController: RouteCollection {
try await Todo.query(on: req.db).all().map { $0.toDTO() } 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 @Sendable
func create(req: Request) async throws -> TodoDTO { func create(req: Request) async throws -> TodoDTO {
let todo = try req.content.decode(TodoDTO.self).toModel() let todo = try req.content.decode(TodoDTO.self).toModel()
try await todo.save(on: req.db) 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() return todo.toDTO()
} }
@Sendable @Sendable
func delete(req: Request) async throws -> HTTPStatus { 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) throw Abort(.notFound)
} }
try await todo.delete(on: req.db) try await todo.delete(on: req.db)
return .noContent 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)
}
} }

91
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<Response> {
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
}

15
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 }
}
}
}

58
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.
"""
}
}
}

63
Sources/App/Views/Shared/MainLayout.swift

@ -0,0 +1,63 @@
import Elementary
extension MainLayout: Sendable where Body: Sendable {}
struct MainLayout<Body: HTML>: 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")) {}
}
}

34
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" }
}
}
}
}
}
}
}

28
Sources/App/configure.swift

@ -3,20 +3,34 @@ import Fluent
import FluentMySQLDriver import FluentMySQLDriver
import Vapor import Vapor
// configures your application // Configures your application
public func configure(_ app: Application) async throws { public func configure(_ app: Application) async throws {
// uncomment to serve files from /Public folder app.middleware = .init()
// app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) // 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( app.databases.use(DatabaseConfigurationFactory.mysql(
hostname: Environment.get("DATABASE_HOST") ?? "localhost", hostname: Environment.get("DATABASE_HOST") ?? "localhost",
port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? MySQLConfiguration.ianaPortNumber, port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? MySQLConfiguration.ianaPortNumber,
username: Environment.get("DATABASE_USERNAME") ?? "vapor_username", username: Environment.get("DATABASE_USERNAME") ?? "riyyi",
password: Environment.get("DATABASE_PASSWORD") ?? "vapor_password", password: Environment.get("DATABASE_PASSWORD") ?? "123",
database: Environment.get("DATABASE_NAME") ?? "vapor_database" database: Environment.get("DATABASE_NAME") ?? "riyyi",
tlsConfiguration: nil // Local connections dont need encryption
), as: .mysql) ), as: .mysql)
app.migrations.add(CreateTodo()) app.migrations.add(CreateTodo())
// register routes
// Register routes
try routes(app) try routes(app)
} }

5
Sources/App/entrypoint.swift

@ -11,6 +11,10 @@ enum Entrypoint {
let app = try await Application.make(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. // 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. // 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. // Note: this has caused issues with some libraries that use `.wait()` and cleanly shutting down.
@ -25,6 +29,7 @@ enum Entrypoint {
try? await app.asyncShutdown() try? await app.asyncShutdown()
throw error throw error
} }
try await app.execute() try await app.execute()
try await app.asyncShutdown() try await app.asyncShutdown()
} }

31
Sources/App/routes.swift

@ -1,14 +1,41 @@
import Elementary
import Fluent import Fluent
import Vapor import Vapor
import VaporElementary
func routes(_ app: Application) throws { func routes(_ app: Application) throws {
app.get { req async in app.routes.caseInsensitive = true
"It works!"
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 app.get("hello") { req async -> String in
"Hello, world!" "Hello, world!"
} }
app.get("test") { _ in
HTMLResponse {
MainLayout(title: "Test123") {
IndexPage()
}
}
}
try app.register(collection: TodoController()) 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#>
}
*/

38
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

61
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
# ------------------------------------------
Loading…
Cancel
Save