Files
myDrive/backend/db/sqliteDB/folderDB.ts
T
oxmc ecc340e7b1
Release Please / release-please (push) Has been cancelled
Docker Build and Push (Development) / build-and-push-dev (push) Has been cancelled
feat: add dark mode UI and SQLite as alternate database backend
Dark mode: CSS custom properties + .dark class toggled via Redux/localStorage,
with a theme toggle in the Header and Settings page. Foundation didn't exist
yet despite prior assumption, so it was built from scratch.

SQLite: new DB_ENGINE=mongo|sqlite env var selects the metadata backend at
runtime (Mongo stays default, no behavior change unless opted in). File/thumbnail
bytes continue to use the existing fs/S3 storage path. Implemented via a shared
DB interface (dbTypes.ts) with parallel mongoDB/ and sqliteDB/ implementations,
selected through dbFactory.ts, with JWT/encryption logic extracted into
userCrypto.ts so both engines share it.
2026-07-05 04:18:33 -07:00

308 lines
8.6 KiB
TypeScript

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;