import { Database, type Changes } from "bun:sqlite"; import _ from "lodash"; import { Ok, Err, Result, None, Some, Option } from "ts-results-es"; import { z } from "zod"; import { fun } from "./common"; const db = new Database("lab.sqlite", { strict: true }) initDB(db); // Error Type export const BOARD_ERR_WRONG_TYPE = "Wrong type" export const BOARD_ERR_NO_BOARDS = "No such Boards" export const BOARD_ERR_ID_CONFLICT = "ID conflict" export const BOARD_ERR_NAME_CONFLICT = "Name conflict in one room" export type BoardErrorType = ( typeof BOARD_ERR_WRONG_TYPE | typeof BOARD_ERR_NO_BOARDS | typeof BOARD_ERR_ID_CONFLICT | typeof BOARD_ERR_NAME_CONFLICT ) const boardSchema = z.object({ id: z.number().nonnegative(), name: z.string(), room: z.string(), ipv4: z.string().ip({ version: "v4" }), ipv6: z.string().ip({ version: "v6" }), port: z.number().nonnegative().lte(65535), cmdID: z.number().nonnegative() }).partial({ ipv6: true, cmdID: true }) export type Board = z.infer export type BoardColumn = keyof Board export function isBoard(obj: any): obj is Board { return boardSchema.safeParse(obj).success } export function isBoardArray(obj: any): obj is Array { return boardSchema.array().safeParse(obj).success } export function isBoardColumn(obj: any): obj is BoardColumn { return boardSchema.keyof().safeParse(obj).success } const userSchema = z.object({ id: z.number().nonnegative(), name: z.string(), password: z.string(), boardID: z.number(), }).partial({ boardID: true, }) export type User = z.infer export type UserColumn = keyof User export function isUser(obj: any): obj is User { return userSchema.safeParse(obj).success } export function isUserArray(obj: any): obj is Array { return userSchema.array().safeParse(obj).success } export function isUserColumn(obj: any): obj is UserColumn { return userSchema.keyof().safeParse(obj).success } function initDB(db: Database) { const tables = allTables() if (!tables.includes("Users")) { db.query(` CREATE TABLE Users( id INT PRIMARY KEY NOT NULL, name TEXT NOT NULL, password TEXT NOT NULL ); `).run(); } if (!tables.includes("Boards")) db.query(` CREATE TABLE Boards( id INT PRIMARY KEY NOT NULL, name TEXT NOT NULL, room TEXT NOT NULL, ipv4 CHAR(16) NOT NULL, ipv6 CHAR(46) , port INT NOT NULL ) `).run(); } export function allTables(): Array { const query = db.query(`SELECT name FROM sqlite_master WHERE type='table'`) var tables = new Array() // Flaten array for (const item of query.values()) { tables.push(item[0]) } query.finalize() return tables } export function tableColumnName(table: string): Array { const query = db.query(`PRAGMA table_info(${table})`) var columnName = new Array() for (const column of query.values()) { columnName.push(column[1]) } return columnName } export namespace BoardTable { export function add(board: Board): Result { if (!isBoard(board)) { return new Err(BOARD_ERR_WRONG_TYPE) } // Ensure no id conflict if (board.id == 0) { const cnt = countAll() board.id = cnt + 1 } else { const retID = includes(board.id) if (retID.isOk()) { if (retID.value) return new Err(BOARD_ERR_ID_CONFLICT) } else { return new Err(BOARD_ERR_WRONG_TYPE) } } // Ensure no name conflict in the same room { const retName = includes(board.name, board.room) if (retName.isOk()) { if (retName.value) { return new Err(BOARD_ERR_NAME_CONFLICT) } } else { return new Err("Wrong type") } } const query = db.query(` INSERT INTO Boards VALUES (${board.id}, '${board.name}', '${board.room}', '${board.ipv4}', '${_.isUndefined(board.ipv6) ? "NULL" : board.ipv6}', ${board.port}); `) return Ok(query.run()) } export function addFromArray(array: Array) : Result, BoardErrorType> { let arrayChanges: Array = [] for (const item of array) { const ret = add(item) if (ret.isErr()) { return ret } else { arrayChanges.push(ret.value) } } return new Ok(arrayChanges) } export function all(): Option> { const query = db.query(`SELECT * FROM Boards`) const ret = query.all() query.finalize() if (isBoardArray(ret)) { return Some(ret) } else { return None } } export function countAll(): number { const query = db.query(`SELECT COUNT(*) FROM Boards`) return query.values()[0][0] as number } export function countByName(name: string): number { const query = db.query(`SELECT * FROM Boards WHERE name=${name}`) return query.values()[0][0] as number } export function countByRoom(room: string): number { const query = db.query(`SELECT * FROM Boards WHERE room=${room}`) return query.values()[0][0] as number } export function remove(name: string, room: string): Result export function remove(id: number): Result export function remove(board: Board): Result export function remove(arg1: any, arg2?: any): Result { let retBoard let condition: string if (isBoard(arg1)) { retBoard = _.cloneDeep(arg1) condition = `id=${arg1.id}` } else if (_.isNumber(arg1)) { retBoard = find(arg1) if (retBoard.isOk() && retBoard.value.isSome()) { retBoard = _.cloneDeep(retBoard.value.value) condition = `id=${arg1}` } else { return new Err(BOARD_ERR_WRONG_TYPE) } } else if (_.isString(arg1) && _.isString(arg2)) { retBoard = find(arg1, arg2) if (retBoard.isOk() && retBoard.value.isSome()) { retBoard = _.cloneDeep(retBoard.value.value) condition = `name=${arg1}` } else { return new Err(BOARD_ERR_NO_BOARDS) } } else { return new Err(BOARD_ERR_WRONG_TYPE) } const query = db.query(`DELETE FROM Boards WHERE ${condition}`) query.run() return new Ok(retBoard) } export function removeAll(): Option> { const array = all() const query = db.query(`DELETE FROM Boards`) query.run() if (array.isSome()) { return new Some(array.value) } else { return None } } export function removeByCondition(condition: string): Result, BoardErrorType> { const rsArr = findByCondition(condition) if (rsArr.isNone()) { return new Err(BOARD_ERR_NO_BOARDS) } const query = db.query(`DELETE FROM Boards WHERE ${condition}`) query.run() return new Ok(rsArr.value) } export function removeByValue(columnName: string, val: string | Array) : Result, BoardErrorType> { if (!isBoardColumn(columnName)) { return new Err(BOARD_ERR_WRONG_TYPE) } let condition: string if (_.isString(val)) { const retCond = fun.sqlConditionFromString(columnName, val, "OR") if (retCond.isSome()) { condition = retCond.value } else { return new Err(BOARD_ERR_WRONG_TYPE) } } else if (fun.isStringArray(val)) { const retCond = fun.sqlConditionFromArray(columnName, val, "OR") if (retCond.isSome()) { condition = retCond.value } else { return new Err(BOARD_ERR_WRONG_TYPE) } } else { return new Err(BOARD_ERR_WRONG_TYPE) } return removeByCondition(condition) } export function removeByName(name: string | Array) : Result, BoardErrorType> { return removeByValue("name", name) } export function removeByRoom(room: string | Array) : Result, BoardErrorType> { return removeByValue("room", room) } export function rooms(): Option> { const query = db.query(`SELECT DISTINCT room FROM Boards`) let rooms: Array = [] const retVal = query.values() if (retVal.length > 0) { for (const item of retVal) { rooms.push(item[0] as string) } return new Some(rooms) } else { return None } } export function find(name: string, room: string): Result, BoardErrorType> export function find(id: number): Result, BoardErrorType> export function find(arg1: any, arg2?: any): Result, BoardErrorType> { let condition: string if (_.isNumber(arg1)) { condition = `id=${arg1}` } else if (_.isString(arg1) && _.isString(arg2)) { condition = `name='${arg1}' AND room='${arg2}'` } else { return new Err(BOARD_ERR_WRONG_TYPE) } const query = db.query(`SELECT * FROM Boards WHERE ${condition}`) const spRet = boardSchema.safeParse(query.get()) if (spRet.success) { return new Ok(Some(spRet.data)) } else { return new Ok(None) } } export function findByName(name: string | Array): Result>, BoardErrorType> { let condition: string if (_.isString(name)) { const retCond = fun.sqlConditionFromString("name", name, "OR") if (retCond.isSome()) { condition = retCond.value } else { return new Err(BOARD_ERR_WRONG_TYPE) } } else if (fun.isStringArray(name)) { const retCond = fun.sqlConditionFromArray("name", name, "OR") if (retCond.isSome()) { condition = retCond.value } else { return new Err(BOARD_ERR_WRONG_TYPE) } } else { return new Err(BOARD_ERR_WRONG_TYPE) } return new Ok(findByCondition(condition)) } export function findByCondition(condition: string): Option> { const query = db.query(`SELECT * FROM Boards WHERE ${condition}`) const ret = query.all() if (isBoardArray(ret)) { return new Some(ret) } else { return None } } export function findByValue(columnName: string, val: string | Array) : Result>, BoardErrorType> { if (!isBoardColumn(columnName)) { return new Err(BOARD_ERR_WRONG_TYPE) } let condition: string if (_.isString(val)) { const retCond = fun.sqlConditionFromString(columnName, val, "OR") if (retCond.isSome()) { condition = retCond.value } else { return new Err(BOARD_ERR_WRONG_TYPE) } } else if (fun.isStringArray(val)) { const retCond = fun.sqlConditionFromArray(columnName, val, "OR") if (retCond.isSome()) { condition = retCond.value } else { return new Err(BOARD_ERR_WRONG_TYPE) } } else { return new Err(BOARD_ERR_WRONG_TYPE) } return new Ok(findByCondition(condition)) } export function includes(name: string, room?: string): Result export function includes(id: number): Result export function includes(arg1: any, arg2?: any): Result { let condition: string if (_.isUndefined(arg2)) { if (_.isNumber(arg1)) { condition = `id=${arg1}` } else if (_.isString(arg1)) { condition = `name='${arg1}'` } else { return new Err(BOARD_ERR_WRONG_TYPE) } } else { if (_.isString(arg1) && _.isString(arg2)) { condition = `name='${arg1} AND room=${arg2}'` } else { return new Err(BOARD_ERR_WRONG_TYPE) } } const query = db.query(`SELECT COUNT(*) FROM Boards WHERE ${condition}`) const num = query.values()[0][0] as number return new Ok(num > 0 ? true : false) } } export namespace UserTable { export function countAll(): number { const query = db.query(`SELECT COUNT(*) FROM Users`) return query.values()[0][0] as number } export function find(id: number): Result, "Wrong Type"> export function find(name: string): Result, "Wrong Type"> export function find(arg: any): Result, "Wrong Type"> { let condition: string if (_.isNumber(arg)) { condition = `id=${arg}` } else if (_.isString(arg)) { condition = `name=${arg}` } else { return new Err("Wrong Type") } const query = db.query(`SELECT * FROM Users WHERE name='${arg}'`) const spRet = userSchema.safeParse(query.get()) if (spRet.success) { return new Ok(Some(spRet.data)) } else { return new Ok(None) } } }