import db from "../connections/sqlite"; import { ObjectId } from "mongodb"; import { IFolderDB } from "../dbTypes"; import { FolderListQueryType } from "../../types/folder-types"; const encodeParentList = (parentList: (string | any)[]): string => parentList.map((id) => id.toString()).join(","); const decodeParentList = (parentList: string): string[] => parentList ? parentList.split(",").filter((p) => p !== "") : []; const rowToFolder = (row: any) => { if (!row) return null; return { _id: row.id, name: row.name, parent: row.parent, owner: row.owner, parentList: decodeParentList(row.parentList), personalFolder: row.personalFolder ? true : undefined, trashed: row.trashed ? true : null, createdAt: new Date(row.createdAt), updatedAt: new Date(row.updatedAt), }; }; // Wraps the stored comma-joined parentList with delimiters at query time so // membership checks can't match a partial id (mirrors Mongo's array $in/$all/$nin // semantics, which match whole elements, not substrings). const containsClause = () => "(',' || parentList || ',') LIKE ?"; const containsParam = (id: string) => `%,${id},%`; const sortColumn = (sortBy: string): { column: string; direction: string } => { if (sortBy === "alp_asc") return { column: "name COLLATE NOCASE", direction: "ASC" }; if (sortBy === "alp_desc") return { column: "name COLLATE NOCASE", direction: "DESC" }; if (sortBy === "date_asc") return { column: "createdAt", direction: "ASC" }; return { column: "createdAt", direction: "DESC" }; }; class FolderDB implements IFolderDB { constructor() {} // READ getFolderSearchList = async ( userID: string, searchQuery: RegExp | string, trashMode: boolean ) => { const search = searchQuery instanceof RegExp ? searchQuery.source : searchQuery; const rows = db .prepare( `SELECT * FROM folders WHERE owner = ? AND name LIKE ? COLLATE NOCASE AND trashed = ? LIMIT 10` ) .all(userID, `%${search}%`, trashMode ? 1 : 0); return rows.map(rowToFolder); }; getFolderInfo = async (folderID: string, userID: string) => { const row = db .prepare("SELECT * FROM folders WHERE id = ? AND owner = ?") .get(folderID, userID); return rowToFolder(row); }; getFolderList = async (queryData: FolderListQueryType, sortBy: string) => { const { userID, search, parent, trashMode } = queryData; const conditions = ["owner = ?"]; const params: any[] = [userID]; if (search && search !== "") { conditions.push("name LIKE ? COLLATE NOCASE"); params.push(`%${search}%`); } else { conditions.push("parent = ?"); params.push(parent); } conditions.push("trashed = ?"); params.push(trashMode ? 1 : 0); const { column, direction } = sortColumn(sortBy); const rows = db .prepare( `SELECT * FROM folders WHERE ${conditions.join( " AND " )} ORDER BY ${column} ${direction}` ) .all(...params); return rows.map(rowToFolder); }; getMoveFolderList = async ( userID: string, parent = "/", search?: string, folderIDs?: string[] ) => { const conditions = ["owner = ?", "trashed = 0"]; const params: any[] = [userID]; if (!search || search === "") { conditions.push("parent = ?"); params.push(parent); } else { conditions.push("name LIKE ? COLLATE NOCASE"); params.push(`%${search}%`); } const rows = db .prepare( `SELECT * FROM folders WHERE ${conditions.join( " AND " )} ORDER BY createdAt DESC` ) .all(...params) as any[]; const filtered = folderIDs && folderIDs.length > 0 ? rows.filter((row) => { if (folderIDs.includes(row.id)) return false; const list = decodeParentList(row.parentList); return !folderIDs.some((id) => list.includes(id)); }) : rows; return filtered.map(rowToFolder); }; getFolderListByIncludedParent = async (userID: string, parent: string) => { const rows = db .prepare( `SELECT * FROM folders WHERE owner = ? AND ${containsClause()} AND trashed = 0` ) .all(userID, containsParam(parent)); return rows.map(rowToFolder); }; findAllFoldersByParent = async (parentID: string, userID: string) => { const rows = db .prepare(`SELECT * FROM folders WHERE owner = ? AND ${containsClause()}`) .all(userID, containsParam(parentID)); return rows.map(rowToFolder); }; // UPDATE moveFolder = async ( folderID: string, userID: string, parent: string, parentList: string[] ) => { const existing: any = db .prepare("SELECT * FROM folders WHERE id = ? AND owner = ?") .get(folderID, userID); if (!existing) return null; db.prepare( "UPDATE folders SET parent = ?, parentList = ?, updatedAt = ? WHERE id = ? AND owner = ?" ).run( parent, encodeParentList(parentList), new Date().toISOString(), folderID, userID ); return this.getFolderInfo(folderID, userID); }; renameFolder = async (folderID: string, userID: string, title: string) => { const existing: any = db .prepare("SELECT * FROM folders WHERE id = ? AND owner = ?") .get(folderID, userID); if (!existing) return null; db.prepare( "UPDATE folders SET name = ?, updatedAt = ? WHERE id = ? AND owner = ?" ).run(title, new Date().toISOString(), folderID, userID); return this.getFolderInfo(folderID, userID); }; trashFoldersByParent = async (parentList: string[], userID: string) => { const clauses = parentList.map(() => containsClause()).join(" AND "); const params = parentList.map((id) => containsParam(id)); const result = db .prepare( `UPDATE folders SET trashed = 1, updatedAt = ? WHERE owner = ? AND ${clauses}` ) .run(new Date().toISOString(), userID, ...params); return result; }; restoreFolder = async (folderID: string, userID: string) => { const existing: any = db .prepare("SELECT * FROM folders WHERE id = ? AND owner = ?") .get(folderID, userID); if (!existing) return null; db.prepare( "UPDATE folders SET trashed = 0, updatedAt = ? WHERE id = ? AND owner = ?" ).run(new Date().toISOString(), folderID, userID); return this.getFolderInfo(folderID, userID); }; restoreFoldersByParent = async (parentList: string[], userID: string) => { const clauses = parentList.map(() => containsClause()).join(" AND "); const params = parentList.map((id) => containsParam(id)); const result = db .prepare( `UPDATE folders SET trashed = 0, updatedAt = ? WHERE owner = ? AND ${clauses}` ) .run(new Date().toISOString(), userID, ...params); return result; }; trashFolder = async (folderID: string, userID: string) => { const existing: any = db .prepare("SELECT * FROM folders WHERE id = ? AND owner = ?") .get(folderID, userID); if (!existing) return null; db.prepare( "UPDATE folders SET trashed = 1, updatedAt = ? WHERE id = ? AND owner = ?" ).run(new Date().toISOString(), folderID, userID); return this.getFolderInfo(folderID, userID); }; // CREATE createFolder = async (folderData: { name: string; parent: string; parentList: string[]; owner: string; }) => { const id = new ObjectId().toString(); const now = new Date().toISOString(); db.prepare( `INSERT INTO folders (id, name, parent, owner, parentList, personalFolder, trashed, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` ).run( id, folderData.name, folderData.parent, folderData.owner, encodeParentList(folderData.parentList), 0, 0, now, now ); const row = db.prepare("SELECT * FROM folders WHERE id = ?").get(id); return rowToFolder(row); }; // DELETE deleteFolder = async (folderID: string, userID: string) => { const result = db .prepare("DELETE FROM folders WHERE id = ? AND owner = ?") .run(folderID, userID); return result; }; deleteFoldersByParentList = async ( parentList: string[], userID: string ) => { const clauses = parentList.map(() => containsClause()).join(" AND "); const params = parentList.map((id) => containsParam(id.toString())); const result = db .prepare(`DELETE FROM folders WHERE owner = ? AND ${clauses}`) .run(userID, ...params); return result; }; deleteFoldersByOwner = async (userID: string) => { const result = db .prepare("DELETE FROM folders WHERE owner = ?") .run(userID); return result; }; } export default FolderDB;