From 7ac0f299046a3c3840d070f10b43c9e4a5521be8 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Wed, 22 Dec 2021 22:11:37 +0100 Subject: [PATCH] Add first database version and model --- Sources/App/Management/SQLiteDatabase.swift | 177 ++++++++++++++++++ .../Model/Migrations/UserTableMigration.swift | 32 ++++ Sources/App/Model/Table.swift | 52 +++++ Sources/App/Model/User.swift | 55 ++++++ 4 files changed, 316 insertions(+) create mode 100644 Sources/App/Management/SQLiteDatabase.swift create mode 100644 Sources/App/Model/Migrations/UserTableMigration.swift create mode 100644 Sources/App/Model/Table.swift create mode 100644 Sources/App/Model/User.swift diff --git a/Sources/App/Management/SQLiteDatabase.swift b/Sources/App/Management/SQLiteDatabase.swift new file mode 100644 index 0000000..b7e0c7f --- /dev/null +++ b/Sources/App/Management/SQLiteDatabase.swift @@ -0,0 +1,177 @@ +import Foundation +import Fluent +import Vapor + +typealias PasswordHash = String +typealias SessionToken = String + +final class SQLiteDatabase { + + /// A mapping between player name and generated access tokens for a session + private var sessionTokenForPlayer = [PlayerName: SessionToken]() + + /// A reverse mapping between generated access tokens and player name + private var playerNameForToken = [SessionToken: PlayerName]() + + private let tables: TableManagement + + init(db: Database) throws { + self.tables = try TableManagement(db: db) + } + + func registerPlayer(named name: PlayerName, hash: PasswordHash, in database: Database) -> EventLoopFuture { + User.query(on: database).filter(\.$name == name).first() + .guard({ $0 == nil }, else: Abort(.conflict)).flatMap { _ in + let user = User(name: name, hash: hash) + return user.create(on: database).map { + // Create a new token and store it for the user + let token = SessionToken.newToken() + self.sessionTokenForPlayer[name] = token + self.playerNameForToken[token] = name + return token + } + } + } + + func passwordHashForExistingPlayer(named name: PlayerName, in database: Database) -> EventLoopFuture { + User.query(on: database).filter(\.$name == name).first() + .unwrap(or: Abort(.forbidden)).map { $0.passwordHash } + } + + func deletePlayer(named name: PlayerName, in database: Database) -> EventLoopFuture { + user(named: name, in: database).flatMap { user in + self.tables.leaveTable(player: user, in: database) + }.flatMap { + User.query(on: database).filter(\.$name == name).delete() + } + } + + func isValid(sessionToken token: SessionToken) -> Bool { + playerNameForToken[token] != nil + } + + func startSession(socket: WebSocket, sessionToken token: SessionToken) -> Bool { + guard let player = playerNameForToken[token] else { + return false + } + return tables.connect(player: player, using: socket) + } + + private func didReceive(message: String, forSessionToken token: SessionToken) { + // TODO: Handle client requests + print("Session \(token.prefix(6)): \(message)") + } + + func endSession(forSessionToken sessionToken: SessionToken) { + guard let player = endExistingSession(forSessionToken: sessionToken) else { + return + } + tables.disconnect(player: player) + } + + private func endExistingSession(forSessionToken token: SessionToken) -> PlayerName? { + guard let player = playerNameForToken.removeValue(forKey: token) else { + return nil + } + sessionTokenForPlayer.removeValue(forKey: player) + return player + } + + + /** + Start a new session for an existing user. + - Parameter name: The user name + - Returns: The generated access token for the session + */ + func startNewSessionForRegisteredPlayer(named name: PlayerName) -> SessionToken { + let token = SessionToken.newToken() + self.sessionTokenForPlayer[name] = token + self.playerNameForToken[token] = name + return token + } + + func registeredPlayerExists(withSessionToken token: SessionToken) -> PlayerName? { + playerNameForToken[token] + } + + func currentTableOfPlayer(named player: PlayerName) -> TableInfo? { + tables.tableInfo(player: player) + } + + private func points(for player: PlayerName, in database: Database) -> EventLoopFuture { + User.query(on: database) + .filter(\.$name == player) + .first() + .unwrap(or: Abort(.notFound)) + .map { $0.points } + } + + private func user(named name: PlayerName, in database: Database) -> EventLoopFuture { + User.query(on: database) + .filter(\.$name == name) + .first() + .unwrap(or: Abort(.notFound)) + } + + private func user(withToken token: SessionToken, in database: Database) -> EventLoopFuture { + database.eventLoop + .future() + .map { self.playerNameForToken[token] } + .unwrap(or: Abort(.unauthorized)) + .flatMap { name in + self.user(named: name, in: database) + } + } + + // MARK: Tables + + /** + Create a new table with optional players. + - Parameter name: The name of the table + - Parameter players: The player creating the table + - Parameter isPublic: Indicates that this is a game joinable by everyone + - Returns: The table id + */ + func createTable(named name: TableName, player: PlayerName, isPublic: Bool, in database: Database) -> EventLoopFuture { + user(named: player, in: database).flatMap { player in + self.tables.createTable(named: name, player: player, isPublic: isPublic, in: database) + } + } + + func getPublicTableInfos() -> [PublicTableInfo] { + tables.publicTableList + } + + func join(tableId: UUID, playerToken: SessionToken, in database: Database) -> EventLoopFuture { + user(withToken: playerToken, in: database).flatMap { player in + self.tables.join(tableId: tableId, player: player, in: database) + } + } + + func leaveTable(playerToken: SessionToken, in database: Database) -> EventLoopFuture { + user(withToken: playerToken, in: database).flatMap { player in + self.tables.leaveTable(player: player, in: database) + } + } + + func performAction(playerToken: SessionToken, action: PlayerAction) -> PlayerActionResult { + guard let player = playerNameForToken[playerToken] else { + return .invalidToken + } + return tables.performAction(player: player, action: action) + } + + func select(game: GameType, playerToken: SessionToken) -> PlayerActionResult { + guard let player = playerNameForToken[playerToken] else { + return .invalidToken + } + return tables.select(game: game, player: player) + } + + func play(card: Card, playerToken: SessionToken) -> PlayerActionResult { + guard let player = playerNameForToken[playerToken] else { + return .invalidToken + } + return tables.play(card: card, player: player) + } +} diff --git a/Sources/App/Model/Migrations/UserTableMigration.swift b/Sources/App/Model/Migrations/UserTableMigration.swift new file mode 100644 index 0000000..5d5590d --- /dev/null +++ b/Sources/App/Model/Migrations/UserTableMigration.swift @@ -0,0 +1,32 @@ +import FluentSQLiteDriver + +struct UserTableMigration: Migration { + + func prepare(on database: FluentSQLiteDriver.Database) -> EventLoopFuture { + let one = database.schema(User.schema) + .id() + .field(User.Key.name.key, .string, .required) + .field(User.Key.hash.key, .string, .required) + .field(User.Key.points.key, .int, .required) + .field(User.Key.table.key, .uuid, .references(Table.schema, Table.Key.id.key)) + .create() + let two = database.enum(Table.Key.language.rawValue) + .case(SupportedLanguage.german.rawValue) + .case(SupportedLanguage.english.rawValue) + .create() + + let three = database.enum(Table.Key.language.rawValue).read().flatMap { lang in + database.schema(Table.schema) + .id() + .field(Table.Key.name.key, .string, .required) + .field(Table.Key.isPublic.key, .bool, .required) + .field(Table.Key.language.key, lang, .required) + .create() + } + return one.and(two).and(three).map { _ in } + } + + func revert(on database: FluentSQLiteDriver.Database) -> EventLoopFuture { + database.eventLoop.makeCompletedFuture(.success(())) + } +} diff --git a/Sources/App/Model/Table.swift b/Sources/App/Model/Table.swift new file mode 100644 index 0000000..c5f5177 --- /dev/null +++ b/Sources/App/Model/Table.swift @@ -0,0 +1,52 @@ +import FluentSQLiteDriver +import Vapor + +/// A registered user +class Table: Model { + + enum Key: String { + case id = "id" + case name = "name" + case isPublic = "public" + case language = "language" + + var key: FieldKey { + .init(stringLiteral: rawValue) + } + } + + /// The name of the SQLite table + static let schema = "table" + + /// The unique identifier for this table. + @ID(key: .id) + var id: UUID? + + /// The user's full name. + @Field(key: Key.name.key) + var name: String + + /// The players sitting at the table + @Children(for: \.$table) + var players: [User] + + @Field(key: Key.isPublic.key) + var isPublic: Bool + + @Enum(key: Key.language.key) + var language: SupportedLanguage + + required init() { } + + /// Creates a new table. + init(id: UUID? = nil, name: String, isPublic: Bool = true, language: SupportedLanguage = .english) { + self.id = id + self.name = name + self.isPublic = isPublic + self.language = language + } + + var stringId: String { + "\(id!)" + } +} diff --git a/Sources/App/Model/User.swift b/Sources/App/Model/User.swift new file mode 100644 index 0000000..f2bca41 --- /dev/null +++ b/Sources/App/Model/User.swift @@ -0,0 +1,55 @@ +import FluentSQLiteDriver +import Vapor + + + +/// A registered user +final class User: Model { + + enum Key: String { + case id = "id" + case name = "name" + case hash = "hash" + case points = "points" + case table = "table_id" + + var key: FieldKey { + .init(stringLiteral: rawValue) + } + } + + /// The name of the SQLite table + static let schema = "user" + + /// The unique identifier for this user. + @ID(key: .id) + var id: UUID? + + /// The user's full name. + @Field(key: Key.name.key) + var name: String + + /// The hash of the user's password + @Field(key: Key.hash.key) + var passwordHash: String + + /// The user's total points + @Field(key: Key.points.key) + var points: Int + + // Example of an optional parent relation. + @OptionalParent(key: Key.table.key) + var table: Table? + + init() { } + + /// Creates a new user. + init(id: UUID? = nil, name: String, hash: String) { + self.id = id + self.name = name + self.passwordHash = hash + self.points = 0 + } +} + +//extension User: Content { }