refactor: migrate to vite and restructure repo

Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
2025-10-18 14:02:20 +00:00
parent 83a5f92506
commit 25796ab609
94 changed files with 478 additions and 312 deletions

View File

@@ -20,10 +20,12 @@ import type * as filesystem from "../filesystem.js";
import type * as functions from "../functions.js";
import type * as http from "../http.js";
import type * as model_directories from "../model/directories.js";
import type * as model_error from "../model/error.js";
import type * as model_files from "../model/files.js";
import type * as model_filesystem from "../model/filesystem.js";
import type * as model_user from "../model/user.js";
import type * as shared_error from "../shared/error.js";
import type * as shared_filesystem from "../shared/filesystem.js";
import type * as shared_types from "../shared/types.js";
import type * as user from "../user.js";
import type {
@@ -53,10 +55,12 @@ declare const fullApi: ApiFromModules<{
functions: typeof functions;
http: typeof http;
"model/directories": typeof model_directories;
"model/error": typeof model_error;
"model/files": typeof model_files;
"model/filesystem": typeof model_filesystem;
"model/user": typeof model_user;
"shared/error": typeof shared_error;
"shared/filesystem": typeof shared_filesystem;
"shared/types": typeof shared_types;
user: typeof user;
}>;
declare const fullApiWithMounts: typeof fullApi;

View File

@@ -3,7 +3,7 @@ import type { Id } from "./_generated/dataModel"
import { authenticatedMutation, authenticatedQuery, authorizedGet } from "./functions"
import * as Directories from "./model/directories"
import * as Files from "./model/files"
import type { FileSystemItem } from "./model/filesystem"
import type { FileSystemItem } from "./shared/filesystem"
export const generateUploadUrl = authenticatedMutation({
handler: async (ctx) => {

View File

@@ -1,20 +1,27 @@
import { v } from "convex/values"
import { authenticatedMutation, authenticatedQuery, authorizedGet } from "./functions"
import * as Directories from "./model/directories"
import * as Err from "./model/error"
import * as Files from "./model/files"
import type {
DirectoryHandle,
FileHandle,
FileSystemItem,
} from "./model/filesystem"
import * as FileSystem from "./model/filesystem"
import {
type FileSystemHandle,
FileType,
authenticatedMutation,
authenticatedQuery,
authorizedGet,
} from "./functions"
import * as Directories from "./model/directories"
import * as Files from "./model/files"
import {
deleteItemsPermanently,
emptyTrash as emptyTrashImpl,
fetchFileUrl as fetchFileUrlImpl,
restoreItems as restoreItemsImpl,
VDirectoryHandle,
VFileSystemHandle,
} from "./model/filesystem"
import * as Err from "./shared/error"
import type {
DirectoryHandle,
FileHandle,
FileSystemHandle,
FileSystemItem,
} from "./shared/filesystem"
import { FileType } from "./shared/filesystem"
export const moveItems = authenticatedMutation({
args: {
@@ -22,7 +29,10 @@ export const moveItems = authenticatedMutation({
items: v.array(VFileSystemHandle),
},
handler: async (ctx, { targetDirectory: targetDirectoryHandle, items }) => {
const targetDirectory = await authorizedGet(ctx, targetDirectoryHandle.id)
const targetDirectory = await authorizedGet(
ctx,
targetDirectoryHandle.id,
)
if (!targetDirectory) {
throw Err.create(
Err.Code.DirectoryNotFound,
@@ -131,13 +141,13 @@ export const permanentlyDeleteItems = authenticatedMutation({
handles: v.array(VFileSystemHandle),
},
handler: async (ctx, { handles }) => {
return await FileSystem.deleteItemsPermanently(ctx, { handles })
return await deleteItemsPermanently(ctx, { handles })
},
})
export const emptyTrash = authenticatedMutation({
handler: async (ctx) => {
return await FileSystem.emptyTrash(ctx)
return await emptyTrashImpl(ctx)
},
})
@@ -146,7 +156,7 @@ export const restoreItems = authenticatedMutation({
handles: v.array(VFileSystemHandle),
},
handler: async (ctx, { handles }) => {
return await FileSystem.restoreItems(ctx, { handles })
return await restoreItemsImpl(ctx, { handles })
},
})
@@ -155,11 +165,6 @@ export const fetchFileUrl = authenticatedQuery({
fileId: v.id("files"),
},
handler: async (ctx, { fileId }) => {
const file = await authorizedGet(ctx, fileId)
if (!file) {
throw Err.create(Err.Code.NotFound, "File not found")
}
return await FileSystem.fetchFileUrl(ctx, { fileId })
return await fetchFileUrlImpl(ctx, { fileId })
},
})

View File

@@ -1,17 +1,17 @@
import type { Doc, Id } from "@fileone/convex/_generated/dataModel"
import type { Doc, Id } from "../_generated/dataModel"
import type {
AuthenticatedMutationCtx,
AuthenticatedQueryCtx,
} from "../functions"
import { authorizedGet } from "../functions"
import * as Err from "./error"
import * as Err from "../shared/error"
import {
type DirectoryHandle,
type DirectoryPath,
type FileSystemItem,
FileType,
newDirectoryHandle,
} from "./filesystem"
} from "../shared/filesystem"
export type DirectoryInfo = Doc<"directories"> & { path: DirectoryPath }

View File

@@ -1,7 +1,7 @@
import type { Doc, Id } from "../_generated/dataModel"
import { type AuthenticatedMutationCtx, authorizedGet } from "../functions"
import * as Err from "./error"
import type { DirectoryHandle, FileHandle } from "./filesystem"
import * as Err from "../shared/error"
import type { DirectoryHandle, FileHandle } from "../shared/filesystem"
export async function renameFile(
ctx: AuthenticatedMutationCtx,

View File

@@ -1,89 +1,24 @@
import { v } from "convex/values"
import type { Doc, Id } from "../_generated/dataModel"
import type {
AuthenticatedMutationCtx,
AuthenticatedQueryCtx,
import {
type AuthenticatedMutationCtx,
type AuthenticatedQueryCtx,
authorizedGet,
} from "../functions"
import { authorizedGet } from "../functions"
import * as Err from "../shared/error"
import type {
DirectoryHandle,
FileHandle,
FileSystemHandle,
} from "../shared/filesystem"
import {
FileType,
newDirectoryHandle,
newFileHandle,
} from "../shared/filesystem"
import * as Directories from "./directories"
import * as Err from "./error"
import * as Files from "./files"
export enum FileType {
File = "File",
Directory = "Directory",
}
export type Directory = {
kind: FileType.Directory
doc: Doc<"directories">
}
export type File = {
kind: FileType.File
doc: Doc<"files">
}
export type FileSystemItem = Directory | File
export type DirectoryPathComponent = {
handle: DirectoryHandle
name: string
}
export type FilePathComponent = {
handle: FileHandle
name: string
}
export type PathComponent = FilePathComponent | DirectoryPathComponent
export type DirectoryPath = [
DirectoryPathComponent,
...DirectoryPathComponent[],
]
export type FilePath = [...DirectoryPathComponent[], PathComponent]
export type ReverseFilePath = [PathComponent, ...DirectoryPathComponent[]]
export type DirectoryHandle = {
kind: FileType.Directory
id: Id<"directories">
}
export type FileHandle = {
kind: FileType.File
id: Id<"files">
}
export type FileSystemHandle = DirectoryHandle | FileHandle
export type DeleteResult = {
deleted: {
files: number
directories: number
}
errors: Err.ApplicationErrorData[]
}
export function newFileSystemHandle(item: FileSystemItem): FileSystemHandle {
console.log("item", item)
switch (item.kind) {
case FileType.File:
return { kind: item.kind, id: item.doc._id }
case FileType.Directory:
return { kind: item.kind, id: item.doc._id }
}
}
export function isSameHandle(
handle1: FileSystemHandle,
handle2: FileSystemHandle,
): boolean {
return handle1.kind === handle2.kind && handle1.id === handle2.id
}
export function newDirectoryHandle(id: Id<"directories">): DirectoryHandle {
return { kind: FileType.Directory, id }
}
export function newFileHandle(id: Id<"files">): FileHandle {
return { kind: FileType.File, id }
}
export const VDirectoryHandle = v.object({
kind: v.literal(FileType.Directory),
id: v.id("directories"),
@@ -95,7 +30,7 @@ export const VFileHandle = v.object({
export const VFileSystemHandle = v.union(VFileHandle, VDirectoryHandle)
export async function queryRootDirectory(
ctx: AuthenticatedQueryCtx,
ctx: AuthenticatedQueryCtx | AuthenticatedMutationCtx,
): Promise<Doc<"directories"> | null> {
return await ctx.db
.query("directories")
@@ -134,24 +69,19 @@ async function collectAllHandlesRecursively(
const fileHandles: FileHandle[] = []
const directoryHandles: DirectoryHandle[] = []
// Process each handle to collect files and directories
for (const handle of handles) {
// Use a queue to process items iteratively instead of recursively
const queue: FileSystemHandle[] = [handle]
while (queue.length > 0) {
const currentHandle = queue.shift()!
// Add current item to appropriate collection
if (currentHandle.kind === FileType.File) {
fileHandles.push(currentHandle)
} else {
directoryHandles.push(currentHandle)
}
// If it's a directory, collect all children and add them to the queue
if (currentHandle.kind === FileType.Directory) {
// Get all child directories that are in trash (deletedAt >= 0)
const childDirectories = await ctx.db
.query("directories")
.withIndex("byParentId", (q) =>
@@ -162,7 +92,6 @@ async function collectAllHandlesRecursively(
)
.collect()
// Get all child files that are in trash (deletedAt >= 0)
const childFiles = await ctx.db
.query("files")
.withIndex("byDirectoryId", (q) =>
@@ -173,16 +102,12 @@ async function collectAllHandlesRecursively(
)
.collect()
// Add child directories to queue for processing
for (const childDir of childDirectories) {
const childHandle = newDirectoryHandle(childDir._id)
queue.push(childHandle)
queue.push(newDirectoryHandle(childDir._id))
}
// Add child files to file handles collection
for (const childFile of childFiles) {
const childFileHandle = newFileHandle(childFile._id)
fileHandles.push(childFileHandle)
fileHandles.push(newFileHandle(childFile._id))
}
}
}
@@ -199,17 +124,14 @@ export async function restoreItems(
ctx: AuthenticatedMutationCtx,
{ handles }: { handles: FileSystemHandle[] },
) {
// Collect all items to restore (including nested items)
const { fileHandles, directoryHandles } =
await collectAllHandlesRecursively(ctx, { handles })
// Restore files and directories by unsetting deletedAt
const [filesResult, directoriesResult] = await Promise.all([
Files.restore(ctx, { items: fileHandles }),
Directories.restore(ctx, { items: directoryHandles }),
])
// Combine results, handling null responses
return {
restored: {
files: filesResult?.restored || 0,
@@ -225,20 +147,15 @@ export async function restoreItems(
export async function deleteItemsPermanently(
ctx: AuthenticatedMutationCtx,
{ handles }: { handles: FileSystemHandle[] },
): Promise<DeleteResult> {
// Collect all items to delete (including nested items)
const {
fileHandles: fileHandlesToDelete,
directoryHandles: directoryHandlesToDelete,
} = await collectAllHandlesRecursively(ctx, { handles })
) {
const { fileHandles, directoryHandles } =
await collectAllHandlesRecursively(ctx, { handles })
// Delete files and directories using their respective models
const [filesResult, directoriesResult] = await Promise.all([
Files.deletePermanently(ctx, { items: fileHandlesToDelete }),
Directories.deletePermanently(ctx, { items: directoryHandlesToDelete }),
Files.deletePermanently(ctx, { items: fileHandles }),
Directories.deletePermanently(ctx, { items: directoryHandles }),
])
// Combine results, handling null responses
return {
deleted: {
files: filesResult?.deleted || 0,
@@ -251,9 +168,7 @@ export async function deleteItemsPermanently(
}
}
export async function emptyTrash(
ctx: AuthenticatedMutationCtx,
): Promise<DeleteResult> {
export async function emptyTrash(ctx: AuthenticatedMutationCtx) {
const rootDir = await queryRootDirectory(ctx)
if (!rootDir) {
throw Err.create(Err.Code.NotFound, "user root directory not found")

View File

@@ -1,6 +1,6 @@
import type { MutationCtx, QueryCtx } from "../_generated/server"
import { authComponent } from "../auth"
import * as Err from "./error"
import * as Err from "../shared/error"
export type AuthUser = Awaited<ReturnType<typeof authComponent.getAuthUser>>

View File

@@ -2,6 +2,14 @@
"name": "@fileone/convex",
"module": "index.ts",
"type": "module",
"exports": {
"./filesystem": "./shared/filesystem.ts",
"./error": "./shared/error.ts",
"./types": "./shared/types.ts",
"./_generated/*": "./_generated/*",
"./model/*": "./model/*",
"./shared/*": "./shared/*"
},
"dependencies": {
"@fileone/path": "workspace:*"
},
@@ -9,6 +17,7 @@
"typescript": "^5",
"better-auth": "1.3.8",
"convex": "^1.27.0",
"convex-helpers": "^0.1.104",
"@convex-dev/better-auth": "^0.8.9"
}
}

View File

@@ -0,0 +1,91 @@
/**
* Client-safe filesystem types and utilities.
* This file contains types and pure functions that can be safely imported by frontend code.
* NO server-only dependencies should be imported here.
*/
import type { Doc, Id } from "../_generated/dataModel"
import type * as Err from "./error"
export enum FileType {
File = "File",
Directory = "Directory",
}
export type Directory = {
kind: FileType.Directory
doc: Doc<"directories">
}
export type File = {
kind: FileType.File
doc: Doc<"files">
}
export type FileSystemItem = Directory | File
export type DirectoryPathComponent = {
handle: DirectoryHandle
name: string
}
export type FilePathComponent = {
handle: FileHandle
name: string
}
export type PathComponent = FilePathComponent | DirectoryPathComponent
export type DirectoryPath = [
DirectoryPathComponent,
...DirectoryPathComponent[],
]
export type FilePath = [...DirectoryPathComponent[], PathComponent]
export type ReverseFilePath = [PathComponent, ...DirectoryPathComponent[]]
export type DirectoryHandle = {
kind: FileType.Directory
id: Id<"directories">
}
export type FileHandle = {
kind: FileType.File
id: Id<"files">
}
export type FileSystemHandle = DirectoryHandle | FileHandle
export type DeleteResult = {
deleted: {
files: number
directories: number
}
errors: Err.ApplicationErrorData[]
}
export function newFileSystemHandle(item: FileSystemItem): FileSystemHandle {
console.log("item", item)
switch (item.kind) {
case FileType.File:
return { kind: item.kind, id: item.doc._id }
case FileType.Directory:
return { kind: item.kind, id: item.doc._id }
}
}
export function isSameHandle(
handle1: FileSystemHandle,
handle2: FileSystemHandle,
): boolean {
return handle1.kind === handle2.kind && handle1.id === handle2.id
}
export function newDirectoryHandle(id: Id<"directories">): DirectoryHandle {
return { kind: FileType.Directory, id }
}
export function newFileHandle(id: Id<"files">): FileHandle {
return { kind: FileType.File, id }
}

View File

@@ -0,0 +1,11 @@
/**
* Shared types that can be safely imported by both client and server code.
* This file should NOT import any server-only dependencies.
*/
import type { Doc } from "../_generated/dataModel"
import type { DirectoryPath } from "./filesystem"
export type DirectoryInfo = Doc<"directories"> & { path: DirectoryPath }
export type DirectoryItem = DirectoryInfo
export type DirectoryItemKind = "directory" | "file"

View File

@@ -1,8 +1,8 @@
import { authenticatedMutation } from "./functions"
import * as FileSystem from "./model/filesystem"
import { ensureRootDirectory as ensureRootDirectoryImpl } from "./model/filesystem"
export const ensureRootDirectory = authenticatedMutation({
handler: async (ctx) => {
return await FileSystem.ensureRootDirectory(ctx)
return await ensureRootDirectoryImpl(ctx)
},
})