Compare commits

..

9 Commits

Author SHA1 Message Date
ad99bca7fd style: rename unused param 2025-11-08 18:41:30 +00:00
b241f4e211 feat: add clear uploads button
add a clear upload button that is visible when there are upload errors

Co-authored-by: Ona <no-reply@ona.com>
2025-11-08 18:38:43 +00:00
027a315a04 style: apply biome formatting to config and generated files
- Convert spaces to tabs in tsconfig files
- Format CLI commands and prompts
- Update generated Convex files
- Format betterauth adapter and schema

Co-authored-by: Ona <no-reply@ona.com>
2025-11-08 18:03:32 +00:00
015524cd63 style: apply biome formatting to UI components
- Convert spaces to tabs for consistency
- Add 'type' modifier to React imports
- Format component code

Co-authored-by: Ona <no-reply@ona.com>
2025-11-08 18:03:26 +00:00
4ebb3fe620 style: format imports and code style
- Consolidate multi-line imports
- Apply consistent formatting

Co-authored-by: Ona <no-reply@ona.com>
2025-11-08 18:03:21 +00:00
b8c46217f7 chore: remove debug console.logs and add error handling
- Remove console.log statements from upload file dialog
- Add onError handler to display error toasts
- Update ErrorCode reference in use-file-drop

Co-authored-by: Ona <no-reply@ona.com>
2025-11-08 18:03:15 +00:00
94d6a22ab2 refactor: update remaining error imports to use ErrorCode
- Replace Err.Code with ErrorCode throughout convex model files
- Update error() function calls to use new signature
- Remove unused Err namespace imports

Co-authored-by: Ona <no-reply@ona.com>
2025-11-08 18:03:10 +00:00
f20f1a93c7 refactor: replace namespace import with direct type import
- Replace 'import type * as Err' with direct ApplicationErrorData import
- Update Err.ApplicationErrorData references to ApplicationErrorData

This improves code clarity and follows the project's import conventions.

Co-authored-by: Ona <no-reply@ona.com>
2025-11-08 17:56:42 +00:00
acfe1523df refactor: wrap all errors in ConvexError
- Update error() helper to throw ConvexError instead of plain objects
- Add isApplicationConvexError() type guard for client-side error checking
- Fix Vite config to include convex/values in optimizeDeps for proper instanceof checks
- Update error handling to check ConvexError wrapper and extract data property

This ensures all application errors are properly typed and can be identified
using instanceof ConvexError on the client side.

Co-authored-by: Ona <no-reply@ona.com>
2025-11-08 17:56:28 +00:00
41 changed files with 909 additions and 780 deletions

View File

@@ -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:"))

View File

@@ -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
View 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.

View File

@@ -1,28 +1,28 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"allowJs": true,
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

View File

@@ -1,39 +1,50 @@
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();
const testEndpoint = async (e: FormEvent<HTMLFormElement>) => {
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 });
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 data = await res.json();
responseInputRef.current!.value = JSON.stringify(data, null, 2);
} catch (error) {
responseInputRef.current!.value = String(error);
}
};
const data = await res.json()
responseInputRef.current!.value = JSON.stringify(data, null, 2)
} catch (error) {
responseInputRef.current!.value = String(error)
}
}
return (
<div className="api-tester">
<form onSubmit={testEndpoint} className="endpoint-row">
<select name="method" className="method">
<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" />
<button type="submit" className="send-button">
Send
</button>
</form>
<textarea ref={responseInputRef} readOnly placeholder="Response will appear here..." className="response-area" />
</div>
);
return (
<div className="api-tester">
<form onSubmit={testEndpoint} className="endpoint-row">
<select name="method" className="method">
<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"
/>
<button type="submit" className="send-button">
Send
</button>
</form>
<textarea
ref={responseInputRef}
readOnly
placeholder="Response will appear here..."
className="response-area"
/>
</div>
)
}

View File

