Files
file-one/packages/convex/model/directories.ts

235 lines
5.3 KiB
TypeScript
Raw Normal View History

2025-09-16 23:17:01 +00:00
import type { Doc, Id } from "@fileone/convex/_generated/dataModel"
2025-09-16 22:36:26 +00:00
import type {
AuthenticatedMutationCtx,
AuthenticatedQueryCtx,
} from "../functions"
import * as Err from "./error"
import type { DirectoryHandle, FilePath, ReverseFilePath } from "./filesystem"
import { newDirectoryHandle } from "./filesystem"
2025-09-13 22:02:27 +01:00
type Directory = {
kind: "directory"
doc: Doc<"directories">
}
type File = {
kind: "file"
doc: Doc<"files">
}
export type DirectoryItem = Directory | File
export type DirectoryItemKind = DirectoryItem["kind"]
2025-09-13 22:02:27 +01:00
export type DirectoryInfo = Doc<"directories"> & { path: FilePath }
export async function fetchRoot(ctx: AuthenticatedQueryCtx) {
return await ctx.db
.query("directories")
.withIndex("byParentId", (q) =>
q.eq("userId", ctx.user._id).eq("parentId", undefined),
)
.first()
}
export async function fetchHandle(
ctx: AuthenticatedQueryCtx,
handle: DirectoryHandle,
): Promise<Doc<"directories">> {
const directory = await ctx.db.get(handle.id)
if (!directory || directory.userId !== ctx.user._id) {
throw Err.create(
Err.Code.DirectoryNotFound,
`Directory ${handle.id} not found`,
)
}
return directory
}
export async function fetch(
ctx: AuthenticatedQueryCtx,
{ directoryId }: { directoryId: Id<"directories"> },
): Promise<DirectoryInfo> {
const directory = await ctx.db.get(directoryId)
if (!directory) {
throw Err.create(
Err.Code.DirectoryNotFound,
`Directory ${directoryId} not found`,
)
}
const path: ReverseFilePath = [
{
handle: newDirectoryHandle(directoryId),
name: directory.name,
},
]
let parentDirId = directory.parentId
while (parentDirId) {
const parentDir = await ctx.db.get(parentDirId)
if (parentDir) {
path.push({
handle: newDirectoryHandle(parentDir._id),
name: parentDir.name,
})
parentDirId = parentDir.parentId
} else {
throw Err.create(Err.Code.Internal)
}
}
return { ...directory, path: path.reverse() as FilePath }
}
2025-09-13 22:02:27 +01:00
export async function fetchContent(
2025-09-16 22:36:26 +00:00
ctx: AuthenticatedQueryCtx,
{ directoryId }: { directoryId?: Id<"directories"> } = {},
2025-09-13 22:02:27 +01:00
): Promise<DirectoryItem[]> {
2025-09-17 00:04:12 +00:00
let dirId: Id<"directories"> | undefined
if (directoryId) {
2025-09-17 00:04:12 +00:00
dirId = directoryId
}
2025-09-13 22:02:27 +01:00
const [files, directories] = await Promise.all([
ctx.db
.query("files")
.withIndex("byDirectoryId", (q) =>
2025-09-16 22:36:26 +00:00
q
.eq("userId", ctx.user._id)
2025-09-17 00:04:12 +00:00
.eq("directoryId", dirId)
2025-09-16 22:36:26 +00:00
.eq("deletedAt", undefined),
)
2025-09-13 22:02:27 +01:00
.collect(),
ctx.db
.query("directories")
2025-09-16 22:36:26 +00:00
.withIndex("byParentId", (q) =>
q
.eq("userId", ctx.user._id)
2025-09-17 00:04:12 +00:00
.eq("parentId", dirId)
2025-09-16 22:36:26 +00:00
.eq("deletedAt", undefined),
)
2025-09-13 22:02:27 +01:00
.collect(),
])
const items: DirectoryItem[] = []
for (const directory of directories) {
items.push({ kind: "directory", doc: directory })
}
for (const file of files) {
items.push({ kind: "file", doc: file })
}
return items
}
export async function create(
2025-09-16 22:36:26 +00:00
ctx: AuthenticatedMutationCtx,
{ name, parentId }: { name: string; parentId: Id<"directories"> },
2025-09-13 22:02:27 +01:00
): Promise<Id<"directories">> {
const parentDir = await ctx.db.get(parentId)
if (!parentDir) {
throw Err.create(
Err.Code.DirectoryNotFound,
`Parent directory ${parentId} not found`,
)
2025-09-15 21:44:41 +00:00
}
const existing = await ctx.db
.query("directories")
.withIndex("uniqueDirectoryInDirectory", (q) =>
2025-09-16 22:36:26 +00:00
q
.eq("userId", ctx.user._id)
.eq("parentId", parentId)
.eq("name", name)
.eq("deletedAt", undefined),
)
.first()
if (existing) {
throw Err.create(
Err.Code.DirectoryExists,
`Directory with name ${name} already exists in ${parentId ? `directory ${parentId}` : "root"}`,
)
}
2025-09-13 22:02:27 +01:00
const now = new Date().toISOString()
return await ctx.db.insert("directories", {
name,
parentId,
2025-09-16 22:36:26 +00:00
userId: ctx.user._id,
2025-09-13 22:02:27 +01:00
createdAt: now,
updatedAt: now,
})
}
export async function move(
ctx: AuthenticatedMutationCtx,
{
targetDirectory,
sourceDirectories,
}: {
targetDirectory: DirectoryHandle
sourceDirectories: DirectoryHandle[]
},
): Promise<void> {
await Promise.all(
sourceDirectories.map((directory) =>
ctx.db.patch(directory.id, { parentId: targetDirectory.id }),
),
)
}
export async function moveToTrashRecursive(
2025-09-16 22:36:26 +00:00
ctx: AuthenticatedMutationCtx,
directoryId: Id<"directories">,
): Promise<void> {
const now = new Date().toISOString()
const filesToDelete: Id<"files">[] = []
const directoriesToDelete: Id<"directories">[] = []
const directoryQueue: Id<"directories">[] = [directoryId]
while (directoryQueue.length > 0) {
const currentDirectoryId = directoryQueue.shift()!
directoriesToDelete.push(currentDirectoryId)
const files = await ctx.db
.query("files")
.withIndex("byDirectoryId", (q) =>
q
2025-09-16 22:36:26 +00:00
.eq("userId", ctx.user._id)
.eq("directoryId", currentDirectoryId)
.eq("deletedAt", undefined),
)
.collect()
for (const file of files) {
filesToDelete.push(file._id)
}
const subdirectories = await ctx.db
.query("directories")
.withIndex("byParentId", (q) =>
2025-09-16 22:36:26 +00:00
q
.eq("userId", ctx.user._id)
.eq("parentId", currentDirectoryId)
.eq("deletedAt", undefined),
)
.collect()
for (const subdirectory of subdirectories) {
directoryQueue.push(subdirectory._id)
}
}
const filePatches = filesToDelete.map((fileId) =>
ctx.db.patch(fileId, { deletedAt: now }),
)
const directoryPatches = directoriesToDelete.map((dirId) =>
ctx.db.patch(dirId, { deletedAt: now }),
)
await Promise.all([...filePatches, ...directoryPatches])
}