From 4fb4c08044f39af2eaeda28ae9c2152cfbea0b60 Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Tue, 25 Mar 2025 21:16:33 +0800 Subject: [PATCH] add error type and more functions in database --- flake.nix | 2 +- server/common.ts | 43 ++++++++ server/database.test.ts | 31 ++++-- server/database.ts | 235 +++++++++++++++++++++++++++++++++++----- 4 files changed, 270 insertions(+), 41 deletions(-) diff --git a/flake.nix b/flake.nix index e8fe474..0446250 100644 --- a/flake.nix +++ b/flake.nix @@ -17,7 +17,7 @@ bun sqlite sqls - dbeaver-bin + sql-studio # LSP typescript-language-server diff --git a/server/common.ts b/server/common.ts index 3426ab6..e9507b5 100644 --- a/server/common.ts +++ b/server/common.ts @@ -1,4 +1,5 @@ import _ from "lodash" +import { Option, Some, None } from "ts-results-es"; import { z } from "zod"; export const uint8 = z.number().nonnegative().lt(Math.pow(2, 8)) @@ -23,6 +24,48 @@ export namespace fun { export function randomFromArray(array: Array) { return array[_.random(0, array.length - 1, false)] } + + export function isStringArray(obj: any): obj is Array { + return z.string().array().safeParse(obj).success + } + + export function isNumberArray(obj: any): obj is Array { + return z.number().array().safeParse(obj).success + } + + export function sqlConditionFromArray( + columnName: string, + array: Array, + type: "AND" | "OR" + ): Option { + let condition: string = "" + const len = array.length + if (len == 0) { + return None + } + for (let i = 0; i < len; i++) { + condition.concat(`${columnName}=${array[i]}`) + if (i != len - 1) { + condition.concat(` ${type} `) + } + } + + return new Some(condition) + } + + export function sqlConditionFromString( + columnName: string, + str: string, + type: "AND" | "OR", + delimiter: string = " " + ): Option { + if (str.length == 0) { + return None + } + + const array = str.split(delimiter) + return sqlConditionFromArray(columnName, array, type) + } } diff --git a/server/database.test.ts b/server/database.test.ts index 183e57c..1e85dc1 100644 --- a/server/database.test.ts +++ b/server/database.test.ts @@ -1,8 +1,9 @@ import { test, expect } from "bun:test" import * as db from "./database.ts" -import _ from "lodash" +import _, { rearg } from "lodash" import { None, Ok, Option, Some } from "ts-results-es" +// Test basic function for database test("DataBase", () => { const allTables = db.allTables() expect(allTables).toBeArray() @@ -11,17 +12,17 @@ test("DataBase", () => { expect(db.UserTable.countAll()).toBe(0) }) -test("Board Table", () => { - const boardsNumber = 10 - const rooms = ["A1", "A1", "A1", "A1", "A1", "A2", "A2", "A2", "A2", "A2"] - - // Try to find something empty +// Test Boards table function +const boardsNumber = 10 +const rooms = ["A1", "A1", "A1", "A1", "A1", "A2", "A2", "A2", "A2", "A2"] +test("Find something empty", () => { const findEmptyByID = db.BoardTable.find(_.random(0, boardsNumber)) expect(findEmptyByID).toEqual(Ok(None)) const findEmptyByName = db.BoardTable.find("Hello", "World") expect(findEmptyByName).toEqual(Ok(None)) +}) - +test("Add some boards", () => { const boardsArray: Array = [] for (let i = 0; i < boardsNumber; i++) { boardsArray.push({ @@ -32,9 +33,16 @@ test("Board Table", () => { port: i, }) } - db.BoardTable.addFromArray(boardsArray) + const retAdd = db.BoardTable.addFromArray(boardsArray) + const isAddOk = retAdd.isOk() + expect(isAddOk).toBeTrue() +}) - // Test Find +test("Get boards table column unique element", () => { + expect(db.BoardTable.rooms().unwrap()).toEqual(["A1", "A2"]) +}) + +test("Find something from boards table", () => { expect(db.BoardTable.find(1)).toEqual(Ok(Some({ id: 1, name: `Board ${1}`, @@ -49,10 +57,9 @@ test("Board Table", () => { ipv4: `192.168.172.${3}`, port: 3, } as db.Board))) +}) - // Test Count +test("Get count of elements from boards table", () => { expect(db.BoardTable.countByName("Board 1")).toBe(1) expect(db.BoardTable.countByRoom("A1")).toBe(5) - - }) diff --git a/server/database.ts b/server/database.ts index e3f022f..d6b1134 100644 --- a/server/database.ts +++ b/server/database.ts @@ -2,10 +2,23 @@ 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(), @@ -21,11 +34,20 @@ const boardSchema = z.object({ }) 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(), @@ -36,11 +58,19 @@ const userSchema = z.object({ }) 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() @@ -93,9 +123,9 @@ export function tableColumnName(table: string): Array { export namespace BoardTable { - export function add(board: Board): Result { + export function add(board: Board): Result { if (!isBoard(board)) { - return new Err("Wrong type") + return new Err(BOARD_ERR_WRONG_TYPE) } // Ensure no id conflict @@ -105,9 +135,9 @@ export namespace BoardTable { } else { const retID = includes(board.id) if (retID.isOk()) { - if (retID.value) return new Err("ID conflict") + if (retID.value) return new Err(BOARD_ERR_ID_CONFLICT) } else { - return new Err("Wrong type") + return new Err(BOARD_ERR_WRONG_TYPE) } } @@ -116,7 +146,7 @@ export namespace BoardTable { const retName = includes(board.name, board.room) if (retName.isOk()) { if (retName.value) { - return new Err("Name conflict in one room") + return new Err(BOARD_ERR_NAME_CONFLICT) } } else { return new Err("Wrong type") @@ -126,7 +156,7 @@ export namespace BoardTable { const query = db.query(` INSERT INTO Boards VALUES (${board.id}, - '${board.name}, + '${board.name}', '${board.room}', '${board.ipv4}', '${_.isUndefined(board.ipv6) ? "NULL" : board.ipv6}', @@ -136,12 +166,14 @@ export namespace BoardTable { return Ok(query.run()) } - export function addFromArray(array: Array): Result, "Wrong type"> { + export function addFromArray(array: Array) + : Result, BoardErrorType> { + let arrayChanges: Array = [] for (const item of array) { const ret = add(item) if (ret.isErr()) { - return new Err("Wrong type") + return ret } else { arrayChanges.push(ret.value) } @@ -150,12 +182,16 @@ export namespace BoardTable { return new Ok(arrayChanges) } - export function all() { + export function all(): Option> { const query = db.query(`SELECT * FROM Boards`) - const boards = query.all() - + const ret = query.all() query.finalize() - return boards + + if (isBoardArray(ret)) { + return Some(ret) + } else { + return None + } } export function countAll(): number { @@ -173,10 +209,10 @@ export namespace BoardTable { 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 { + 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)) { @@ -189,7 +225,7 @@ export namespace BoardTable { retBoard = _.cloneDeep(retBoard.value.value) condition = `id=${arg1}` } else { - return new Err("No such Board") + return new Err(BOARD_ERR_WRONG_TYPE) } } else if (_.isString(arg1) && _.isString(arg2)) { @@ -198,11 +234,11 @@ export namespace BoardTable { retBoard = _.cloneDeep(retBoard.value.value) condition = `name=${arg1}` } else { - return new Err("No such Board") + return new Err(BOARD_ERR_NO_BOARDS) } } else { - return new Err("Wrong Type") + return new Err(BOARD_ERR_WRONG_TYPE) } const query = db.query(`DELETE FROM Boards WHERE ${condition}`) @@ -211,9 +247,90 @@ export namespace BoardTable { return new Ok(retBoard) } - export function find(name: string, room: string): Result, "Wrong type"> - export function find(id: number): Result, "Wrong type"> - export function find(arg1: any, arg2?: any): Result, "Wrong type"> { + 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}` @@ -222,7 +339,7 @@ export namespace BoardTable { condition = `name='${arg1}' AND room='${arg2}'` } else { - return new Err("Wrong type") + return new Err(BOARD_ERR_WRONG_TYPE) } const query = db.query(`SELECT * FROM Boards WHERE ${condition}`) @@ -236,9 +353,71 @@ export namespace BoardTable { } } - export function includes(name: string, room?: string): Result - export function includes(id: number): Result - export function includes(arg1: any, arg2?: any): Result { + 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(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 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)) { @@ -248,13 +427,13 @@ export namespace BoardTable { condition = `name='${arg1}'` } else { - return new Err("Wrong type") + return new Err(BOARD_ERR_WRONG_TYPE) } } else { if (_.isString(arg1) && _.isString(arg2)) { condition = `name='${arg1} AND room=${arg2}'` } else { - return new Err("Wrong type") + return new Err(BOARD_ERR_WRONG_TYPE) } }