@@ -1,92 +1,92 @@
import * as React from "react"
import type * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className,
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<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
)}
{...props}
/>
)
return (
<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,
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@@ -1,242 +1,241 @@
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 (
<fieldset
data-slot="field-set"
className={cn(
"flex flex-col gap-6",
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
className
)}
{...props}
/>
)
return (
<fieldset
data-slot="field-set"
className={cn(
"flex flex-col gap-6",
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
className,
)}
{...props}
/>
)
}
function FieldLegend({
className,
variant = "legend",
...props
className,
variant = "legend",
...props
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
return (
<legend
data-slot="field-legend"
data-variant={variant}
className={cn(
"mb-3 font-medium",
"data-[variant=legend]:text-base",
"data-[variant=label]:text-sm",
className
)}
{...props}
/>
)
return (
<legend
data-slot="field-legend"
data-variant={variant}
className={cn(
"mb-3 font-medium",
"data-[variant=legend]:text-base",
"data-[variant=label]:text-sm",
className,
)}
{...props}
/>
)
}
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<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
)}
{...props}
/>
)
return (
<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,
)}
{...props}
/>
)
}
const fieldVariants = cva(
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
{
variants: {
orientation: {
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
horizontal: [
"flex-row items-center",
"[&>[data-slot=field-label]]:flex-auto",
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
responsive: [
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
},
},
defaultVariants: {
orientation: "vertical",
},
}
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
{
variants: {
orientation: {
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
horizontal: [
"flex-row items-center",
"[&>[data-slot=field-label]]:flex-auto",
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
responsive: [
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
},
},
defaultVariants: {
orientation: "vertical",
},
},
)
function Field({
className,
orientation = "vertical",
...props
className,
orientation = "vertical",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
return (
<div
role="group"
data-slot="field"
data-orientation={orientation}
className={cn(fieldVariants({ orientation }), className)}
{...props}
/>
)
return (
<div
role="group"
data-slot="field"
data-orientation={orientation}
className={cn(fieldVariants({ orientation }), className)}
{...props}
/>
)
}
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-content"
className={cn(
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
className
)}
{...props}
/>
)
return (
<div
data-slot="field-content"
className={cn(
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
className,
)}
{...props}
/>
)
}
function FieldLabel({
className,
...props
className,
...props
}: React.ComponentProps<typeof Label>) {
return (
<Label
data-slot="field-label"
className={cn(
"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
)}
{...props}
/>
)
return (
<Label
data-slot="field-label"
className={cn(
"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,
)}
{...props}
/>
)
}
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<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
)}
{...props}
/>
)
return (
<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,
)}
{...props}
/>
)
}
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="field-description"
className={cn(
"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
)}
{...props}
/>
)
return (
<p
data-slot="field-description"
className={cn(
"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,
)}
{...props}
/>
)
}
function FieldSeparator({
children,
className,
...props
children,
className,
...props
}: React.ComponentProps<"div"> & {
children?: React.ReactNode
children?: React.ReactNode
}) {
return (
<div
data-slot="field-separator"
data-content={!!children}
className={cn(
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
className
)}
{...props}
>
<Separator className="absolute inset-0 top-1/2" />
{children && (
<span
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
data-slot="field-separator-content"
>
{children}
</span>
)}
</div>
)
return (
<div
data-slot="field-separator"
data-content={!!children}
className={cn(
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
className,
)}
{...props}
>
<Separator className="absolute inset-0 top-1/2" />
{children && (
<span
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
data-slot="field-separator-content"
>
{children}
</span>
)}
</div>
)
}
function FieldError({
className,
children,
errors,
...props
className,
children,
errors,
...props
}: React.ComponentProps<"div"> & {
errors?: Array<{ message?: string } | undefined>
errors?: Array<{ message?: string } | undefined>
}) {
const content = useMemo(() => {
if (children) {
return children
}
const content = useMemo(() => {
if (children) {
return children
}
if (!errors) {
return null
}
if (!errors) {
return null
}
if (errors?.length === 1 && errors[0]?.message) {
return errors[0].message
}
if (errors?.length === 1 && errors[0]?.message) {
return errors[0].message
}
return (
<ul className="ml-4 flex list-disc flex-col gap-1">
{errors.map(
(error, index) =>
error?.message && <li key={index}>{error.message}</li>
)}
</ul>
)
}, [children, errors])
return (
<ul className="ml-4 flex list-disc flex-col gap-1">
{errors.map(
(error, index) =>
error?.message && <li key={index}>{error.message}</li>,
)}
</ul>
)
}, [children, errors])
if (!content) {
return null
}
if (!content) {
return null
}
return (
<div
role="alert"
data-slot="field-error"
className={cn("text-destructive text-sm font-normal", className)}
{...props}
>
{content}
</div>
)
return (
<div
role="alert"
data-slot="field-error"
className={cn("text-destructive text-sm font-normal", className)}
{...props}
>
{content}
</div>
)
}
export {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldGroup,
FieldLegend,
FieldSeparator,
FieldSet,
FieldContent,
FieldTitle,
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldGroup,
FieldLegend,
FieldSeparator,
FieldSet,
FieldContent,
FieldTitle,
}

