Compare commits

...

2 Commits

Author SHA1 Message Date
7cad95307b feat: record abs path of dirs 2025-09-16 22:36:26 +00:00
73857b9b1d feat: remove sidebar trigger 2025-09-16 22:16:53 +00:00
5 changed files with 60 additions and 37 deletions

View File

@@ -1,5 +1,5 @@
import type { Id } from "@convex/_generated/dataModel"
import { v } from "convex/values" import { v } from "convex/values"
import type { Id } from "./_generated/dataModel"
import { authenticatedMutation, authenticatedQuery } from "./functions" import { authenticatedMutation, authenticatedQuery } from "./functions"
import type { DirectoryItem } from "./model/directories" import type { DirectoryItem } from "./model/directories"
import * as Directories from "./model/directories" import * as Directories from "./model/directories"
@@ -18,8 +18,9 @@ export const fetchFiles = authenticatedQuery({
handler: async (ctx, { directoryId }) => { handler: async (ctx, { directoryId }) => {
return await ctx.db return await ctx.db
.query("files") .query("files")
.withIndex("byDirectoryId", (q) => q.eq("directoryId", directoryId)) .withIndex("byDirectoryId", (q) =>
.filter((q) => q.eq(q.field("userId"), ctx.user._id)) q.eq("userId", ctx.user._id).eq("directoryId", directoryId),
)
.collect() .collect()
}, },
}) })
@@ -29,7 +30,7 @@ export const fetchDirectoryContent = authenticatedQuery({
directoryId: v.optional(v.id("directories")), directoryId: v.optional(v.id("directories")),
}, },
handler: async (ctx, { directoryId }): Promise<DirectoryItem[]> => { handler: async (ctx, { directoryId }): Promise<DirectoryItem[]> => {
return await Directories.fetchContent(ctx, directoryId, ctx.user._id) return await Directories.fetchContent(ctx, directoryId)
}, },
}) })
@@ -42,7 +43,6 @@ export const createDirectory = authenticatedMutation({
return await Directories.create(ctx, { return await Directories.create(ctx, {
name, name,
parentId: directoryId, parentId: directoryId,
userId: ctx.user._id,
}) })
}, },
}) })
@@ -77,7 +77,6 @@ export const moveToTrash = authenticatedMutation({
itemId: v.union(v.id("files"), v.id("directories")), itemId: v.union(v.id("files"), v.id("directories")),
}, },
handler: async (ctx, { itemId, kind }) => { handler: async (ctx, { itemId, kind }) => {
// Verify ownership before allowing deletion
switch (kind) { switch (kind) {
case "file": { case "file": {
const file = await ctx.db.get(itemId as Id<"files">) const file = await ctx.db.get(itemId as Id<"files">)
@@ -97,7 +96,6 @@ export const moveToTrash = authenticatedMutation({
await Directories.moveToTrashRecursive( await Directories.moveToTrashRecursive(
ctx, ctx,
itemId as Id<"directories">, itemId as Id<"directories">,
ctx.user._id,
) )
break break
} }

View File

@@ -1,5 +1,8 @@
import type { Doc, Id } from "@convex/_generated/dataModel" import type { Doc, Id } from "@convex/_generated/dataModel"
import type { MutationCtx, QueryCtx } from "@convex/_generated/server" import type {
AuthenticatedMutationCtx,
AuthenticatedQueryCtx,
} from "../functions"
import * as Err from "./error" import * as Err from "./error"
type Directory = { type Directory = {
@@ -16,23 +19,27 @@ export type DirectoryItem = Directory | File
export type DirectoryItemKind = DirectoryItem["kind"] export type DirectoryItemKind = DirectoryItem["kind"]
export async function fetchContent( export async function fetchContent(
ctx: QueryCtx, ctx: AuthenticatedQueryCtx,
directoryId?: Id<"directories">, directoryId?: Id<"directories">,
userId?: Id<"users">,
): Promise<DirectoryItem[]> { ): Promise<DirectoryItem[]> {
const [files, directories] = await Promise.all([ const [files, directories] = await Promise.all([
ctx.db ctx.db
.query("files") .query("files")
.withIndex("byDirectoryId", (q) => .withIndex("byDirectoryId", (q) =>
q.eq("directoryId", directoryId).eq("deletedAt", undefined), q
.eq("userId", ctx.user._id)
.eq("directoryId", directoryId)
.eq("deletedAt", undefined),
) )
.filter((q) => userId ? q.eq(q.field("userId"), userId) : q.neq(q.field("userId"), null))
.collect(), .collect(),
ctx.db ctx.db
.query("directories") .query("directories")
.withIndex("byParentId", (q) => q.eq("parentId", directoryId)) .withIndex("byParentId", (q) =>
.filter((q) => q.eq(q.field("deletedAt"), undefined)) q
.filter((q) => userId ? q.eq(q.field("userId"), userId) : q.neq(q.field("userId"), null)) .eq("userId", ctx.user._id)
.eq("parentId", directoryId)
.eq("deletedAt", undefined),
)
.collect(), .collect(),
]) ])
@@ -48,16 +55,16 @@ export async function fetchContent(
} }
export async function create( export async function create(
ctx: MutationCtx, ctx: AuthenticatedMutationCtx,
{ name, parentId, userId }: { name: string; parentId?: Id<"directories">; userId: Id<"users"> }, { name, parentId }: { name: string; parentId?: Id<"directories"> },
): Promise<Id<"directories">> { ): Promise<Id<"directories">> {
// Check if parent directory exists and belongs to user let parentDir: Doc<"directories"> | null = null
if (parentId) { if (parentId) {
const parentDir = await ctx.db.get(parentId) parentDir = await ctx.db.get(parentId)
if (!parentDir || parentDir.userId !== userId) { if (!parentDir) {
throw Err.create( throw Err.create(
Err.Code.DirectoryExists, Err.Code.DirectoryNotFound,
"Parent directory not found or access denied", `Parent directory ${parentId} not found`,
) )
} }
} }
@@ -65,9 +72,12 @@ export async function create(
const existing = await ctx.db const existing = await ctx.db
.query("directories") .query("directories")
.withIndex("uniqueDirectoryInDirectory", (q) => .withIndex("uniqueDirectoryInDirectory", (q) =>
q.eq("parentId", parentId).eq("name", name), q
.eq("userId", ctx.user._id)
.eq("parentId", parentId)
.eq("name", name)
.eq("deletedAt", undefined),
) )
.filter((q) => q.eq(q.field("userId"), userId))
.first() .first()
if (existing) { if (existing) {
@@ -81,16 +91,16 @@ export async function create(
return await ctx.db.insert("directories", { return await ctx.db.insert("directories", {
name, name,
parentId, parentId,
userId, userId: ctx.user._id,
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
path: parentDir ? `${parentDir.path}/${name}` : "/",
}) })
} }
export async function moveToTrashRecursive( export async function moveToTrashRecursive(
ctx: MutationCtx, ctx: AuthenticatedMutationCtx,
directoryId: Id<"directories">, directoryId: Id<"directories">,
userId: Id<"users">,
): Promise<void> { ): Promise<void> {
const now = new Date().toISOString() const now = new Date().toISOString()
@@ -107,10 +117,10 @@ export async function moveToTrashRecursive(
.query("files") .query("files")
.withIndex("byDirectoryId", (q) => .withIndex("byDirectoryId", (q) =>
q q
.eq("userId", ctx.user._id)
.eq("directoryId", currentDirectoryId) .eq("directoryId", currentDirectoryId)
.eq("deletedAt", undefined), .eq("deletedAt", undefined),
) )
.filter((q) => q.eq(q.field("userId"), userId))
.collect() .collect()
for (const file of files) { for (const file of files) {
@@ -120,9 +130,11 @@ export async function moveToTrashRecursive(
const subdirectories = await ctx.db const subdirectories = await ctx.db
.query("directories") .query("directories")
.withIndex("byParentId", (q) => .withIndex("byParentId", (q) =>
q.eq("parentId", currentDirectoryId).eq("deletedAt", undefined), q
.eq("userId", ctx.user._id)
.eq("parentId", currentDirectoryId)
.eq("deletedAt", undefined),
) )
.filter((q) => q.eq(q.field("userId"), userId))
.collect() .collect()
for (const subdirectory of subdirectories) { for (const subdirectory of subdirectories) {

View File

@@ -2,6 +2,7 @@ import { ConvexError } from "convex/values"
export enum Code { export enum Code {
DirectoryExists = "DirectoryExists", DirectoryExists = "DirectoryExists",
DirectoryNotFound = "DirectoryNotFound",
FileExists = "FileExists", FileExists = "FileExists",
Internal = "Internal", Internal = "Internal",
Unauthenticated = "Unauthenticated", Unauthenticated = "Unauthenticated",

View File

@@ -16,12 +16,18 @@ const schema = defineSchema({
updatedAt: v.string(), updatedAt: v.string(),
deletedAt: v.optional(v.string()), deletedAt: v.optional(v.string()),
}) })
.index("byDirectoryId", ["directoryId", "deletedAt"]) .index("byDirectoryId", ["userId", "directoryId", "deletedAt"])
.index("byUserId", ["userId", "deletedAt"]) .index("byUserId", ["userId", "deletedAt"])
.index("byDeletedAt", ["deletedAt"]) .index("byDeletedAt", ["deletedAt"])
.index("uniqueFileInDirectory", ["directoryId", "name", "deletedAt"]), .index("uniqueFileInDirectory", [
"userId",
"directoryId",
"name",
"deletedAt",
]),
directories: defineTable({ directories: defineTable({
name: v.string(), name: v.string(),
path: v.string(),
userId: v.id("users"), userId: v.id("users"),
parentId: v.optional(v.id("directories")), parentId: v.optional(v.id("directories")),
createdAt: v.string(), createdAt: v.string(),
@@ -29,8 +35,14 @@ const schema = defineSchema({
deletedAt: v.optional(v.string()), deletedAt: v.optional(v.string()),
}) })
.index("byUserId", ["userId", "deletedAt"]) .index("byUserId", ["userId", "deletedAt"])
.index("byParentId", ["parentId", "deletedAt"]) .index("byParentId", ["userId", "parentId", "deletedAt"])
.index("uniqueDirectoryInDirectory", ["parentId", "name", "deletedAt"]), .index("uniqueDirectoryInDirectory", [
"userId",
"parentId",
"name",
"deletedAt",
])
.index("byPath", ["path", "deletedAt"]),
}) })
export default schema export default schema

View File

@@ -23,9 +23,9 @@ import {
BreadcrumbItem, BreadcrumbItem,
BreadcrumbList, BreadcrumbList,
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator,
} from "../components/ui/breadcrumb" } from "../components/ui/breadcrumb"
import { Button } from "../components/ui/button" import { Button } from "../components/ui/button"
import { SidebarTrigger } from "../components/ui/sidebar"
import { FileTable } from "./file-table" import { FileTable } from "./file-table"
import { newItemKindAtom } from "./state" import { newItemKindAtom } from "./state"
@@ -33,12 +33,12 @@ export function FilesPage() {
return ( return (
<> <>
<header className="flex py-2 shrink-0 items-center gap-2 border-b px-4 w-full"> <header className="flex py-2 shrink-0 items-center gap-2 border-b px-4 w-full">
<SidebarTrigger className="-ml-1.5" />
<Breadcrumb> <Breadcrumb>
<BreadcrumbList> <BreadcrumbList>
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbPage>Files</BreadcrumbPage> <BreadcrumbPage>All Files</BreadcrumbPage>
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbSeparator />
</BreadcrumbList> </BreadcrumbList>
</Breadcrumb> </Breadcrumb>
<div className="ml-auto flex flex-row gap-2"> <div className="ml-auto flex flex-row gap-2">