feat: Initial commit

This commit is contained in:
2024-12-31 17:02:29 -05:00
parent bb568ba60e
commit 8dba393267
21 changed files with 1881 additions and 63 deletions

View File

@@ -0,0 +1,49 @@
import Fluent
import Foundation
import Vapor
struct ApiController: RouteCollection {
func boot(routes: any RoutesBuilder) throws {
let users = routes.grouped("api", "users")
users.get(use: usersIndex(req:))
users.post(use: createUser(req:))
let proCon = routes.grouped("api", "procons")
proCon.get(use: prosAndConsIndex(req:))
proCon.post(use: createProCon(req:))
}
@Sendable
func usersIndex(req: Request) async throws -> [User] {
try await User.query(on: req.db).all()
}
@Sendable
func prosAndConsIndex(req: Request) async throws -> [ProCon] {
try await ProCon.query(on: req.db).all()
}
@Sendable
func createUser(req: Request) async throws -> User {
let user = try req.content.decode(User.self)
try await user.save(on: req.db)
return user
}
@Sendable
func createProCon(req: Request) async throws -> ProCon {
let proconData = try req.content.decode(ProConDTO.self)
let proCon = ProCon(type: proconData.type, description: proconData.description, userId: proconData.userId)
try await proCon.create(on: req.db)
return proCon
}
}
struct ProConDTO: Content {
let id: UUID?
let type: ProCon.ProConType
let description: String
let userId: User.IDValue
}

View File

@@ -0,0 +1,19 @@
import Fluent
import Vapor
struct CreateProCon: AsyncMigration {
func prepare(on database: Database) async throws {
try await database.schema("procon")
.id()
.field("description", .string, .required)
.field("type", .string, .required)
.field("userId", .uuid, .required, .references("user", "id"))
.create()
}
func revert(on database: Database) async throws {
try await database.schema("procon").delete()
}
}

View File

@@ -0,0 +1,16 @@
import Fluent
import Vapor
struct CreateUser: AsyncMigration {
func prepare(on database: Database) async throws {
try await database.schema("user")
.id()
.field("displayName", .string, .required)
.create()
}
func revert(on database: Database) async throws {
try await database.schema("user").delete()
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
import Fluent
import Foundation
import Vapor
final class ProCon: Model {
static let schema = "procon"
@ID(key: .id)
var id: UUID?
@Parent(key: "userId")
var user: User
@Field(key: "description")
var description: String
@Field(key: "type")
var type: ProConType
init() {}
init(
id: UUID? = nil,
type: ProConType,
description: String,
userId: User.IDValue
) {
self.id = id
self.type = type
self.description = description
$user.id = userId
}
enum ProConType: String, Codable, Equatable, Sendable {
case pro, con
}
}
extension ProCon: Content {}

View File

@@ -0,0 +1,25 @@
import Fluent
import Foundation
import Vapor
final class User: Model {
static let schema = "user"
@ID(key: .id)
var id: UUID?
@Field(key: "displayName")
var displayName: String
@Children(for: \.$user)
var prosAndCons: [ProCon]
init() {}
init(id: UUID? = nil, displayName: String) {
self.id = id
self.displayName = displayName
}
}
extension User: Content {}

View File

@@ -1,9 +1,22 @@
import Fluent
import FluentSQLiteDriver
import Leaf
import Vapor
// 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))
// register routes
try routes(app)
// uncomment to serve files from /Public folder
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
app.middleware.use(app.sessions.middleware)
// app.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite)
app.databases.use(.sqlite(.memory), as: .sqlite)
app.migrations.add(CreateProCon())
app.migrations.add(CreateUser())
app.views.use(.leaf)
// register routes
try routes(app)
try app.register(collection: ApiController())
try await app.autoMigrate()
}

View File

@@ -1,31 +1,31 @@
import Vapor
import Logging
import NIOCore
import NIOPosix
import Vapor
@main
enum Entrypoint {
static func main() async throws {
var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)
let app = try await Application.make(env)
static func main() async throws {
var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)
// 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 {
app.logger.report(error: error)
try? await app.asyncShutdown()
throw error
}
try await app.execute()
try await app.asyncShutdown()
let app = try await Application.make(env)
// 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 {
app.logger.report(error: error)
try? await app.asyncShutdown()
throw error
}
try await app.execute()
try await app.asyncShutdown()
}
}

View File

@@ -1,11 +1,93 @@
import Fluent
import Foundation
import Vapor
func routes(_ app: Application) throws {
app.get { req async in
"It works!"
app.get { req in
let output = try await req.view.render("login")
return output
}
app.get("loggedIn") { req in
guard let userIdString = req.session.data["userId"],
let displayName = req.session.data["displayName"],
let userId = UUID(uuidString: userIdString)
else {
return try await req.view.render("/")
}
guard let user = try await User.query(on: req.db)
.filter(\.$id == userId)
.with(\.$prosAndCons)
.first()
else {
throw Abort(.unauthorized)
}
// let prosAndCons = try await user.$prosAndCons.get(on: req.db)
return try await req.view.render(
"loggedIn",
LoggedInContext(name: displayName, prosAndCons: user.prosAndCons)
)
}
app.get("submitProOrCon") { req in
let params = try req.query.decode(SubmitProOrCon.self)
guard let userIdString = req.session.data["userId"],
let userId = UUID(uuidString: userIdString)
else {
throw Abort(.unauthorized)
}
let proOrCon = ProCon(type: params.type, description: params.description, userId: userId)
_ = try await req.db.transaction {
proOrCon.save(on: $0)
}
.get()
return req.redirect(to: "loggedIn")
}
app.get("login") { req in
let params = try req.query.decode(LoginParams.self)
req.logger.info("params: \(params)")
do {
try checkForBadWords(in: params.displayName)
} catch {
throw Abort(.unauthorized, reason: "Stop using such naughty language.")
}
app.get("hello") { req async -> String in
"Hello, world!"
}
let user = User(displayName: params.displayName)
_ = try await req.db.transaction {
user.save(on: $0)
}.get()
let userId = user.id?.uuidString ?? "nil"
req.session.data["userId"] = userId
req.session.data["displayName"] = user.displayName
// return try await req.view.render("loggedIn", ["name": user.displayName])
return req.redirect(to: "loggedIn")
}
}
struct DisplayNameError: Error {}
struct LoginParams: Content {
let displayName: String
}
struct SubmitProOrCon: Content {
let type: ProCon.ProConType
let description: String
}
struct LoggedInContext: Encodable {
let name: String
let pros: [ProCon]
let cons: [ProCon]
init(name: String, prosAndCons: [ProCon]) {
self.name = name
self.cons = prosAndCons.filter { $0.type == .con }
self.pros = prosAndCons.filter { $0.type == .pro }
}
}