View File

@@ -1,22 +1,22 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import type * as React from "react"
import { cn } from "@/lib/utils"
function Label({
className,
...props
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
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
)}
{...props}
/>
)
return (
<LabelPrimitive.Root
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,
)}
{...props}
/>
)
}
export { Label }

View File

@@ -1,29 +1,29 @@
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import type * as React from "react"
import { cn } from "@/lib/utils"
function Progress({
className,
value,
...props
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
}
export { Progress }

View File

@@ -1,28 +1,28 @@
"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"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
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
)}
{...props}
/>
)
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
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,
)}
{...props}
/>
)
}
export { Separator }

View File

@@ -113,4 +113,4 @@ function RenameMenuItem() {
Rename
</ContextMenuItem>
)
}
}

View File

@@ -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"

View File

@@ -53,12 +53,9 @@ export const clearFileUploadStatusesAtom = atom(
},
)
export const clearAllFileUploadStatusesAtom = atom(
null,
(get, set) => {
set(fileUploadStatusesAtom, {})
},
)
export const clearAllFileUploadStatusesAtom = atom(null, (_, set) => {
set(fileUploadStatusesAtom, {})
})
export const fileUploadCountAtom = atom(
(get) => Object.keys(get(fileUploadStatusesAtom)).length,

View File

@@ -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

View File

@@ -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

View File

@@ -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)
toast.error(formatError(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) {

View File

@@ -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"],

View File

@@ -1,29 +1,29 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

View File

@@ -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
}
})
}

View File

@@ -1,11 +1,11 @@
{
"name": "@drexa/auth",
"module": "index.ts",
"type": "module",
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
}
"name": "@drexa/auth",
"module": "index.ts",
"type": "module",
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
}
}

View File

@@ -1,29 +1,29 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

View File

@@ -1,13 +1,13 @@
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,
findOne,
findMany,
updateOne,
updateMany,
deleteOne,
deleteMany,
} = createApi(schema, createAuth);
create,
findOne,
findMany,
updateOne,
updateMany,
deleteOne,
deleteMany,
} = createApi(schema, createAuth)

View File

@@ -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)
export const auth = getStaticAuth(createAuth)

View File

@@ -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

View File

