Everywhere: Create basic test page
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
+19
-12
@@ -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")
|
||||||
] }
|
]
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,20 @@ enum Entrypoint {
|
|||||||
static func main() async throws {
|
static func main() async throws {
|
||||||
var env = try Environment.detect()
|
var env = try Environment.detect()
|
||||||
try LoggingSystem.bootstrap(from: &env)
|
try LoggingSystem.bootstrap(from: &env)
|
||||||
|
|
||||||
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.
|
||||||
// If enabled, you should be careful about calling async functions before this point as it can cause assertion failures.
|
// If enabled, you should be careful about calling async functions before this point as it can cause assertion failures.
|
||||||
// let executorTakeoverSuccess = NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor()
|
// let executorTakeoverSuccess = NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor()
|
||||||
// app.logger.debug("Tried to install SwiftNIO's EventLoopGroup as Swift's global concurrency executor", metadata: ["success": .stringConvertible(executorTakeoverSuccess)])
|
// app.logger.debug("Tried to install SwiftNIO's EventLoopGroup as Swift's global concurrency executor", metadata: ["success": .stringConvertible(executorTakeoverSuccess)])
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try await configure(app)
|
try await configure(app)
|
||||||
} catch {
|
} catch {
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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#>
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
# ------------------------------------------
|
||||||
Reference in New Issue
Block a user