ecc340e7b1
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.
308 lines
8.6 KiB
TypeScript
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;
|