@@ -2,69 +2,69 @@
// 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({
name: v.string(),
email: v.string(),
emailVerified: v.boolean(),
image: v.optional(v.union(v.null(), v.string())),
createdAt: v.number(),
updatedAt: v.number(),
userId: v.optional(v.union(v.null(), v.string())),
})
.index("email_name", ["email","name"])
.index("name", ["name"])
.index("userId", ["userId"]),
session: defineTable({
expiresAt: v.number(),
token: v.string(),
createdAt: v.number(),
updatedAt: v.number(),
ipAddress: v.optional(v.union(v.null(), v.string())),
userAgent: v.optional(v.union(v.null(), v.string())),
userId: v.string(),
})
.index("expiresAt", ["expiresAt"])
.index("expiresAt_userId", ["expiresAt","userId"])
.index("token", ["token"])
.index("userId", ["userId"]),
account: defineTable({
accountId: v.string(),
providerId: v.string(),
userId: v.string(),
accessToken: v.optional(v.union(v.null(), v.string())),
refreshToken: v.optional(v.union(v.null(), v.string())),
idToken: v.optional(v.union(v.null(), v.string())),
accessTokenExpiresAt: v.optional(v.union(v.null(), v.number())),
refreshTokenExpiresAt: v.optional(v.union(v.null(), v.number())),
scope: v.optional(v.union(v.null(), v.string())),
password: v.optional(v.union(v.null(), v.string())),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("accountId", ["accountId"])
.index("accountId_providerId", ["accountId","providerId"])
.index("providerId_userId", ["providerId","userId"])
.index("userId", ["userId"]),
verification: defineTable({
identifier: v.string(),
value: v.string(),
expiresAt: v.number(),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("expiresAt", ["expiresAt"])
.index("identifier", ["identifier"]),
jwks: defineTable({
publicKey: v.string(),
privateKey: v.string(),
createdAt: v.number(),
}),
};
user: defineTable({
name: v.string(),
email: v.string(),
emailVerified: v.boolean(),
image: v.optional(v.union(v.null(), v.string())),
createdAt: v.number(),
updatedAt: v.number(),
userId: v.optional(v.union(v.null(), v.string())),
})
.index("email_name", ["email", "name"])
.index("name", ["name"])
.index("userId", ["userId"]),
session: defineTable({
expiresAt: v.number(),
token: v.string(),
createdAt: v.number(),
updatedAt: v.number(),
ipAddress: v.optional(v.union(v.null(), v.string())),
userAgent: v.optional(v.union(v.null(), v.string())),
userId: v.string(),
})
.index("expiresAt", ["expiresAt"])
.index("expiresAt_userId", ["expiresAt", "userId"])
.index("token", ["token"])
.index("userId", ["userId"]),
account: defineTable({
accountId: v.string(),
providerId: v.string(),
userId: v.string(),
accessToken: v.optional(v.union(v.null(), v.string())),
refreshToken: v.optional(v.union(v.null(), v.string())),
idToken: v.optional(v.union(v.null(), v.string())),
accessTokenExpiresAt: v.optional(v.union(v.null(), v.number())),
refreshTokenExpiresAt: v.optional(v.union(v.null(), v.number())),
scope: v.optional(v.union(v.null(), v.string())),
password: v.optional(v.union(v.null(), v.string())),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("accountId", ["accountId"])
.index("accountId_providerId", ["accountId", "providerId"])
.index("providerId_userId", ["providerId", "userId"])
.index("userId", ["userId"]),
verification: defineTable({
identifier: v.string(),
value: v.string(),
expiresAt: v.number(),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("expiresAt", ["expiresAt"])
.index("identifier", ["identifier"]),
jwks: defineTable({
publicKey: v.string(),
privateKey: v.string(),
createdAt: v.number(),
}),
}
const schema = defineSchema(tables);
const schema = defineSchema(tables)
export default schema;
export default schema

View File

@@ -9,10 +9,10 @@
*/
import type {
ApiFromModules,
FilterApi,
FunctionReference,
} from "convex/server";
ApiFromModules,
FilterApi,
FunctionReference,
} 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">
>;
typeof fullApi,
FunctionReference<any, "public">
>
export declare const internal: FilterApi<
typeof fullApi,
FunctionReference<any, "internal">
>;
typeof fullApi,
FunctionReference<any, "internal">
>

View File

@@ -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

View File

@@ -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

View File

@@ -9,17 +9,17 @@
*/
import {
ActionBuilder,
HttpActionBuilder,
MutationBuilder,
QueryBuilder,
GenericActionCtx,
GenericMutationCtx,
GenericQueryCtx,
GenericDatabaseReader,
GenericDatabaseWriter,
} from "convex/server";
import type { DataModel } from "./dataModel.js";
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.
@@ -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>

View File

@@ -9,14 +9,14 @@
*/
import {
actionGeneric,
httpActionGeneric,
queryGeneric,
mutationGeneric,
internalActionGeneric,
internalMutationGeneric,
internalQueryGeneric,
} from "convex/server";
actionGeneric,
httpActionGeneric,
internalActionGeneric,
internalMutationGeneric,
internalQueryGeneric,
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

View File

@@ -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 })
},
})

View File

@@ -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
}
}

View File

@@ -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 }
},

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -1,25 +1,25 @@
{
/* This TypeScript project config describes the environment that
* Convex functions run in and is used to typecheck them.
* You can modify it, but some settings are required to use Convex.
*/
"compilerOptions": {
/* These settings are not required by Convex and can be modified. */
"allowJs": true,
"strict": true,
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
/* This TypeScript project config describes the environment that
* Convex functions run in and is used to typecheck them.
* You can modify it, but some settings are required to use Convex.
*/
"compilerOptions": {
/* These settings are not required by Convex and can be modified. */
"allowJs": true,
"strict": true,
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
/* These compiler options are required by Convex */
"target": "ESNext",
"lib": ["ES2021", "dom"],
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"isolatedModules": true,
"noEmit": true
},
"include": ["./**/*"],
"exclude": ["./_generated"]
/* These compiler options are required by Convex */
"target": "ESNext",
"lib": ["ES2021", "dom"],
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"isolatedModules": true,
"noEmit": true
},
"include": ["./**/*"],
"exclude": ["./_generated"]
}