refactor: move convex into packages
This commit is contained in:
46
packages/convex/_generated/api.d.ts
vendored
Normal file
46
packages/convex/_generated/api.d.ts
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated `api` utility.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type {
|
||||
ApiFromModules,
|
||||
FilterApi,
|
||||
FunctionReference,
|
||||
} from "convex/server";
|
||||
import type * as files from "../files.js";
|
||||
import type * as functions from "../functions.js";
|
||||
import type * as model_directories from "../model/directories.js";
|
||||
import type * as model_error from "../model/error.js";
|
||||
import type * as model_user from "../model/user.js";
|
||||
import type * as users from "../users.js";
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's API.
|
||||
*
|
||||
* Usage:
|
||||
* ```js
|
||||
* const myFunctionReference = api.myModule.myFunction;
|
||||
* ```
|
||||
*/
|
||||
declare const fullApi: ApiFromModules<{
|
||||
files: typeof files;
|
||||
functions: typeof functions;
|
||||
"model/directories": typeof model_directories;
|
||||
"model/error": typeof model_error;
|
||||
"model/user": typeof model_user;
|
||||
users: typeof users;
|
||||
}>;
|
||||
export declare const api: FilterApi<
|
||||
typeof fullApi,
|
||||
FunctionReference<any, "public">
|
||||
>;
|
||||
export declare const internal: FilterApi<
|
||||
typeof fullApi,
|
||||
FunctionReference<any, "internal">
|
||||
>;
|
22
packages/convex/_generated/api.js
Normal file
22
packages/convex/_generated/api.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated `api` utility.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { anyApi } from "convex/server";
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's API.
|
||||
*
|
||||
* Usage:
|
||||
* ```js
|
||||
* const myFunctionReference = api.myModule.myFunction;
|
||||
* ```
|
||||
*/
|
||||
export const api = anyApi;
|
||||
export const internal = anyApi;
|
60
packages/convex/_generated/dataModel.d.ts
vendored
Normal file
60
packages/convex/_generated/dataModel.d.ts
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated data model types.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type {
|
||||
DataModelFromSchemaDefinition,
|
||||
DocumentByName,
|
||||
TableNamesInDataModel,
|
||||
SystemTableNames,
|
||||
} from "convex/server";
|
||||
import type { GenericId } from "convex/values";
|
||||
import schema from "../schema.js";
|
||||
|
||||
/**
|
||||
* The names of all of your Convex tables.
|
||||
*/
|
||||
export type TableNames = TableNamesInDataModel<DataModel>;
|
||||
|
||||
/**
|
||||
* The type of a document stored in Convex.
|
||||
*
|
||||
* @typeParam TableName - A string literal type of the table name (like "users").
|
||||
*/
|
||||
export type Doc<TableName extends TableNames> = DocumentByName<
|
||||
DataModel,
|
||||
TableName
|
||||
>;
|
||||
|
||||
/**
|
||||
* An identifier for a document in Convex.
|
||||
*
|
||||
* Convex documents are uniquely identified by their `Id`, which is accessible
|
||||
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
|
||||
*
|
||||
* Documents can be loaded using `db.get(id)` in query and mutation functions.
|
||||
*
|
||||
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
||||
* strings when type checking.
|
||||
*
|
||||
* @typeParam TableName - A string literal type of the table name (like "users").
|
||||
*/
|
||||
export type Id<TableName extends TableNames | SystemTableNames> =
|
||||
GenericId<TableName>;
|
||||
|
||||
/**
|
||||
* A type describing your Convex data model.
|
||||
*
|
||||
* This type includes information about what tables you have, the type of
|
||||
* documents stored in those tables, and the indexes defined on them.
|
||||
*
|
||||
* This type is used to parameterize methods like `queryGeneric` and
|
||||
* `mutationGeneric` to make them type-safe.
|
||||
*/
|
||||
export type DataModel = DataModelFromSchemaDefinition<typeof schema>;
|
142
packages/convex/_generated/server.d.ts
vendored
Normal file
142
packages/convex/_generated/server.d.ts
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated utilities for implementing server-side Convex query and mutation functions.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {
|
||||
ActionBuilder,
|
||||
HttpActionBuilder,
|
||||
MutationBuilder,
|
||||
QueryBuilder,
|
||||
GenericActionCtx,
|
||||
GenericMutationCtx,
|
||||
GenericQueryCtx,
|
||||
GenericDatabaseReader,
|
||||
GenericDatabaseWriter,
|
||||
} from "convex/server";
|
||||
import type { DataModel } from "./dataModel.js";
|
||||
|
||||
/**
|
||||
* Define a query in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to read your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const query: QueryBuilder<DataModel, "public">;
|
||||
|
||||
/**
|
||||
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const internalQuery: QueryBuilder<DataModel, "internal">;
|
||||
|
||||
/**
|
||||
* Define a mutation in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const mutation: MutationBuilder<DataModel, "public">;
|
||||
|
||||
/**
|
||||
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const internalMutation: MutationBuilder<DataModel, "internal">;
|
||||
|
||||
/**
|
||||
* Define an action in this Convex app's public API.
|
||||
*
|
||||
* An action is a function which can execute any JavaScript code, including non-deterministic
|
||||
* code and code with side-effects, like calling third-party services.
|
||||
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
||||
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
||||
*
|
||||
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const action: ActionBuilder<DataModel, "public">;
|
||||
|
||||
/**
|
||||
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const internalAction: ActionBuilder<DataModel, "internal">;
|
||||
|
||||
/**
|
||||
* Define an HTTP action.
|
||||
*
|
||||
* This function will be used to respond to HTTP requests received by a Convex
|
||||
* deployment if the requests matches the path and method where this action
|
||||
* is routed. Be sure to route your action in `convex/http.js`.
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||
*/
|
||||
export declare const httpAction: HttpActionBuilder;
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex query functions.
|
||||
*
|
||||
* The query context is passed as the first argument to any Convex query
|
||||
* function run on the server.
|
||||
*
|
||||
* This differs from the {@link MutationCtx} because all of the services are
|
||||
* read-only.
|
||||
*/
|
||||
export type QueryCtx = GenericQueryCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex mutation functions.
|
||||
*
|
||||
* The mutation context is passed as the first argument to any Convex mutation
|
||||
* function run on the server.
|
||||
*/
|
||||
export type MutationCtx = GenericMutationCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex action functions.
|
||||
*
|
||||
* The action context is passed as the first argument to any Convex action
|
||||
* function run on the server.
|
||||
*/
|
||||
export type ActionCtx = GenericActionCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* An interface to read from the database within Convex query functions.
|
||||
*
|
||||
* The two entry points are {@link DatabaseReader.get}, which fetches a single
|
||||
* document by its {@link Id}, or {@link DatabaseReader.query}, which starts
|
||||
* building a query.
|
||||
*/
|
||||
export type DatabaseReader = GenericDatabaseReader<DataModel>;
|
||||
|
||||
/**
|
||||
* An interface to read from and write to the database within Convex mutation
|
||||
* functions.
|
||||
*
|
||||
* Convex guarantees that all writes within a single mutation are
|
||||
* executed atomically, so you never have to worry about partial writes leaving
|
||||
* your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
|
||||
* for the guarantees Convex provides your functions.
|
||||
*/
|
||||
export type DatabaseWriter = GenericDatabaseWriter<DataModel>;
|
89
packages/convex/_generated/server.js
Normal file
89
packages/convex/_generated/server.js
Normal file
@@ -0,0 +1,89 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated utilities for implementing server-side Convex query and mutation functions.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {
|
||||
actionGeneric,
|
||||
httpActionGeneric,
|
||||
queryGeneric,
|
||||
mutationGeneric,
|
||||
internalActionGeneric,
|
||||
internalMutationGeneric,
|
||||
internalQueryGeneric,
|
||||
} from "convex/server";
|
||||
|
||||
/**
|
||||
* Define a query in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to read your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const query = queryGeneric;
|
||||
|
||||
/**
|
||||
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const internalQuery = internalQueryGeneric;
|
||||
|
||||
/**
|
||||
* Define a mutation in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const mutation = mutationGeneric;
|
||||
|
||||
/**
|
||||
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const internalMutation = internalMutationGeneric;
|
||||
|
||||
/**
|
||||
* Define an action in this Convex app's public API.
|
||||
*
|
||||
* An action is a function which can execute any JavaScript code, including non-deterministic
|
||||
* code and code with side-effects, like calling third-party services.
|
||||
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
||||
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
||||
*
|
||||
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const action = actionGeneric;
|
||||
|
||||
/**
|
||||
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const internalAction = internalActionGeneric;
|
||||
|
||||
/**
|
||||
* Define a Convex HTTP action.
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object
|
||||
* as its second.
|
||||
* @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`.
|
||||
*/
|
||||
export const httpAction = httpActionGeneric;
|
22
packages/convex/auth.config.ts
Normal file
22
packages/convex/auth.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
const clientId = process.env.WORKOS_CLIENT_ID
|
||||
|
||||
const authConfig = {
|
||||
providers: [
|
||||
{
|
||||
type: "customJwt",
|
||||
issuer: `https://api.workos.com/`,
|
||||
algorithm: "RS256",
|
||||
jwks: `https://api.workos.com/sso/jwks/${clientId}`,
|
||||
applicationID: clientId,
|
||||
},
|
||||
{
|
||||
type: "customJwt",
|
||||
issuer: `https://api.workos.com/user_management/${clientId}`,
|
||||
algorithm: "RS256",
|
||||
jwks: `https://api.workos.com/sso/jwks/${clientId}`,
|
||||
applicationID: clientId,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default authConfig
|
106
packages/convex/files.ts
Normal file
106
packages/convex/files.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { Id } from "@fileone/convex/_generated/dataModel"
|
||||
import { v } from "convex/values"
|
||||
import { authenticatedMutation, authenticatedQuery } from "./functions"
|
||||
import type { DirectoryItem } from "./model/directories"
|
||||
import * as Directories from "./model/directories"
|
||||
|
||||
export const generateUploadUrl = authenticatedMutation({
|
||||
handler: async (ctx) => {
|
||||
// ctx.user and ctx.identity are automatically available
|
||||
return await ctx.storage.generateUploadUrl()
|
||||
},
|
||||
})
|
||||
|
||||
export const fetchFiles = authenticatedQuery({
|
||||
args: {
|
||||
directoryId: v.optional(v.id("directories")),
|
||||
},
|
||||
handler: async (ctx, { directoryId }) => {
|
||||
return await ctx.db
|
||||
.query("files")
|
||||
.withIndex("byDirectoryId", (q) =>
|
||||
q.eq("userId", ctx.user._id).eq("directoryId", directoryId),
|
||||
)
|
||||
.collect()
|
||||
},
|
||||
})
|
||||
|
||||
export const fetchDirectoryContent = authenticatedQuery({
|
||||
args: {
|
||||
directoryId: v.optional(v.id("directories")),
|
||||
},
|
||||
handler: async (ctx, { directoryId }): Promise<DirectoryItem[]> => {
|
||||
return await Directories.fetchContent(ctx, directoryId)
|
||||
},
|
||||
})
|
||||
|
||||
export const createDirectory = authenticatedMutation({
|
||||
args: {
|
||||
name: v.string(),
|
||||
directoryId: v.optional(v.id("directories")),
|
||||
},
|
||||
handler: async (ctx, { name, directoryId }): Promise<Id<"directories">> => {
|
||||
return await Directories.create(ctx, {
|
||||
name,
|
||||
parentId: directoryId,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const saveFile = authenticatedMutation({
|
||||
args: {
|
||||
name: v.string(),
|
||||
size: v.number(),
|
||||
directoryId: v.optional(v.id("directories")),
|
||||
storageId: v.id("_storage"),
|
||||
mimeType: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, { name, storageId, directoryId, size, mimeType }) => {
|
||||
const now = new Date().toISOString()
|
||||
|
||||
await ctx.db.insert("files", {
|
||||
name,
|
||||
size,
|
||||
storageId,
|
||||
directoryId,
|
||||
userId: ctx.user._id,
|
||||
mimeType,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const moveToTrash = authenticatedMutation({
|
||||
args: {
|
||||
kind: v.union(v.literal("file"), v.literal("directory")),
|
||||
itemId: v.union(v.id("files"), v.id("directories")),
|
||||
},
|
||||
handler: async (ctx, { itemId, kind }) => {
|
||||
switch (kind) {
|
||||
case "file": {
|
||||
const file = await ctx.db.get(itemId as Id<"files">)
|
||||
if (!file || file.userId !== ctx.user._id) {
|
||||
throw new Error("File not found or access denied")
|
||||
}
|
||||
await ctx.db.patch(itemId, {
|
||||
deletedAt: new Date().toISOString(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case "directory": {
|
||||
const directory = await ctx.db.get(itemId as Id<"directories">)
|
||||
if (!directory || directory.userId !== ctx.user._id) {
|
||||
throw new Error("Directory not found or access denied")
|
||||
}
|
||||
await Directories.moveToTrashRecursive(
|
||||
ctx,
|
||||
itemId as Id<"directories">,
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return itemId
|
||||
},
|
||||
})
|
46
packages/convex/functions.ts
Normal file
46
packages/convex/functions.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { UserIdentity } from "convex/server"
|
||||
import {
|
||||
customCtx,
|
||||
customMutation,
|
||||
customQuery,
|
||||
} from "convex-helpers/server/customFunctions"
|
||||
import type { Doc } from "./_generated/dataModel"
|
||||
import type { MutationCtx, QueryCtx } from "./_generated/server"
|
||||
import { mutation, query } from "./_generated/server"
|
||||
import { userIdentityOrThrow, userOrThrow } from "./model/user"
|
||||
|
||||
export type AuthenticatedQueryCtx = QueryCtx & {
|
||||
user: Doc<"users">
|
||||
identity: UserIdentity
|
||||
}
|
||||
|
||||
export type AuthenticatedMutationCtx = MutationCtx & {
|
||||
user: Doc<"users">
|
||||
identity: UserIdentity
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom query that automatically provides authenticated user context
|
||||
* Throws an error if the user is not authenticated
|
||||
*/
|
||||
export const authenticatedQuery = customQuery(
|
||||
query,
|
||||
customCtx(async (ctx: QueryCtx) => {
|
||||
const user = await userOrThrow(ctx)
|
||||
const identity = await userIdentityOrThrow(ctx)
|
||||
return { user, identity }
|
||||
}),
|
||||
)
|
||||
|
||||
/**
|
||||
* Custom mutation that automatically provides authenticated user context
|
||||
* Throws an error if the user is not authenticated
|
||||
*/
|
||||
export const authenticatedMutation = customMutation(
|
||||
mutation,
|
||||
customCtx(async (ctx: MutationCtx) => {
|
||||
const user = await userOrThrow(ctx)
|
||||
const identity = await userIdentityOrThrow(ctx)
|
||||
return { user, identity }
|
||||
}),
|
||||
)
|
154
packages/convex/model/directories.ts
Normal file
154
packages/convex/model/directories.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import type { Doc, Id } from "@fileone/convex/_generated/dataModel"
|
||||
import type {
|
||||
AuthenticatedMutationCtx,
|
||||
AuthenticatedQueryCtx,
|
||||
} from "../functions"
|
||||
import * as Err from "./error"
|
||||
|
||||
type Directory = {
|
||||
kind: "directory"
|
||||
doc: Doc<"directories">
|
||||
}
|
||||
|
||||
type File = {
|
||||
kind: "file"
|
||||
doc: Doc<"files">
|
||||
}
|
||||
|
||||
export type DirectoryItem = Directory | File
|
||||
export type DirectoryItemKind = DirectoryItem["kind"]
|
||||
|
||||
export async function fetchContent(
|
||||
ctx: AuthenticatedQueryCtx,
|
||||
directoryId?: Id<"directories">,
|
||||
): Promise<DirectoryItem[]> {
|
||||
const [files, directories] = await Promise.all([
|
||||
ctx.db
|
||||
.query("files")
|
||||
.withIndex("byDirectoryId", (q) =>
|
||||
q
|
||||
.eq("userId", ctx.user._id)
|
||||
.eq("directoryId", directoryId)
|
||||
.eq("deletedAt", undefined),
|
||||
)
|
||||
.collect(),
|
||||
ctx.db
|
||||
.query("directories")
|
||||
.withIndex("byParentId", (q) =>
|
||||
q
|
||||
.eq("userId", ctx.user._id)
|
||||
.eq("parentId", directoryId)
|
||||
.eq("deletedAt", undefined),
|
||||
)
|
||||
.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(
|
||||
ctx: AuthenticatedMutationCtx,
|
||||
{ name, parentId }: { name: string; parentId?: Id<"directories"> },
|
||||
): Promise<Id<"directories">> {
|
||||
let parentDir: Doc<"directories"> | null = null
|
||||
if (parentId) {
|
||||
parentDir = await ctx.db.get(parentId)
|
||||
if (!parentDir) {
|
||||
throw Err.create(
|
||||
Err.Code.DirectoryNotFound,
|
||||
`Parent directory ${parentId} not found`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const existing = await ctx.db
|
||||
.query("directories")
|
||||
.withIndex("uniqueDirectoryInDirectory", (q) =>
|
||||
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"}`,
|
||||
)
|
||||
}
|
||||
|
||||
const now = new Date().toISOString()
|
||||
return await ctx.db.insert("directories", {
|
||||
name,
|
||||
parentId,
|
||||
userId: ctx.user._id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
path: parentDir ? `${parentDir.path}/${name}` : "/",
|
||||
})
|
||||
}
|
||||
|
||||
export async function moveToTrashRecursive(
|
||||
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
|
||||
.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) =>
|
||||
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])
|
||||
}
|
22
packages/convex/model/error.ts
Normal file
22
packages/convex/model/error.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ConvexError } from "convex/values"
|
||||
|
||||
export enum Code {
|
||||
DirectoryExists = "DirectoryExists",
|
||||
DirectoryNotFound = "DirectoryNotFound",
|
||||
FileExists = "FileExists",
|
||||
Internal = "Internal",
|
||||
Unauthenticated = "Unauthenticated",
|
||||
}
|
||||
|
||||
export type ApplicationError = ConvexError<{ code: Code; message: string }>
|
||||
|
||||
export function isApplicationError(error: unknown): error is ApplicationError {
|
||||
return error instanceof ConvexError && "code" in error.data
|
||||
}
|
||||
|
||||
export function create(code: Code, message?: string) {
|
||||
return new ConvexError({
|
||||
code,
|
||||
message,
|
||||
})
|
||||
}
|
46
packages/convex/model/user.ts
Normal file
46
packages/convex/model/user.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Id } from "../_generated/dataModel"
|
||||
import type { MutationCtx, QueryCtx } from "../_generated/server"
|
||||
import type { AuthenticatedMutationCtx } from "../functions"
|
||||
import * as Err from "./error"
|
||||
|
||||
/**
|
||||
* Get the current authenticated user identity
|
||||
* Throws an error if the user is not authenticated */
|
||||
export async function userIdentityOrThrow(ctx: QueryCtx | MutationCtx) {
|
||||
const identity = await ctx.auth.getUserIdentity()
|
||||
|
||||
if (!identity) {
|
||||
throw Err.create(Err.Code.Unauthenticated, "Not authenticated")
|
||||
}
|
||||
|
||||
return identity
|
||||
}
|
||||
|
||||
/**
|
||||
* Get internal user document from JWT authentication
|
||||
* Throws an error if the user is not authenticated
|
||||
*/
|
||||
export async function userOrThrow(ctx: QueryCtx | MutationCtx) {
|
||||
const identity = await userIdentityOrThrow(ctx)
|
||||
|
||||
// Look for existing user by JWT subject
|
||||
const user = await ctx.db
|
||||
.query("users")
|
||||
.withIndex("byJwtSubject", (q) => q.eq("jwtSubject", identity.subject))
|
||||
.first()
|
||||
|
||||
if (!user) {
|
||||
throw Err.create(
|
||||
Err.Code.Unauthenticated,
|
||||
"User not found - please sync user first",
|
||||
)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
export async function register(ctx: AuthenticatedMutationCtx) {
|
||||
await ctx.db.insert("users", {
|
||||
jwtSubject: ctx.identity.subject,
|
||||
})
|
||||
}
|
9
packages/convex/package.json
Normal file
9
packages/convex/package.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@fileone/convex",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
"convex": "^1.27.0"
|
||||
}
|
||||
}
|
48
packages/convex/schema.ts
Normal file
48
packages/convex/schema.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { defineSchema, defineTable } from "convex/server"
|
||||
import { v } from "convex/values"
|
||||
|
||||
const schema = defineSchema({
|
||||
users: defineTable({
|
||||
jwtSubject: v.string(),
|
||||
}).index("byJwtSubject", ["jwtSubject"]),
|
||||
files: defineTable({
|
||||
storageId: v.id("_storage"),
|
||||
userId: v.id("users"),
|
||||
directoryId: v.optional(v.id("directories")),
|
||||
name: v.string(),
|
||||
size: v.number(),
|
||||
mimeType: v.optional(v.string()),
|
||||
createdAt: v.string(),
|
||||
updatedAt: v.string(),
|
||||
deletedAt: v.optional(v.string()),
|
||||
})
|
||||
.index("byDirectoryId", ["userId", "directoryId", "deletedAt"])
|
||||
.index("byUserId", ["userId", "deletedAt"])
|
||||
.index("byDeletedAt", ["deletedAt"])
|
||||
.index("uniqueFileInDirectory", [
|
||||
"userId",
|
||||
"directoryId",
|
||||
"name",
|
||||
"deletedAt",
|
||||
]),
|
||||
directories: defineTable({
|
||||
name: v.string(),
|
||||
path: v.string(),
|
||||
userId: v.id("users"),
|
||||
parentId: v.optional(v.id("directories")),
|
||||
createdAt: v.string(),
|
||||
updatedAt: v.string(),
|
||||
deletedAt: v.optional(v.string()),
|
||||
})
|
||||
.index("byUserId", ["userId", "deletedAt"])
|
||||
.index("byParentId", ["userId", "parentId", "deletedAt"])
|
||||
.index("uniqueDirectoryInDirectory", [
|
||||
"userId",
|
||||
"parentId",
|
||||
"name",
|
||||
"deletedAt",
|
||||
])
|
||||
.index("byPath", ["path", "deletedAt"]),
|
||||
})
|
||||
|
||||
export default schema
|
32
packages/convex/users.ts
Normal file
32
packages/convex/users.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { mutation } from "./_generated/server"
|
||||
import { authenticatedQuery } from "./functions"
|
||||
import * as Err from "./model/error"
|
||||
|
||||
export const getCurrentUser = authenticatedQuery({
|
||||
handler: async (ctx) => {
|
||||
// ctx.user is the internal Convex user document
|
||||
return ctx.user
|
||||
},
|
||||
})
|
||||
|
||||
export const syncUser = mutation({
|
||||
handler: async (ctx) => {
|
||||
const identity = await ctx.auth.getUserIdentity()
|
||||
if (!identity) {
|
||||
throw Err.create(Err.Code.Unauthenticated)
|
||||
}
|
||||
|
||||
const existingUser = await ctx.db
|
||||
.query("users")
|
||||
.withIndex("byJwtSubject", (q) =>
|
||||
q.eq("jwtSubject", identity.subject),
|
||||
)
|
||||
.first()
|
||||
|
||||
if (!existingUser) {
|
||||
await ctx.db.insert("users", {
|
||||
jwtSubject: identity.subject,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
@@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@convex-dev/workos": "^0.0.1",
|
||||
"@fileone/convex": "workspace:*",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
@@ -42,4 +43,4 @@
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { api } from "@convex/_generated/api"
|
||||
import type { Id } from "@convex/_generated/dataModel"
|
||||
import type { DirectoryItem } from "@convex/model/directories"
|
||||
import { api } from "@fileone/convex/_generated/api"
|
||||
import type { DirectoryItem } from "@fileone/convex/model/directories"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import {
|
||||
type ColumnDef,
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { api } from "@convex/_generated/api"
|
||||
import { api } from "@fileone/convex/_generated/api"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { useMutation as useConvexMutation } from "convex/react"
|
||||
import { useSetAtom } from "jotai"
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { atom } from "jotai"
|
||||
import type { Id } from "@convex/_generated/dataModel"
|
||||
import type { Id } from "@fileone/convex/_generated/dataModel"
|
||||
import type {
|
||||
DirectoryItem,
|
||||
DirectoryItemKind,
|
||||
} from "@convex/model/directories"
|
||||
} from "@fileone/convex/model/directories"
|
||||
import { atom } from "jotai"
|
||||
|
||||
export const contextMenuTargeItemAtom = atom<DirectoryItem | null>(null)
|
||||
export const optimisticDeletedItemsAtom = atom(
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import {
|
||||
Code as ErrorCode,
|
||||
isApplicationError,
|
||||
} from "@fileone/convex/model/error"
|
||||
import { toast } from "sonner"
|
||||
import { Code as ErrorCode, isApplicationError } from "@convex/model/error"
|
||||
|
||||
const ERROR_MESSAGE = {
|
||||
[ErrorCode.DirectoryExists]: "Directory already exists",
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { api } from "@convex/_generated/api"
|
||||
import { api } from "@fileone/convex/_generated/api"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router"
|
||||
import { useConvexAuth, useMutation as useConvexMutation } from "convex/react"
|
||||
|
@@ -23,8 +23,7 @@
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@convex/*": ["../../convex/*"]
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
|
Reference in New Issue
Block a user