mirror of
https://github.com/get-drexa/drive.git
synced 2025-11-30 21:41:39 +00:00
Compare commits
9 Commits
9b8367ade4
...
ad99bca7fd
| Author | SHA1 | Date | |
|---|---|---|---|
|
ad99bca7fd
|
|||
|
b241f4e211
|
|||
| 027a315a04 | |||
| 015524cd63 | |||
| 4ebb3fe620 | |||
| b8c46217f7 | |||
| 94d6a22ab2 | |||
| f20f1a93c7 | |||
| acfe1523df |
@@ -1,11 +1,7 @@
|
||||
import { generateApiKey, newPrefix } from "@drexa/auth"
|
||||
import chalk from "chalk"
|
||||
import { Command } from "commander"
|
||||
import {
|
||||
promptNumber,
|
||||
promptOptionalDate,
|
||||
promptText,
|
||||
} from "../../prompts.ts"
|
||||
import { promptNumber, promptOptionalDate, promptText } from "../../prompts.ts"
|
||||
|
||||
export const apikeyCommand = new Command("apikey")
|
||||
.description("Generate a new API key")
|
||||
@@ -53,7 +49,8 @@ export const apikeyCommand = new Command("apikey")
|
||||
console.log(chalk.green(` ${result.unhashedKey}\n`))
|
||||
console.log(chalk.gray("─".repeat(60)))
|
||||
console.log(
|
||||
chalk.bold("\nHashed Key ") + chalk.dim("(store this in your database):"),
|
||||
chalk.bold("\nHashed Key ") +
|
||||
chalk.dim("(store this in your database):"),
|
||||
)
|
||||
console.log(chalk.dim(` ${result.hashedKey}\n`))
|
||||
console.log(chalk.bold("Description:"))
|
||||
|
||||
@@ -29,7 +29,9 @@ export async function promptNumber(
|
||||
): Promise<number> {
|
||||
const rl = createReadlineInterface()
|
||||
try {
|
||||
const defaultStr = defaultValue ? chalk.dim(` (default: ${defaultValue})`) : ""
|
||||
const defaultStr = defaultValue
|
||||
? chalk.dim(` (default: ${defaultValue})`)
|
||||
: ""
|
||||
const input = await rl.question(chalk.cyan(`${message}${defaultStr} `))
|
||||
|
||||
if ((!input || input.trim() === "") && defaultValue !== undefined) {
|
||||
@@ -59,7 +61,8 @@ export async function promptOptionalDate(
|
||||
const rl = createReadlineInterface()
|
||||
try {
|
||||
const input = await rl.question(
|
||||
chalk.cyan(`${message} `) + chalk.dim("(optional, format: YYYY-MM-DD) "),
|
||||
chalk.cyan(`${message} `) +
|
||||
chalk.dim("(optional, format: YYYY-MM-DD) "),
|
||||
)
|
||||
|
||||
if (!input || input.trim() === "") {
|
||||
@@ -68,7 +71,9 @@ export async function promptOptionalDate(
|
||||
|
||||
const date = new Date(input.trim())
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
console.error(chalk.red("✗ Invalid date format. Please use YYYY-MM-DD"))
|
||||
console.error(
|
||||
chalk.red("✗ Invalid date format. Please use YYYY-MM-DD"),
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
83
apps/cli/test-example.md
Normal file
83
apps/cli/test-example.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Testing the CLI
|
||||
|
||||
To test the API key generation interactively, run:
|
||||
|
||||
```bash
|
||||
bun drexa generate apikey
|
||||
```
|
||||
|
||||
## Example Session
|
||||
|
||||
The CLI now uses **chalk** for beautiful colored output!
|
||||
|
||||
```
|
||||
$ bun drexa generate apikey
|
||||
|
||||
🔑 Generate API Key
|
||||
|
||||
Enter API key prefix (e.g., 'proxy', 'admin'): testkey
|
||||
Enter key byte length: (default: 32)
|
||||
Enter description: Test API Key for development
|
||||
Enter expiration date (optional, format: YYYY-MM-DD):
|
||||
|
||||
⏳ Generating API key...
|
||||
|
||||
✓ API Key Generated Successfully!
|
||||
|
||||
────────────────────────────────────────────────────────────
|
||||
|
||||
⚠️ IMPORTANT: Save the unhashed key now. It won't be shown again!
|
||||
|
||||
Unhashed Key (save this):
|
||||
sk-testkey-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789
|
||||
|
||||
────────────────────────────────────────────────────────────
|
||||
|
||||
Hashed Key (store this in your database):
|
||||
$argon2id$v=19$m=4,t=3,p=1$...
|
||||
|
||||
Description:
|
||||
Test API Key for development
|
||||
|
||||
Expires At:
|
||||
Never
|
||||
|
||||
────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
### Color Scheme
|
||||
- **Prompts**: Cyan text with dimmed hints
|
||||
- **Success messages**: Green with checkmark
|
||||
- **Warnings**: Yellow with warning icon
|
||||
- **Errors**: Red with X mark
|
||||
- **Important data**: Green (unhashed key), dimmed (hashed key)
|
||||
- **Separators**: Gray lines
|
||||
|
||||
## Testing with Invalid Input
|
||||
|
||||
### Invalid prefix (contains dash)
|
||||
```bash
|
||||
$ bun drexa generate apikey
|
||||
Enter API key prefix (e.g., 'proxy', 'admin'): test-key
|
||||
✗ Invalid prefix: cannot contain "-" character. Please use alphanumeric characters only.
|
||||
```
|
||||
|
||||
### Invalid key byte length
|
||||
```bash
|
||||
$ bun drexa generate apikey
|
||||
Enter API key prefix (e.g., 'proxy', 'admin'): testkey
|
||||
Enter key byte length: (default: 32) -5
|
||||
✗ Please enter a valid positive number
|
||||
```
|
||||
|
||||
### Invalid date format
|
||||
```bash
|
||||
$ bun drexa generate apikey
|
||||
Enter API key prefix (e.g., 'proxy', 'admin'): testkey
|
||||
Enter key byte length: (default: 32)
|
||||
Enter description: Test
|
||||
Enter expiration date (optional, format: YYYY-MM-DD): invalid-date
|
||||
✗ Invalid date format. Please use YYYY-MM-DD
|
||||
```
|
||||
|
||||
All error messages are displayed in red for better visibility.
|
||||
@@ -1,25 +1,25 @@
|
||||
import { useRef, type FormEvent } from "react";
|
||||
import { type FormEvent, useRef } from "react"
|
||||
|
||||
export function APITester() {
|
||||
const responseInputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const responseInputRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const testEndpoint = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
try {
|
||||
const form = e.currentTarget;
|
||||
const formData = new FormData(form);
|
||||
const endpoint = formData.get("endpoint") as string;
|
||||
const url = new URL(endpoint, location.href);
|
||||
const method = formData.get("method") as string;
|
||||
const res = await fetch(url, { method });
|
||||
const form = e.currentTarget
|
||||
const formData = new FormData(form)
|
||||
const endpoint = formData.get("endpoint") as string
|
||||
const url = new URL(endpoint, location.href)
|
||||
const method = formData.get("method") as string
|
||||
const res = await fetch(url, { method })
|
||||
|
||||
const data = await res.json();
|
||||
responseInputRef.current!.value = JSON.stringify(data, null, 2);
|
||||
const data = await res.json()
|
||||
responseInputRef.current!.value = JSON.stringify(data, null, 2)
|
||||
} catch (error) {
|
||||
responseInputRef.current!.value = String(error);
|
||||
responseInputRef.current!.value = String(error)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="api-tester">
|
||||
@@ -28,12 +28,23 @@ export function APITester() {
|
||||
<option value="GET">GET</option>
|
||||
<option value="PUT">PUT</option>
|
||||
</select>
|
||||
<input type="text" name="endpoint" defaultValue="/api/hello" className="url-input" placeholder="/api/hello" />
|
||||
<input
|
||||
type="text"
|
||||
name="endpoint"
|
||||
defaultValue="/api/hello"
|
||||
className="url-input"
|
||||
placeholder="/api/hello"
|
||||
/>
|
||||
<button type="submit" className="send-button">
|
||||
Send
|
||||
</button>
|
||||
</form>
|
||||
<textarea ref={responseInputRef} readOnly placeholder="Response will appear here..." className="response-area" />
|
||||
<textarea
|
||||
ref={responseInputRef}
|
||||
readOnly
|
||||
placeholder="Response will appear here..."
|
||||
className="response-area"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as React from "react"
|
||||
import type * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -8,7 +8,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -21,7 +21,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -54,7 +54,7 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useMemo } from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useMemo } from "react"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
||||
return (
|
||||
@@ -12,7 +11,7 @@ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
||||
className={cn(
|
||||
"flex flex-col gap-6",
|
||||
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -32,7 +31,7 @@ function FieldLegend({
|
||||
"mb-3 font-medium",
|
||||
"data-[variant=legend]:text-base",
|
||||
"data-[variant=label]:text-sm",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -45,7 +44,7 @@ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="field-group"
|
||||
className={cn(
|
||||
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -73,7 +72,7 @@ const fieldVariants = cva(
|
||||
defaultVariants: {
|
||||
orientation: "vertical",
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function Field({
|
||||
@@ -98,7 +97,7 @@ function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="field-content"
|
||||
className={cn(
|
||||
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -116,7 +115,7 @@ function FieldLabel({
|
||||
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
||||
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
|
||||
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -129,7 +128,7 @@ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="field-label"
|
||||
className={cn(
|
||||
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -144,7 +143,7 @@ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
|
||||
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
|
||||
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -164,7 +163,7 @@ function FieldSeparator({
|
||||
data-content={!!children}
|
||||
className={cn(
|
||||
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -206,7 +205,7 @@ function FieldError({
|
||||
<ul className="ml-4 flex list-disc flex-col gap-1">
|
||||
{errors.map(
|
||||
(error, index) =>
|
||||
error?.message && <li key={index}>{error.message}</li>
|
||||
error?.message && <li key={index}>{error.message}</li>,
|
||||
)}
|
||||
</ul>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import type * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -12,7 +12,7 @@ function Label({
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
import type * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -13,7 +13,7 @@ function Progress({
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
import type * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -18,7 +18,7 @@ function Separator({
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { Id } from "@fileone/convex/dataModel"
|
||||
import type {
|
||||
DirectoryItem,
|
||||
DirectoryItemKind,
|
||||
} from "@fileone/convex/types"
|
||||
import type { DirectoryItem, DirectoryItemKind } from "@fileone/convex/types"
|
||||
import type { RowSelectionState } from "@tanstack/react-table"
|
||||
import { atom } from "jotai"
|
||||
|
||||
|
||||
@@ -53,12 +53,9 @@ export const clearFileUploadStatusesAtom = atom(
|
||||
},
|
||||
)
|
||||
|
||||
export const clearAllFileUploadStatusesAtom = atom(
|
||||
null,
|
||||
(get, set) => {
|
||||
export const clearAllFileUploadStatusesAtom = atom(null, (_, set) => {
|
||||
set(fileUploadStatusesAtom, {})
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
export const fileUploadCountAtom = atom(
|
||||
(get) => Object.keys(get(fileUploadStatusesAtom)).length,
|
||||
|
||||
@@ -88,7 +88,6 @@ function useUploadFilesAtom({
|
||||
)
|
||||
},
|
||||
}).catch((error) => {
|
||||
console.log("error", error)
|
||||
store.set(
|
||||
fileUploadStatusAtomFamily(pickedFile.id),
|
||||
{
|
||||
@@ -130,6 +129,9 @@ function useUploadFilesAtom({
|
||||
toast.success("All files uploaded successfully")
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(formatError(error))
|
||||
},
|
||||
}),
|
||||
[uploadFile, store.set],
|
||||
)
|
||||
@@ -270,6 +272,7 @@ export function UploadFileDialog({
|
||||
onClick={openFilePicker}
|
||||
uploadFilesAtom={uploadFilesAtom}
|
||||
/>
|
||||
<ClearUploadErrorsButton />
|
||||
<UploadButton
|
||||
uploadFilesAtom={uploadFilesAtom}
|
||||
onClick={onUploadButtonClick}
|
||||
@@ -373,10 +376,10 @@ function SelectMoreFilesButton({
|
||||
uploadFilesAtom: UploadFilesAtom
|
||||
}) {
|
||||
const pickedFiles = useAtomValue(pickedFilesAtom)
|
||||
const { data: uploadResults, isPending: isUploading } =
|
||||
useAtomValue(uploadFilesAtom)
|
||||
const fileUploadCount = useAtomValue(fileUploadCountAtom)
|
||||
const { isPending: isUploading } = useAtomValue(uploadFilesAtom)
|
||||
|
||||
if (pickedFiles.length === 0 || uploadResults) {
|
||||
if (pickedFiles.length === 0 || fileUploadCount > 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -387,6 +390,29 @@ function SelectMoreFilesButton({
|
||||
)
|
||||
}
|
||||
|
||||
function ClearUploadErrorsButton() {
|
||||
const hasUploadErrors = useAtomValue(hasFileUploadsErrorAtom)
|
||||
const clearAllFileUploadStatuses = useSetAtom(
|
||||
clearAllFileUploadStatusesAtom,
|
||||
)
|
||||
const setPickedFiles = useSetAtom(pickedFilesAtom)
|
||||
|
||||
if (!hasUploadErrors) {
|
||||
return null
|
||||
}
|
||||
|
||||
function clearUploadErrors() {
|
||||
setPickedFiles([])
|
||||
clearAllFileUploadStatuses()
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="outline" onClick={clearUploadErrors}>
|
||||
Clear uploads
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function UploadButton({
|
||||
uploadFilesAtom,
|
||||
onClick,
|
||||
@@ -533,7 +559,6 @@ function PickedFileItem({
|
||||
}) {
|
||||
const fileUploadAtom = fileUploadStatusAtomFamily(pickedFile.id)
|
||||
const fileUpload = useAtomValue(fileUploadAtom)
|
||||
console.log("fileUpload", fileUpload)
|
||||
const { file, id } = pickedFile
|
||||
|
||||
let statusIndicator: React.ReactNode
|
||||
|
||||
@@ -54,7 +54,7 @@ export function useFileDrop({
|
||||
errors: Err.ApplicationErrorData[]
|
||||
}) => {
|
||||
const conflictCount = errors.reduce((acc, error) => {
|
||||
if (error.code === Err.Code.Conflict) {
|
||||
if (error.code === Err.ErrorCode.Conflict) {
|
||||
return acc + 1
|
||||
}
|
||||
return acc
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
Code as ErrorCode,
|
||||
type ApplicationErrorData,
|
||||
ErrorCode,
|
||||
isApplicationError,
|
||||
} from "@fileone/convex/error"
|
||||
import { ConvexError } from "convex/values"
|
||||
import { toast } from "sonner"
|
||||
|
||||
const ERROR_MESSAGE = {
|
||||
@@ -9,13 +11,19 @@ const ERROR_MESSAGE = {
|
||||
[ErrorCode.FileExists]: "File already exists",
|
||||
[ErrorCode.Internal]: "Internal application error",
|
||||
[ErrorCode.Conflict]: "Conflict",
|
||||
[ErrorCode.DirectoryNotFound]: "Directory not found",
|
||||
[ErrorCode.FileNotFound]: "File not found",
|
||||
[ErrorCode.Unauthenticated]: "Unauthenticated",
|
||||
[ErrorCode.NotFound]: "Not found",
|
||||
[ErrorCode.StorageQuotaExceeded]: "Storage is full",
|
||||
} as const
|
||||
|
||||
export function isApplicationConvexError(
|
||||
error: unknown,
|
||||
): error is ConvexError<ApplicationErrorData> {
|
||||
return error instanceof ConvexError && isApplicationError(error.data)
|
||||
}
|
||||
|
||||
export function formatError(error: unknown): string {
|
||||
if (isApplicationError(error)) {
|
||||
if (isApplicationConvexError(error)) {
|
||||
return ERROR_MESSAGE[error.data.code]
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
@@ -25,8 +33,12 @@ export function formatError(error: unknown): string {
|
||||
}
|
||||
|
||||
export function defaultOnError(error: unknown) {
|
||||
console.log(error)
|
||||
if (isApplicationConvexError(error)) {
|
||||
toast.error(formatError(error))
|
||||
} else {
|
||||
console.error("Catastrophic error:", error)
|
||||
toast.error("An unexpected error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
export function withDefaultOnError(fn: (error: unknown) => void) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import path from "node:path"
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
import { TanStackRouterVite } from "@tanstack/router-plugin/vite"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import path from "path"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
export default defineConfig({
|
||||
@@ -19,7 +19,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ["convex/react", "convex-helpers"],
|
||||
include: ["convex/react", "convex/values", "convex-helpers"],
|
||||
// Workaround for better-auth bug: https://github.com/better-auth/better-auth/issues/4457
|
||||
// Vite's esbuild incorrectly transpiles better-call dependency causing 'super' keyword errors
|
||||
exclude: ["better-auth", "@convex-dev/better-auth"],
|
||||
|
||||
@@ -62,10 +62,14 @@ export class WebCryptoSha256Hasher implements PassswordHasher {
|
||||
}
|
||||
return btoa(binary).replace(/[+/=]/g, (char) => {
|
||||
switch (char) {
|
||||
case "+": return "-"
|
||||
case "/": return "_"
|
||||
case "=": return ""
|
||||
default: return char
|
||||
case "+":
|
||||
return "-"
|
||||
case "/":
|
||||
return "_"
|
||||
case "=":
|
||||
return ""
|
||||
default:
|
||||
return char
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createApi } from "@convex-dev/better-auth";
|
||||
import schema from "./schema";
|
||||
import { createAuth } from "../auth";
|
||||
import { createApi } from "@convex-dev/better-auth"
|
||||
import { createAuth } from "../auth"
|
||||
import schema from "./schema"
|
||||
|
||||
export const {
|
||||
create,
|
||||
@@ -10,4 +10,4 @@ export const {
|
||||
updateMany,
|
||||
deleteOne,
|
||||
deleteMany,
|
||||
} = createApi(schema, createAuth);
|
||||
} = createApi(schema, createAuth)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createAuth } from '../auth'
|
||||
import { getStaticAuth } from '@convex-dev/better-auth'
|
||||
import { getStaticAuth } from "@convex-dev/better-auth"
|
||||
import { createAuth } from "../auth"
|
||||
|
||||
// Export a static instance for Better Auth schema generation
|
||||
export const auth = getStaticAuth(createAuth)
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineComponent } from "convex/server";
|
||||
import { defineComponent } from "convex/server"
|
||||
|
||||
const component = defineComponent("betterAuth");
|
||||
const component = defineComponent("betterAuth")
|
||||
|
||||
export default component;
|
||||
export default component
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// To regenerate the schema, run:
|
||||
// `npx @better-auth/cli generate --output undefined -y`
|
||||
|
||||
import { defineSchema, defineTable } from "convex/server";
|
||||
import { v } from "convex/values";
|
||||
import { defineSchema, defineTable } from "convex/server"
|
||||
import { v } from "convex/values"
|
||||
|
||||
export const tables = {
|
||||
user: defineTable({
|
||||
@@ -63,8 +63,8 @@ export const tables = {
|
||||
privateKey: v.string(),
|
||||
createdAt: v.number(),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const schema = defineSchema(tables);
|
||||
const schema = defineSchema(tables)
|
||||
|
||||
export default schema;
|
||||
export default schema
|
||||
|
||||
8
packages/convex/convex/_generated/api.d.ts
vendored
8
packages/convex/convex/_generated/api.d.ts
vendored
@@ -12,7 +12,7 @@ import type {
|
||||
ApiFromModules,
|
||||
FilterApi,
|
||||
FunctionReference,
|
||||
} from "convex/server";
|
||||
} from "convex/server"
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's API.
|
||||
@@ -22,12 +22,12 @@ import type {
|
||||
* const myFunctionReference = api.myModule.myFunction;
|
||||
* ```
|
||||
*/
|
||||
declare const fullApi: ApiFromModules<{}>;
|
||||
declare const fullApi: ApiFromModules<{}>
|
||||
export declare const api: FilterApi<
|
||||
typeof fullApi,
|
||||
FunctionReference<any, "public">
|
||||
>;
|
||||
>
|
||||
export declare const internal: FilterApi<
|
||||
typeof fullApi,
|
||||
FunctionReference<any, "internal">
|
||||
>;
|
||||
>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { anyApi } from "convex/server";
|
||||
import { anyApi } from "convex/server"
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's API.
|
||||
@@ -18,5 +18,5 @@ import { anyApi } from "convex/server";
|
||||
* const myFunctionReference = api.myModule.myFunction;
|
||||
* ```
|
||||
*/
|
||||
export const api = anyApi;
|
||||
export const internal = anyApi;
|
||||
export const api = anyApi
|
||||
export const internal = anyApi
|
||||
|
||||
13
packages/convex/convex/_generated/dataModel.d.ts
vendored
13
packages/convex/convex/_generated/dataModel.d.ts
vendored
@@ -8,8 +8,8 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { AnyDataModel } from "convex/server";
|
||||
import type { GenericId } from "convex/values";
|
||||
import { AnyDataModel } from "convex/server"
|
||||
import type { GenericId } from "convex/values"
|
||||
|
||||
/**
|
||||
* No `schema.ts` file found!
|
||||
@@ -25,12 +25,12 @@ import type { GenericId } from "convex/values";
|
||||
/**
|
||||
* The names of all of your Convex tables.
|
||||
*/
|
||||
export type TableNames = string;
|
||||
export type TableNames = string
|
||||
|
||||
/**
|
||||
* The type of a document stored in Convex.
|
||||
*/
|
||||
export type Doc = any;
|
||||
export type Doc = any
|
||||
|
||||
/**
|
||||
* An identifier for a document in Convex.
|
||||
@@ -43,8 +43,7 @@ export type Doc = any;
|
||||
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
||||
* strings when type checking.
|
||||
*/
|
||||
export type Id<TableName extends TableNames = TableNames> =
|
||||
GenericId<TableName>;
|
||||
export type Id<TableName extends TableNames = TableNames> = GenericId<TableName>
|
||||
|
||||
/**
|
||||
* A type describing your Convex data model.
|
||||
@@ -55,4 +54,4 @@ export type Id<TableName extends TableNames = TableNames> =
|
||||
* This type is used to parameterize methods like `queryGeneric` and
|
||||
* `mutationGeneric` to make them type-safe.
|
||||
*/
|
||||
export type DataModel = AnyDataModel;
|
||||
export type DataModel = AnyDataModel
|
||||
|
||||
28
packages/convex/convex/_generated/server.d.ts
vendored
28
packages/convex/convex/_generated/server.d.ts
vendored
@@ -18,8 +18,8 @@ import {
|
||||
GenericQueryCtx,
|
||||
GenericDatabaseReader,
|
||||
GenericDatabaseWriter,
|
||||
} from "convex/server";
|
||||
import type { DataModel } from "./dataModel.js";
|
||||
} from "convex/server"
|
||||
import type { DataModel } from "./dataModel.js"
|
||||
|
||||
/**
|
||||
* Define a query in this Convex app's public API.
|
||||
@@ -29,7 +29,7 @@ import type { DataModel } from "./dataModel.js";
|
||||
* @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">;
|
||||
export declare const query: QueryBuilder<DataModel, "public">
|
||||
|
||||
/**
|
||||
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||
@@ -39,7 +39,7 @@ export declare const query: QueryBuilder<DataModel, "public">;
|
||||
* @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">;
|
||||
export declare const internalQuery: QueryBuilder<DataModel, "internal">
|
||||
|
||||
/**
|
||||
* Define a mutation in this Convex app's public API.
|
||||
@@ -49,7 +49,7 @@ export declare const internalQuery: QueryBuilder<DataModel, "internal">;
|
||||
* @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">;
|
||||
export declare const mutation: MutationBuilder<DataModel, "public">
|
||||
|
||||
/**
|
||||
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||
@@ -59,7 +59,7 @@ export declare const mutation: MutationBuilder<DataModel, "public">;
|
||||
* @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">;
|
||||
export declare const internalMutation: MutationBuilder<DataModel, "internal">
|
||||
|
||||
/**
|
||||
* Define an action in this Convex app's public API.
|
||||
@@ -72,7 +72,7 @@ export declare const internalMutation: MutationBuilder<DataModel, "internal">;
|
||||
* @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">;
|
||||
export declare const action: ActionBuilder<DataModel, "public">
|
||||
|
||||
/**
|
||||
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||
@@ -80,7 +80,7 @@ export declare const action: ActionBuilder<DataModel, "public">;
|
||||
* @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">;
|
||||
export declare const internalAction: ActionBuilder<DataModel, "internal">
|
||||
|
||||
/**
|
||||
* Define an HTTP action.
|
||||
@@ -92,7 +92,7 @@ export declare const internalAction: ActionBuilder<DataModel, "internal">;
|
||||
* @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;
|
||||
export declare const httpAction: HttpActionBuilder
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex query functions.
|
||||
@@ -103,7 +103,7 @@ export declare const httpAction: HttpActionBuilder;
|
||||
* This differs from the {@link MutationCtx} because all of the services are
|
||||
* read-only.
|
||||
*/
|
||||
export type QueryCtx = GenericQueryCtx<DataModel>;
|
||||
export type QueryCtx = GenericQueryCtx<DataModel>
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex mutation functions.
|
||||
@@ -111,7 +111,7 @@ export type QueryCtx = GenericQueryCtx<DataModel>;
|
||||
* The mutation context is passed as the first argument to any Convex mutation
|
||||
* function run on the server.
|
||||
*/
|
||||
export type MutationCtx = GenericMutationCtx<DataModel>;
|
||||
export type MutationCtx = GenericMutationCtx<DataModel>
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex action functions.
|
||||
@@ -119,7 +119,7 @@ export type MutationCtx = GenericMutationCtx<DataModel>;
|
||||
* The action context is passed as the first argument to any Convex action
|
||||
* function run on the server.
|
||||
*/
|
||||
export type ActionCtx = GenericActionCtx<DataModel>;
|
||||
export type ActionCtx = GenericActionCtx<DataModel>
|
||||
|
||||
/**
|
||||
* An interface to read from the database within Convex query functions.
|
||||
@@ -128,7 +128,7 @@ export type ActionCtx = GenericActionCtx<DataModel>;
|
||||
* document by its {@link Id}, or {@link DatabaseReader.query}, which starts
|
||||
* building a query.
|
||||
*/
|
||||
export type DatabaseReader = GenericDatabaseReader<DataModel>;
|
||||
export type DatabaseReader = GenericDatabaseReader<DataModel>
|
||||
|
||||
/**
|
||||
* An interface to read from and write to the database within Convex mutation
|
||||
@@ -139,4 +139,4 @@ export type DatabaseReader = GenericDatabaseReader<DataModel>;
|
||||
* 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>;
|
||||
export type DatabaseWriter = GenericDatabaseWriter<DataModel>
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
import {
|
||||
actionGeneric,
|
||||
httpActionGeneric,
|
||||
queryGeneric,
|
||||
mutationGeneric,
|
||||
internalActionGeneric,
|
||||
internalMutationGeneric,
|
||||
internalQueryGeneric,
|
||||
} from "convex/server";
|
||||
mutationGeneric,
|
||||
queryGeneric,
|
||||
} from "convex/server"
|
||||
|
||||
/**
|
||||
* Define a query in this Convex app's public API.
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
* @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;
|
||||
export const query = queryGeneric
|
||||
|
||||
/**
|
||||
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||
@@ -36,7 +36,7 @@ export const query = queryGeneric;
|
||||
* @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;
|
||||
export const internalQuery = internalQueryGeneric
|
||||
|
||||
/**
|
||||
* Define a mutation in this Convex app's public API.
|
||||
@@ -46,7 +46,7 @@ export const internalQuery = internalQueryGeneric;
|
||||
* @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;
|
||||
export const mutation = mutationGeneric
|
||||
|
||||
/**
|
||||
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||
@@ -56,7 +56,7 @@ export const mutation = mutationGeneric;
|
||||
* @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;
|
||||
export const internalMutation = internalMutationGeneric
|
||||
|
||||
/**
|
||||
* Define an action in this Convex app's public API.
|
||||
@@ -69,7 +69,7 @@ export const internalMutation = internalMutationGeneric;
|
||||
* @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;
|
||||
export const action = actionGeneric
|
||||
|
||||
/**
|
||||
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||
@@ -77,7 +77,7 @@ export const action = actionGeneric;
|
||||
* @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;
|
||||
export const internalAction = internalActionGeneric
|
||||
|
||||
/**
|
||||
* Define a Convex HTTP action.
|
||||
@@ -86,4 +86,4 @@ export const internalAction = internalActionGeneric;
|
||||
* as its second.
|
||||
* @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`.
|
||||
*/
|
||||
export const httpAction = httpActionGeneric;
|
||||
export const httpAction = httpActionGeneric
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Id } from "@fileone/convex/dataModel"
|
||||
import { v } from "convex/values"
|
||||
import { ConvexError, v } from "convex/values"
|
||||
import {
|
||||
authenticatedMutation,
|
||||
authenticatedQuery,
|
||||
@@ -8,19 +8,19 @@ import {
|
||||
import * as Directories from "./model/directories"
|
||||
import * as Files from "./model/files"
|
||||
import * as User from "./model/user"
|
||||
import * as Err from "./shared/error"
|
||||
import { ErrorCode, error } from "./shared/error"
|
||||
|
||||
export const generateUploadUrl = authenticatedMutation({
|
||||
handler: async (ctx) => {
|
||||
const usageStatistics = await User.queryCachedUsageStatistics(ctx)
|
||||
if (!usageStatistics) {
|
||||
throw Err.create(Err.Code.Internal, "Internal server error")
|
||||
const userInfo = await User.queryInfo(ctx)
|
||||
if (!userInfo) {
|
||||
throw new ConvexError({ message: "Internal server error" })
|
||||
}
|
||||
if (
|
||||
usageStatistics.storageUsageBytes >=
|
||||
usageStatistics.storageQuotaBytes
|
||||
) {
|
||||
throw Err.create(Err.Code.Forbidden, "Storage quota exceeded")
|
||||
if (userInfo.storageUsageBytes >= userInfo.storageQuotaBytes) {
|
||||
throw new ConvexError({
|
||||
code: ErrorCode.StorageQuotaExceeded,
|
||||
message: "Storage quota exceeded",
|
||||
})
|
||||
}
|
||||
return await ctx.storage.generateUploadUrl()
|
||||
},
|
||||
@@ -53,7 +53,10 @@ export const fetchDirectory = authenticatedQuery({
|
||||
handler: async (ctx, { directoryId }) => {
|
||||
const directory = await authorizedGet(ctx, directoryId)
|
||||
if (!directory) {
|
||||
throw new Error("Directory not found")
|
||||
error({
|
||||
code: ErrorCode.NotFound,
|
||||
message: "Directory not found",
|
||||
})
|
||||
}
|
||||
return await Directories.fetch(ctx, { directoryId })
|
||||
},
|
||||
@@ -67,7 +70,10 @@ export const createDirectory = authenticatedMutation({
|
||||
handler: async (ctx, { name, directoryId }): Promise<Id<"directories">> => {
|
||||
const parentDirectory = await authorizedGet(ctx, directoryId)
|
||||
if (!parentDirectory) {
|
||||
throw new Error("Parent directory not found")
|
||||
error({
|
||||
code: ErrorCode.NotFound,
|
||||
message: "Parent directory not found",
|
||||
})
|
||||
}
|
||||
|
||||
return await Directories.create(ctx, {
|
||||
@@ -76,56 +82,3 @@ export const createDirectory = authenticatedMutation({
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const saveFile = authenticatedMutation({
|
||||
args: {
|
||||
name: v.string(),
|
||||
directoryId: v.id("directories"),
|
||||
storageId: v.id("_storage"),
|
||||
},
|
||||
handler: async (ctx, { name, storageId, directoryId }) => {
|
||||
const directory = await authorizedGet(ctx, directoryId)
|
||||
if (!directory) {
|
||||
throw new Error("Directory not found")
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
const fileMetadata = await Promise.all([
|
||||
ctx.db.system.get(storageId),
|
||||
ctx.user.queryUsageStatistics(),
|
||||
])
|
||||
if (!fileMetadata) {
|
||||
throw Err.create(Err.Code.Internal, "Internal server error")
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
ctx.db.insert("files", {
|
||||
name,
|
||||
size: fileMetadata.size,
|
||||
storageId,
|
||||
directoryId,
|
||||
userId: ctx.user._id,
|
||||
mimeType,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}),
|
||||
])
|
||||
},
|
||||
})
|
||||
|
||||
export const renameFile = authenticatedMutation({
|
||||
args: {
|
||||
directoryId: v.optional(v.id("directories")),
|
||||
itemId: v.id("files"),
|
||||
newName: v.string(),
|
||||
},
|
||||
handler: async (ctx, { directoryId, itemId, newName }) => {
|
||||
const file = await authorizedGet(ctx, itemId)
|
||||
if (!file) {
|
||||
throw new Error("File not found")
|
||||
}
|
||||
|
||||
await Files.renameFile(ctx, { directoryId, itemId, newName })
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { v } from "convex/values"
|
||||
import { ConvexError, v } from "convex/values"
|
||||
import {
|
||||
apiKeyAuthenticatedQuery,
|
||||
authenticatedMutation,
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
VDirectoryHandle,
|
||||
VFileSystemHandle,
|
||||
} from "./model/filesystem"
|
||||
import * as Err from "./shared/error"
|
||||
import { createErrorData, ErrorCode, error } from "./shared/error"
|
||||
import type {
|
||||
DirectoryHandle,
|
||||
FileHandle,
|
||||
@@ -36,10 +36,10 @@ export const moveItems = authenticatedMutation({
|
||||
targetDirectoryHandle.id,
|
||||
)
|
||||
if (!targetDirectory) {
|
||||
throw Err.create(
|
||||
Err.Code.DirectoryNotFound,
|
||||
`Directory ${targetDirectoryHandle.id} not found`,
|
||||
)
|
||||
error({
|
||||
code: ErrorCode.NotFound,
|
||||
message: `Directory ${targetDirectoryHandle.id} not found`,
|
||||
})
|
||||
}
|
||||
|
||||
const directoryHandles: DirectoryHandle[] = []
|
||||
@@ -81,10 +81,10 @@ export const moveToTrash = authenticatedMutation({
|
||||
for (const handle of handles) {
|
||||
const item = await authorizedGet(ctx, handle.id)
|
||||
if (!item) {
|
||||
throw Err.create(
|
||||
Err.Code.NotFound,
|
||||
`Item ${handle.id} not found`,
|
||||
)
|
||||
error({
|
||||
code: ErrorCode.NotFound,
|
||||
message: `Item ${handle.id} not found`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ export const moveToTrash = authenticatedMutation({
|
||||
})
|
||||
|
||||
const results = await Promise.allSettled(promises)
|
||||
const errors: Err.ApplicationErrorData[] = []
|
||||
const errors = []
|
||||
const okHandles: FileSystemHandle[] = []
|
||||
for (const result of results) {
|
||||
switch (result.status) {
|
||||
@@ -113,7 +113,7 @@ export const moveToTrash = authenticatedMutation({
|
||||
okHandles.push(result.value)
|
||||
break
|
||||
case "rejected":
|
||||
errors.push(Err.createJson(Err.Code.Internal))
|
||||
errors.push(createErrorData(ErrorCode.Internal))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "convex-helpers/server/customFunctions"
|
||||
import * as ApiKey from "./model/apikey"
|
||||
import { type AuthUser, userIdentityOrThrow, userOrThrow } from "./model/user"
|
||||
import * as Err from "./shared/error"
|
||||
import { ErrorCode, error } from "./shared/error"
|
||||
|
||||
export type AuthenticatedQueryCtx = QueryCtx & {
|
||||
user: AuthUser
|
||||
@@ -65,7 +65,10 @@ export const apiKeyAuthenticatedQuery = customQuery(query, {
|
||||
},
|
||||
input: async (ctx, args) => {
|
||||
if (!(await ApiKey.verifyApiKey(ctx, args.apiKey))) {
|
||||
throw Err.create(Err.Code.Unauthenticated, "Invalid API key")
|
||||
error({
|
||||
code: ErrorCode.Unauthenticated,
|
||||
message: "Invalid API key",
|
||||
})
|
||||
}
|
||||
return { ctx: ctx as ApiKeyAuthenticatedQueryCtx, args }
|
||||
},
|
||||
@@ -80,7 +83,10 @@ export const apiKeyAuthenticatedMutation = customMutation(mutation, {
|
||||
},
|
||||
input: async (ctx, args) => {
|
||||
if (!(await ApiKey.verifyApiKey(ctx, args.apiKey))) {
|
||||
throw Err.create(Err.Code.Unauthenticated, "Invalid API key")
|
||||
error({
|
||||
code: ErrorCode.Unauthenticated,
|
||||
message: "Invalid API key",
|
||||
})
|
||||
}
|
||||
return { ctx, args }
|
||||
},
|
||||
|
||||
@@ -4,7 +4,8 @@ import type {
|
||||
AuthenticatedQueryCtx,
|
||||
} from "../functions"
|
||||
import { authorizedGet } from "../functions"
|
||||
import * as Err from "../shared/error"
|
||||
import type { ApplicationErrorData } from "../shared/error"
|
||||
import { createErrorData, ErrorCode, error } from "../shared/error"
|
||||
import {
|
||||
type DirectoryHandle,
|
||||
type DirectoryPath,
|
||||
@@ -30,10 +31,10 @@ export async function fetchHandle(
|
||||
): Promise<Doc<"directories">> {
|
||||
const directory = await authorizedGet(ctx, handle.id)
|
||||
if (!directory) {
|
||||
throw Err.create(
|
||||
Err.Code.DirectoryNotFound,
|
||||
`Directory ${handle.id} not found`,
|
||||
)
|
||||
error({
|
||||
code: ErrorCode.NotFound,
|
||||
message: `Directory ${handle.id} not found`,
|
||||
})
|
||||
}
|
||||
return directory
|
||||
}
|
||||
@@ -44,10 +45,10 @@ export async function fetch(
|
||||
): Promise<DirectoryInfo> {
|
||||
const directory = await authorizedGet(ctx, directoryId)
|
||||
if (!directory) {
|
||||
throw Err.create(
|
||||
Err.Code.DirectoryNotFound,
|
||||
`Directory ${directoryId} not found`,
|
||||
)
|
||||
error({
|
||||
code: ErrorCode.NotFound,
|
||||
message: `Directory ${directoryId} not found`,
|
||||
})
|
||||
}
|
||||
|
||||
const path: DirectoryPath = [
|
||||
@@ -66,7 +67,10 @@ export async function fetch(
|
||||
})
|
||||
parentDirId = parentDir.parentId
|
||||
} else {
|
||||
throw Err.create(Err.Code.DirectoryNotFound, "Parent directory not found")
|
||||
error({
|
||||
code: ErrorCode.NotFound,
|
||||
message: "Parent directory not found",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,10 +139,10 @@ export async function create(
|
||||
): Promise<Id<"directories">> {
|
||||
const parentDir = await authorizedGet(ctx, parentId)
|
||||
if (!parentDir) {
|
||||
throw Err.create(
|
||||
Err.Code.DirectoryNotFound,
|
||||
`Parent directory ${parentId} not found`,
|
||||
)
|
||||
error({
|
||||
code: ErrorCode.NotFound,
|
||||
message: `Parent directory ${parentId} not found`,
|
||||
})
|
||||
}
|
||||
|
||||
const existing = await ctx.db
|
||||
@@ -153,10 +157,10 @@ export async function create(
|
||||
.first()
|
||||
|
||||
if (existing) {
|
||||
throw Err.create(
|
||||
Err.Code.DirectoryExists,
|
||||
`Directory with name ${name} already exists in ${parentId ? `directory ${parentId}` : "root"}`,
|
||||
)
|
||||
error({
|
||||
code: ErrorCode.DirectoryExists,
|
||||
message: `Directory with name ${name} already exists in ${parentId ? `directory ${parentId}` : "root"}`,
|
||||
})
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
@@ -183,10 +187,10 @@ export async function move(
|
||||
sourceDirectories.map((directory) =>
|
||||
authorizedGet(ctx, directory.id).then((d) => {
|
||||
if (!d) {
|
||||
throw Err.create(
|
||||
Err.Code.DirectoryNotFound,
|
||||
`Directory ${directory.id} not found`,
|
||||
)
|
||||
error({
|
||||
code: ErrorCode.NotFound,
|
||||
message: `Directory ${directory.id} not found`,
|
||||
})
|
||||
}
|
||||
return ctx.db
|
||||
.query("directories")
|
||||
@@ -202,14 +206,14 @@ export async function move(
|
||||
),
|
||||
)
|
||||
|
||||
const errors: Err.ApplicationErrorData[] = []
|
||||
const errors: ApplicationErrorData[] = []
|
||||
const okDirectories: DirectoryHandle[] = []
|
||||
conflictCheckResults.forEach((result, i) => {
|
||||
if (result.status === "fulfilled") {
|
||||
if (result.value) {
|
||||
errors.push(
|
||||
Err.createJson(
|
||||
Err.Code.Conflict,
|
||||
createErrorData(
|
||||
ErrorCode.Conflict,
|
||||
`Directory ${targetDirectory.id} already contains a directory with name ${result.value.name}`,
|
||||
),
|
||||
)
|
||||
@@ -217,7 +221,7 @@ export async function move(
|
||||
okDirectories.push(sourceDirectories[i]!)
|
||||
}
|
||||
} else if (result.status === "rejected") {
|
||||
errors.push(Err.createJson(Err.Code.Internal))
|
||||
errors.push(createErrorData(ErrorCode.Internal))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -243,7 +247,7 @@ export async function move(
|
||||
|
||||
for (const updateResult of results) {
|
||||
if (updateResult.status === "rejected") {
|
||||
errors.push(Err.createJson(Err.Code.Internal))
|
||||
errors.push(createErrorData(ErrorCode.Internal))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,11 +339,11 @@ export async function deletePermanently(
|
||||
|
||||
const deleteResults = await Promise.allSettled(deleteDirectoryPromises)
|
||||
|
||||
const errors: Err.ApplicationErrorData[] = []
|
||||
const errors: ApplicationErrorData[] = []
|
||||
let successfulDeletions = 0
|
||||
for (const result of deleteResults) {
|
||||
if (result.status === "rejected") {
|
||||
errors.push(Err.createJson(Err.Code.Internal))
|
||||
errors.push(createErrorData(ErrorCode.Internal))
|
||||
} else {
|
||||
successfulDeletions += 1
|
||||
}
|
||||
@@ -378,11 +382,11 @@ export async function restore(
|
||||
|
||||
const restoreResults = await Promise.allSettled(restoreDirectoryPromises)
|
||||
|
||||
const errors: Err.ApplicationErrorData[] = []
|
||||
const errors: ApplicationErrorData[] = []
|
||||
let successfulRestorations = 0
|
||||
for (const result of restoreResults) {
|
||||
if (result.status === "rejected") {
|
||||
errors.push(Err.createJson(Err.Code.Internal))
|
||||
errors.push(createErrorData(ErrorCode.Internal))
|
||||
} else {
|
||||
successfulRestorations += 1
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Doc, Id } from "@fileone/convex/dataModel"
|
||||
import { type AuthenticatedMutationCtx, authorizedGet } from "../functions"
|
||||
import * as Err from "../shared/error"
|
||||
import type { ApplicationErrorData } from "../shared/error"
|
||||
import { createErrorData, ErrorCode, error } from "../shared/error"
|
||||
import type { DirectoryHandle, FileHandle } from "../shared/filesystem"
|
||||
|
||||
export async function renameFile(
|
||||
@@ -27,10 +28,10 @@ export async function renameFile(
|
||||
.first()
|
||||
|
||||
if (existing) {
|
||||
throw Err.create(
|
||||
Err.Code.FileExists,
|
||||
`File with name ${newName} already exists in ${directoryId ? `directory ${directoryId}` : "root"}`,
|
||||
)
|
||||
error({
|
||||
code: ErrorCode.FileExists,
|
||||
message: `File with name ${newName} already exists in ${directoryId ? `directory ${directoryId}` : "root"}`,
|
||||
})
|
||||
}
|
||||
|
||||
await ctx.db.patch(itemId, { name: newName, updatedAt: Date.now() })
|
||||
@@ -50,10 +51,10 @@ export async function move(
|
||||
items.map((fileHandle) =>
|
||||
authorizedGet(ctx, fileHandle.id).then((f) => {
|
||||
if (!f) {
|
||||
throw Err.create(
|
||||
Err.Code.FileNotFound,
|
||||
`File ${fileHandle.id} not found`,
|
||||
)
|
||||
error({
|
||||
code: ErrorCode.NotFound,
|
||||
message: `File ${fileHandle.id} not found`,
|
||||
})
|
||||
}
|
||||
return ctx.db
|
||||
.query("files")
|
||||
@@ -69,14 +70,14 @@ export async function move(
|
||||
),
|
||||
)
|
||||
|
||||
const errors: Err.ApplicationErrorData[] = []
|
||||
const errors: ApplicationErrorData[] = []
|
||||
const okFiles: FileHandle[] = []
|
||||
conflictCheckResults.forEach((result, i) => {
|
||||
if (result.status === "fulfilled") {
|
||||
if (result.value) {
|
||||
errors.push(
|
||||
Err.createJson(
|
||||
Err.Code.Conflict,
|
||||
createErrorData(
|
||||
ErrorCode.Conflict,
|
||||
`Directory ${targetDirectoryHandle.id} already contains a file with name ${result.value.name}`,
|
||||
),
|
||||
)
|
||||
@@ -84,7 +85,7 @@ export async function move(
|
||||
okFiles.push(items[i])
|
||||
}
|
||||
} else if (result.status === "rejected") {
|
||||
errors.push(Err.createJson(Err.Code.Internal))
|
||||
errors.push(createErrorData(ErrorCode.Internal))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -99,7 +100,7 @@ export async function move(
|
||||
|
||||
for (const updateResult of results) {
|
||||
if (updateResult.status === "rejected") {
|
||||
errors.push(Err.createJson(Err.Code.Internal))
|
||||
errors.push(createErrorData(ErrorCode.Internal))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,11 +137,11 @@ export async function deletePermanently(
|
||||
|
||||
const deleteResults = await Promise.allSettled(deleteFilePromises)
|
||||
|
||||
const errors: Err.ApplicationErrorData[] = []
|
||||
const errors: ApplicationErrorData[] = []
|
||||
let successfulDeletions = 0
|
||||
for (const result of deleteResults) {
|
||||
if (result.status === "rejected") {
|
||||
errors.push(Err.createJson(Err.Code.Internal))
|
||||
errors.push(createErrorData(ErrorCode.Internal))
|
||||
} else {
|
||||
successfulDeletions += 1
|
||||
}
|
||||
@@ -179,11 +180,11 @@ export async function restore(
|
||||
|
||||
const restoreResults = await Promise.allSettled(restoreFilePromises)
|
||||
|
||||
const errors: Err.ApplicationErrorData[] = []
|
||||
const errors: ApplicationErrorData[] = []
|
||||
let successfulRestorations = 0
|
||||
for (const result of restoreResults) {
|
||||
if (result.status === "rejected") {
|
||||
errors.push(Err.createJson(Err.Code.Internal))
|
||||
errors.push(createErrorData(ErrorCode.Internal))
|
||||
} else {
|
||||
successfulRestorations += 1
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ConvexError } from "convex/values"
|
||||
import type { Doc, Id } from "../_generated/dataModel"
|
||||
import type { MutationCtx } from "../_generated/server"
|
||||
import type {
|
||||
@@ -5,7 +6,7 @@ import type {
|
||||
AuthenticatedMutationCtx,
|
||||
AuthenticatedQueryCtx,
|
||||
} from "../functions"
|
||||
import * as Err from "../shared/error"
|
||||
import { ErrorCode, error } from "../shared/error"
|
||||
|
||||
export async function create(
|
||||
ctx: MutationCtx,
|
||||
@@ -22,7 +23,7 @@ export async function create(
|
||||
})
|
||||
const doc = await ctx.db.get(id)
|
||||
if (!doc) {
|
||||
throw Err.create(Err.Code.Internal, "Failed to create file share")
|
||||
throw new ConvexError({ message: "Failed to create file share" })
|
||||
}
|
||||
return doc
|
||||
}
|
||||
@@ -46,11 +47,17 @@ export async function find(
|
||||
.withIndex("byShareToken", (q) => q.eq("shareToken", shareToken))
|
||||
.first()
|
||||
if (!doc) {
|
||||
throw Err.create(Err.Code.NotFound, "File share not found")
|
||||
error({
|
||||
code: ErrorCode.NotFound,
|
||||
message: "File share not found",
|
||||
})
|
||||
}
|
||||
|
||||
if (hasExpired(doc)) {
|
||||
throw Err.create(Err.Code.NotFound, "File share not found")
|
||||
error({
|
||||
code: ErrorCode.NotFound,
|
||||
message: "File share not found",
|
||||
})
|
||||
}
|
||||
|
||||
return doc
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { v } from "convex/values"
|
||||
import { ConvexError, v } from "convex/values"
|
||||
import type { Doc, Id } from "../_generated/dataModel"
|
||||
import {
|
||||
type AuthenticatedMutationCtx,
|
||||
type AuthenticatedQueryCtx,
|
||||
authorizedGet,
|
||||
} from "../functions"
|
||||
import * as Err from "../shared/error"
|
||||
import { ErrorCode, error } from "../shared/error"
|
||||
import type {
|
||||
DirectoryHandle,
|
||||
FileHandle,
|
||||
@@ -174,7 +174,10 @@ export async function deleteItemsPermanently(
|
||||
export async function emptyTrash(ctx: AuthenticatedMutationCtx) {
|
||||
const rootDir = await queryRootDirectory(ctx)
|
||||
if (!rootDir) {
|
||||
throw Err.create(Err.Code.NotFound, "user root directory not found")
|
||||
error({
|
||||
code: ErrorCode.NotFound,
|
||||
message: "user root directory not found",
|
||||
})
|
||||
}
|
||||
|
||||
const dirs = await ctx.db
|
||||
@@ -221,12 +224,18 @@ export async function fetchFileUrl(
|
||||
): Promise<string> {
|
||||
const file = await authorizedGet(ctx, fileId)
|
||||
if (!file) {
|
||||
throw Err.create(Err.Code.NotFound, "file not found")
|
||||
error({
|
||||
code: ErrorCode.NotFound,
|
||||
message: "file not found",
|
||||
})
|
||||
}
|
||||
|
||||
const url = await ctx.storage.getUrl(file.storageId)
|
||||
if (!url) {
|
||||
throw Err.create(Err.Code.NotFound, "file not found")
|
||||
error({
|
||||
code: ErrorCode.NotFound,
|
||||
message: "file not found",
|
||||
})
|
||||
}
|
||||
|
||||
return url
|
||||
@@ -238,7 +247,10 @@ export async function openFile(
|
||||
) {
|
||||
const file = await authorizedGet(ctx, fileId)
|
||||
if (!file) {
|
||||
throw Err.create(Err.Code.NotFound, "file not found")
|
||||
error({
|
||||
code: ErrorCode.NotFound,
|
||||
message: "file not found",
|
||||
})
|
||||
}
|
||||
|
||||
const fileShare = await FilePreview.find(ctx, {
|
||||
@@ -281,7 +293,10 @@ export async function saveFile(
|
||||
) {
|
||||
const directory = await authorizedGet(ctx, directoryId)
|
||||
if (!directory) {
|
||||
throw Err.create(Err.Code.NotFound, "directory not found")
|
||||
error({
|
||||
code: ErrorCode.NotFound,
|
||||
message: "directory not found",
|
||||
})
|
||||
}
|
||||
|
||||
const [fileMetadata, userInfo] = await Promise.all([
|
||||
@@ -289,7 +304,7 @@ export async function saveFile(
|
||||
User.queryInfo(ctx),
|
||||
])
|
||||
if (!fileMetadata || !userInfo) {
|
||||
throw Err.create(Err.Code.Internal, "Internal server error")
|
||||
throw new ConvexError({ message: "Internal server error" })
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -297,7 +312,10 @@ export async function saveFile(
|
||||
userInfo.storageQuotaBytes
|
||||
) {
|
||||
await ctx.storage.delete(storageId)
|
||||
throw Err.create(Err.Code.StorageQuotaExceeded, "Storage quota exceeded")
|
||||
error({
|
||||
code: ErrorCode.StorageQuotaExceeded,
|
||||
message: "Storage quota exceeded",
|
||||
})
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { MutationCtx, QueryCtx } from "@fileone/convex/server"
|
||||
import type { Doc } from "../_generated/dataModel"
|
||||
import { authComponent } from "../auth"
|
||||
import { type AuthenticatedQueryCtx, authorizedGet } from "../functions"
|
||||
import * as Err from "../shared/error"
|
||||
import { ErrorCode, error } from "../shared/error"
|
||||
|
||||
export type AuthUser = Awaited<ReturnType<typeof authComponent.getAuthUser>>
|
||||
|
||||
@@ -12,7 +12,10 @@ export type AuthUser = Awaited<ReturnType<typeof authComponent.getAuthUser>>
|
||||
export async function userIdentityOrThrow(ctx: QueryCtx | MutationCtx) {
|
||||
const identity = await ctx.auth.getUserIdentity()
|
||||
if (!identity) {
|
||||
throw Err.create(Err.Code.Unauthenticated, "Not authenticated")
|
||||
error({
|
||||
code: ErrorCode.Unauthenticated,
|
||||
message: "Not authenticated",
|
||||
})
|
||||
}
|
||||
return identity
|
||||
}
|
||||
|
||||
@@ -1,36 +1,44 @@
|
||||
import { ConvexError } from "convex/values"
|
||||
|
||||
export enum Code {
|
||||
export enum ErrorCode {
|
||||
Conflict = "Conflict",
|
||||
DirectoryExists = "DirectoryExists",
|
||||
DirectoryNotFound = "DirectoryNotFound",
|
||||
FileExists = "FileExists",
|
||||
FileNotFound = "FileNotFound",
|
||||
Internal = "Internal",
|
||||
Unauthenticated = "Unauthenticated",
|
||||
NotFound = "NotFound",
|
||||
StorageQuotaExceeded = "StorageQuotaExceeded",
|
||||
}
|
||||
|
||||
export type ApplicationErrorData = { code: Code; message?: string }
|
||||
export type ApplicationError = ConvexError<ApplicationErrorData>
|
||||
export type ApplicationErrorData = { code: ErrorCode; message?: string }
|
||||
|
||||
export function isApplicationError(error: unknown): error is ApplicationError {
|
||||
return error instanceof ConvexError && "code" in error.data
|
||||
export function isApplicationError(
|
||||
error: unknown,
|
||||
): error is ApplicationErrorData {
|
||||
return (
|
||||
error !== null &&
|
||||
typeof error === "object" &&
|
||||
"code" in error &&
|
||||
"message" in error &&
|
||||
Object.values(ErrorCode).includes(
|
||||
(error as { code: string }).code as ErrorCode,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export function create(code: Code, message?: string): ApplicationError {
|
||||
return new ConvexError({
|
||||
code,
|
||||
message:
|
||||
code === Code.Internal ? "Internal application error" : message,
|
||||
})
|
||||
}
|
||||
|
||||
export function createJson(code: Code, message?: string): ApplicationErrorData {
|
||||
export function createErrorData(
|
||||
code: ErrorCode,
|
||||
message?: string,
|
||||
): ApplicationErrorData {
|
||||
return {
|
||||
code,
|
||||
message:
|
||||
code === Code.Internal ? "Internal application error" : message,
|
||||
code === ErrorCode.Internal
|
||||
? "Internal application error"
|
||||
: message,
|
||||
}
|
||||
}
|
||||
|
||||
export function error(data: ApplicationErrorData): never {
|
||||
throw new ConvexError(data)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type { Doc, Id } from "@fileone/convex/dataModel"
|
||||
import type * as Err from "./error"
|
||||
import type { ApplicationErrorData } from "./error"
|
||||
|
||||
export enum FileType {
|
||||
File = "File",
|
||||
@@ -67,7 +67,7 @@ export type DeleteResult = {
|
||||
files: number
|
||||
directories: number
|
||||
}
|
||||
errors: Err.ApplicationErrorData[]
|
||||
errors: ApplicationErrorData[]
|
||||
}
|
||||
|
||||
export function newFileSystemHandle(item: FileSystemItem): FileSystemHandle {
|
||||
|
||||
Reference in New Issue
Block a user