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 { generateApiKey, newPrefix } from "@drexa/auth"
|
||||||
import chalk from "chalk"
|
import chalk from "chalk"
|
||||||
import { Command } from "commander"
|
import { Command } from "commander"
|
||||||
import {
|
import { promptNumber, promptOptionalDate, promptText } from "../../prompts.ts"
|
||||||
promptNumber,
|
|
||||||
promptOptionalDate,
|
|
||||||
promptText,
|
|
||||||
} from "../../prompts.ts"
|
|
||||||
|
|
||||||
export const apikeyCommand = new Command("apikey")
|
export const apikeyCommand = new Command("apikey")
|
||||||
.description("Generate a new API key")
|
.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.green(` ${result.unhashedKey}\n`))
|
||||||
console.log(chalk.gray("─".repeat(60)))
|
console.log(chalk.gray("─".repeat(60)))
|
||||||
console.log(
|
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.dim(` ${result.hashedKey}\n`))
|
||||||
console.log(chalk.bold("Description:"))
|
console.log(chalk.bold("Description:"))
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ export async function promptNumber(
|
|||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
const rl = createReadlineInterface()
|
const rl = createReadlineInterface()
|
||||||
try {
|
try {
|
||||||
const defaultStr = defaultValue ? chalk.dim(` (default: ${defaultValue})`) : ""
|
const defaultStr = defaultValue
|
||||||
|
? chalk.dim(` (default: ${defaultValue})`)
|
||||||
|
: ""
|
||||||
const input = await rl.question(chalk.cyan(`${message}${defaultStr} `))
|
const input = await rl.question(chalk.cyan(`${message}${defaultStr} `))
|
||||||
|
|
||||||
if ((!input || input.trim() === "") && defaultValue !== undefined) {
|
if ((!input || input.trim() === "") && defaultValue !== undefined) {
|
||||||
@@ -59,7 +61,8 @@ export async function promptOptionalDate(
|
|||||||
const rl = createReadlineInterface()
|
const rl = createReadlineInterface()
|
||||||
try {
|
try {
|
||||||
const input = await rl.question(
|
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() === "") {
|
if (!input || input.trim() === "") {
|
||||||
@@ -68,7 +71,9 @@ export async function promptOptionalDate(
|
|||||||
|
|
||||||
const date = new Date(input.trim())
|
const date = new Date(input.trim())
|
||||||
if (Number.isNaN(date.getTime())) {
|
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)
|
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,28 +1,28 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Environment setup & latest features
|
// Environment setup & latest features
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "Preserve",
|
"module": "Preserve",
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
|
||||||
// Bundler mode
|
// Bundler mode
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
|
||||||
// Best practices
|
// Best practices
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
// Some stricter flags (disabled by default)
|
// Some stricter flags (disabled by default)
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"noPropertyAccessFromIndexSignature": false
|
"noPropertyAccessFromIndexSignature": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,50 @@
|
|||||||
import { useRef, type FormEvent } from "react";
|
import { type FormEvent, useRef } from "react"
|
||||||
|
|
||||||
export function APITester() {
|
export function APITester() {
|
||||||
const responseInputRef = useRef<HTMLTextAreaElement>(null);
|
const responseInputRef = useRef<HTMLTextAreaElement>(null)
|
||||||
|
|
||||||
const testEndpoint = async (e: FormEvent<HTMLFormElement>) => {
|
const testEndpoint = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const form = e.currentTarget;
|
const form = e.currentTarget
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form)
|
||||||
const endpoint = formData.get("endpoint") as string;
|
const endpoint = formData.get("endpoint") as string
|
||||||
const url = new URL(endpoint, location.href);
|
const url = new URL(endpoint, location.href)
|
||||||
const method = formData.get("method") as string;
|
const method = formData.get("method") as string
|
||||||
const res = await fetch(url, { method });
|
const res = await fetch(url, { method })
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json()
|
||||||
responseInputRef.current!.value = JSON.stringify(data, null, 2);
|
responseInputRef.current!.value = JSON.stringify(data, null, 2)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
responseInputRef.current!.value = String(error);
|
responseInputRef.current!.value = String(error)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="api-tester">
|
<div className="api-tester">
|
||||||
<form onSubmit={testEndpoint} className="endpoint-row">
|
<form onSubmit={testEndpoint} className="endpoint-row">
|
||||||
<select name="method" className="method">
|
<select name="method" className="method">
|
||||||
<option value="GET">GET</option>
|
<option value="GET">GET</option>
|
||||||
<option value="PUT">PUT</option>
|
<option value="PUT">PUT</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="text" name="endpoint" defaultValue="/api/hello" className="url-input" placeholder="/api/hello" />
|
<input
|
||||||
<button type="submit" className="send-button">
|
type="text"
|
||||||
Send
|
name="endpoint"
|
||||||
</button>
|
defaultValue="/api/hello"
|
||||||
</form>
|
className="url-input"
|
||||||
<textarea ref={responseInputRef} readOnly placeholder="Response will appear here..." className="response-area" />
|
placeholder="/api/hello"
|
||||||
</div>
|
/>
|
||||||
);
|
<button type="submit" className="send-button">
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<textarea
|
||||||
|
ref={responseInputRef}
|
||||||
|
readOnly
|
||||||
|
placeholder="Response will appear here..."
|
||||||
|
className="response-area"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,92 +1,92 @@
|
|||||||
import * as React from "react"
|
import type * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-header"
|
data-slot="card-header"
|
||||||
className={cn(
|
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",
|
"@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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-title"
|
data-slot="card-title"
|
||||||
className={cn("leading-none font-semibold", className)}
|
className={cn("leading-none font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-description"
|
data-slot="card-description"
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-action"
|
data-slot="card-action"
|
||||||
className={cn(
|
className={cn(
|
||||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-content"
|
data-slot="card-content"
|
||||||
className={cn("px-6", className)}
|
className={cn("px-6", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-footer"
|
data-slot="card-footer"
|
||||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
CardAction,
|
CardAction,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardContent,
|
CardContent,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,242 +1,241 @@
|
|||||||
import { useMemo } from "react"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { useMemo } from "react"
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
||||||
return (
|
return (
|
||||||
<fieldset
|
<fieldset
|
||||||
data-slot="field-set"
|
data-slot="field-set"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col gap-6",
|
"flex flex-col gap-6",
|
||||||
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldLegend({
|
function FieldLegend({
|
||||||
className,
|
className,
|
||||||
variant = "legend",
|
variant = "legend",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
|
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
|
||||||
return (
|
return (
|
||||||
<legend
|
<legend
|
||||||
data-slot="field-legend"
|
data-slot="field-legend"
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(
|
className={cn(
|
||||||
"mb-3 font-medium",
|
"mb-3 font-medium",
|
||||||
"data-[variant=legend]:text-base",
|
"data-[variant=legend]:text-base",
|
||||||
"data-[variant=label]:text-sm",
|
"data-[variant=label]:text-sm",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="field-group"
|
data-slot="field-group"
|
||||||
className={cn(
|
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",
|
"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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldVariants = cva(
|
const fieldVariants = cva(
|
||||||
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
|
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
orientation: {
|
orientation: {
|
||||||
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
|
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
|
||||||
horizontal: [
|
horizontal: [
|
||||||
"flex-row items-center",
|
"flex-row items-center",
|
||||||
"[&>[data-slot=field-label]]:flex-auto",
|
"[&>[data-slot=field-label]]:flex-auto",
|
||||||
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||||
],
|
],
|
||||||
responsive: [
|
responsive: [
|
||||||
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
|
"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:[&>[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",
|
"@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: {
|
defaultVariants: {
|
||||||
orientation: "vertical",
|
orientation: "vertical",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
function Field({
|
function Field({
|
||||||
className,
|
className,
|
||||||
orientation = "vertical",
|
orientation = "vertical",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
data-slot="field"
|
data-slot="field"
|
||||||
data-orientation={orientation}
|
data-orientation={orientation}
|
||||||
className={cn(fieldVariants({ orientation }), className)}
|
className={cn(fieldVariants({ orientation }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="field-content"
|
data-slot="field-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldLabel({
|
function FieldLabel({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Label>) {
|
}: React.ComponentProps<typeof Label>) {
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
data-slot="field-label"
|
data-slot="field-label"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
"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-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",
|
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="field-label"
|
data-slot="field-label"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
|
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
data-slot="field-description"
|
data-slot="field-description"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
|
"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",
|
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
|
||||||
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldSeparator({
|
function FieldSeparator({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & {
|
}: React.ComponentProps<"div"> & {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="field-separator"
|
data-slot="field-separator"
|
||||||
data-content={!!children}
|
data-content={!!children}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Separator className="absolute inset-0 top-1/2" />
|
<Separator className="absolute inset-0 top-1/2" />
|
||||||
{children && (
|
{children && (
|
||||||
<span
|
<span
|
||||||
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
|
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
|
||||||
data-slot="field-separator-content"
|
data-slot="field-separator-content"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldError({
|
function FieldError({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
errors,
|
errors,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & {
|
}: React.ComponentProps<"div"> & {
|
||||||
errors?: Array<{ message?: string } | undefined>
|
errors?: Array<{ message?: string } | undefined>
|
||||||
}) {
|
}) {
|
||||||
const content = useMemo(() => {
|
const content = useMemo(() => {
|
||||||
if (children) {
|
if (children) {
|
||||||
return children
|
return children
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!errors) {
|
if (!errors) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errors?.length === 1 && errors[0]?.message) {
|
if (errors?.length === 1 && errors[0]?.message) {
|
||||||
return errors[0].message
|
return errors[0].message
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="ml-4 flex list-disc flex-col gap-1">
|
<ul className="ml-4 flex list-disc flex-col gap-1">
|
||||||
{errors.map(
|
{errors.map(
|
||||||
(error, index) =>
|
(error, index) =>
|
||||||
error?.message && <li key={index}>{error.message}</li>
|
error?.message && <li key={index}>{error.message}</li>,
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
)
|
)
|
||||||
}, [children, errors])
|
}, [children, errors])
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
data-slot="field-error"
|
data-slot="field-error"
|
||||||
className={cn("text-destructive text-sm font-normal", className)}
|
className={cn("text-destructive text-sm font-normal", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Field,
|
Field,
|
||||||
FieldLabel,
|
FieldLabel,
|
||||||
FieldDescription,
|
FieldDescription,
|
||||||
FieldError,
|
FieldError,
|
||||||
FieldGroup,
|
FieldGroup,
|
||||||
FieldLegend,
|
FieldLegend,
|
||||||
FieldSeparator,
|
FieldSeparator,
|
||||||
FieldSet,
|
FieldSet,
|
||||||
FieldContent,
|
FieldContent,
|
||||||
FieldTitle,
|
FieldTitle,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
import type * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function Label({
|
function Label({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<LabelPrimitive.Root
|
<LabelPrimitive.Root
|
||||||
data-slot="label"
|
data-slot="label"
|
||||||
className={cn(
|
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",
|
"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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Label }
|
export { Label }
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||||
|
import type * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function Progress({
|
function Progress({
|
||||||
className,
|
className,
|
||||||
value,
|
value,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<ProgressPrimitive.Root
|
<ProgressPrimitive.Root
|
||||||
data-slot="progress"
|
data-slot="progress"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ProgressPrimitive.Indicator
|
<ProgressPrimitive.Indicator
|
||||||
data-slot="progress-indicator"
|
data-slot="progress-indicator"
|
||||||
className="bg-primary h-full w-full flex-1 transition-all"
|
className="bg-primary h-full w-full flex-1 transition-all"
|
||||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
/>
|
/>
|
||||||
</ProgressPrimitive.Root>
|
</ProgressPrimitive.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Progress }
|
export { Progress }
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||||
|
import type * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function Separator({
|
function Separator({
|
||||||
className,
|
className,
|
||||||
orientation = "horizontal",
|
orientation = "horizontal",
|
||||||
decorative = true,
|
decorative = true,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<SeparatorPrimitive.Root
|
<SeparatorPrimitive.Root
|
||||||
data-slot="separator"
|
data-slot="separator"
|
||||||
decorative={decorative}
|
decorative={decorative}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
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",
|
"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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Separator }
|
export { Separator }
|
||||||
|
|||||||
@@ -113,4 +113,4 @@ function RenameMenuItem() {
|
|||||||
Rename
|
Rename
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import type { Id } from "@fileone/convex/dataModel"
|
import type { Id } from "@fileone/convex/dataModel"
|
||||||
import type {
|
import type { DirectoryItem, DirectoryItemKind } from "@fileone/convex/types"
|
||||||
DirectoryItem,
|
|
||||||
DirectoryItemKind,
|
|
||||||
} from "@fileone/convex/types"
|
|
||||||
import type { RowSelectionState } from "@tanstack/react-table"
|
import type { RowSelectionState } from "@tanstack/react-table"
|
||||||
import { atom } from "jotai"
|
import { atom } from "jotai"
|
||||||
|
|
||||||
|
|||||||
@@ -53,12 +53,9 @@ export const clearFileUploadStatusesAtom = atom(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export const clearAllFileUploadStatusesAtom = atom(
|
export const clearAllFileUploadStatusesAtom = atom(null, (_, set) => {
|
||||||
null,
|
set(fileUploadStatusesAtom, {})
|
||||||
(get, set) => {
|
})
|
||||||
set(fileUploadStatusesAtom, {})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export const fileUploadCountAtom = atom(
|
export const fileUploadCountAtom = atom(
|
||||||
(get) => Object.keys(get(fileUploadStatusesAtom)).length,
|
(get) => Object.keys(get(fileUploadStatusesAtom)).length,
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ function useUploadFilesAtom({
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.log("error", error)
|
|
||||||
store.set(
|
store.set(
|
||||||
fileUploadStatusAtomFamily(pickedFile.id),
|
fileUploadStatusAtomFamily(pickedFile.id),
|
||||||
{
|
{
|
||||||
@@ -130,6 +129,9 @@ function useUploadFilesAtom({
|
|||||||
toast.success("All files uploaded successfully")
|
toast.success("All files uploaded successfully")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(formatError(error))
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
[uploadFile, store.set],
|
[uploadFile, store.set],
|
||||||
)
|
)
|
||||||
@@ -270,6 +272,7 @@ export function UploadFileDialog({
|
|||||||
onClick={openFilePicker}
|
onClick={openFilePicker}
|
||||||
uploadFilesAtom={uploadFilesAtom}
|
uploadFilesAtom={uploadFilesAtom}
|
||||||
/>
|
/>
|
||||||
|
<ClearUploadErrorsButton />
|
||||||
<UploadButton
|
<UploadButton
|
||||||
uploadFilesAtom={uploadFilesAtom}
|
uploadFilesAtom={uploadFilesAtom}
|
||||||
onClick={onUploadButtonClick}
|
onClick={onUploadButtonClick}
|
||||||
@@ -373,10 +376,10 @@ function SelectMoreFilesButton({
|
|||||||
uploadFilesAtom: UploadFilesAtom
|
uploadFilesAtom: UploadFilesAtom
|
||||||
}) {
|
}) {
|
||||||
const pickedFiles = useAtomValue(pickedFilesAtom)
|
const pickedFiles = useAtomValue(pickedFilesAtom)
|
||||||
const { data: uploadResults, isPending: isUploading } =
|
const fileUploadCount = useAtomValue(fileUploadCountAtom)
|
||||||
useAtomValue(uploadFilesAtom)
|
const { isPending: isUploading } = useAtomValue(uploadFilesAtom)
|
||||||
|
|
||||||
if (pickedFiles.length === 0 || uploadResults) {
|
if (pickedFiles.length === 0 || fileUploadCount > 0) {
|
||||||
return null
|
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({
|
function UploadButton({
|
||||||
uploadFilesAtom,
|
uploadFilesAtom,
|
||||||
onClick,
|
onClick,
|
||||||
@@ -533,7 +559,6 @@ function PickedFileItem({
|
|||||||
}) {
|
}) {
|
||||||
const fileUploadAtom = fileUploadStatusAtomFamily(pickedFile.id)
|
const fileUploadAtom = fileUploadStatusAtomFamily(pickedFile.id)
|
||||||
const fileUpload = useAtomValue(fileUploadAtom)
|
const fileUpload = useAtomValue(fileUploadAtom)
|
||||||
console.log("fileUpload", fileUpload)
|
|
||||||
const { file, id } = pickedFile
|
const { file, id } = pickedFile
|
||||||
|
|
||||||
let statusIndicator: React.ReactNode
|
let statusIndicator: React.ReactNode
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function useFileDrop({
|
|||||||
errors: Err.ApplicationErrorData[]
|
errors: Err.ApplicationErrorData[]
|
||||||
}) => {
|
}) => {
|
||||||
const conflictCount = errors.reduce((acc, error) => {
|
const conflictCount = errors.reduce((acc, error) => {
|
||||||
if (error.code === Err.Code.Conflict) {
|
if (error.code === Err.ErrorCode.Conflict) {
|
||||||
return acc + 1
|
return acc + 1
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
Code as ErrorCode,
|
type ApplicationErrorData,
|
||||||
|
ErrorCode,
|
||||||
isApplicationError,
|
isApplicationError,
|
||||||
} from "@fileone/convex/error"
|
} from "@fileone/convex/error"
|
||||||
|
import { ConvexError } from "convex/values"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
const ERROR_MESSAGE = {
|
const ERROR_MESSAGE = {
|
||||||
@@ -9,13 +11,19 @@ const ERROR_MESSAGE = {
|
|||||||
[ErrorCode.FileExists]: "File already exists",
|
[ErrorCode.FileExists]: "File already exists",
|
||||||
[ErrorCode.Internal]: "Internal application error",
|
[ErrorCode.Internal]: "Internal application error",
|
||||||
[ErrorCode.Conflict]: "Conflict",
|
[ErrorCode.Conflict]: "Conflict",
|
||||||
[ErrorCode.DirectoryNotFound]: "Directory not found",
|
|
||||||
[ErrorCode.FileNotFound]: "File not found",
|
|
||||||
[ErrorCode.Unauthenticated]: "Unauthenticated",
|
[ErrorCode.Unauthenticated]: "Unauthenticated",
|
||||||
|
[ErrorCode.NotFound]: "Not found",
|
||||||
|
[ErrorCode.StorageQuotaExceeded]: "Storage is full",
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
export function isApplicationConvexError(
|
||||||
|
error: unknown,
|
||||||
|
): error is ConvexError<ApplicationErrorData> {
|
||||||
|
return error instanceof ConvexError && isApplicationError(error.data)
|
||||||
|
}
|
||||||
|
|
||||||
export function formatError(error: unknown): string {
|
export function formatError(error: unknown): string {
|
||||||
if (isApplicationError(error)) {
|
if (isApplicationConvexError(error)) {
|
||||||
return ERROR_MESSAGE[error.data.code]
|
return ERROR_MESSAGE[error.data.code]
|
||||||
}
|
}
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
@@ -25,8 +33,12 @@ export function formatError(error: unknown): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function defaultOnError(error: unknown) {
|
export function defaultOnError(error: unknown) {
|
||||||
console.log(error)
|
if (isApplicationConvexError(error)) {
|
||||||
toast.error(formatError(error))
|
toast.error(formatError(error))
|
||||||
|
} else {
|
||||||
|
console.error("Catastrophic error:", error)
|
||||||
|
toast.error("An unexpected error occurred")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withDefaultOnError(fn: (error: unknown) => void) {
|
export function withDefaultOnError(fn: (error: unknown) => void) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import path from "node:path"
|
||||||
import tailwindcss from "@tailwindcss/vite"
|
import tailwindcss from "@tailwindcss/vite"
|
||||||
import { TanStackRouterVite } from "@tanstack/router-plugin/vite"
|
import { TanStackRouterVite } from "@tanstack/router-plugin/vite"
|
||||||
import react from "@vitejs/plugin-react"
|
import react from "@vitejs/plugin-react"
|
||||||
import path from "path"
|
|
||||||
import { defineConfig } from "vite"
|
import { defineConfig } from "vite"
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -19,7 +19,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
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
|
// 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
|
// Vite's esbuild incorrectly transpiles better-call dependency causing 'super' keyword errors
|
||||||
exclude: ["better-auth", "@convex-dev/better-auth"],
|
exclude: ["better-auth", "@convex-dev/better-auth"],
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Environment setup & latest features
|
// Environment setup & latest features
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "Preserve",
|
"module": "Preserve",
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
|
||||||
// Bundler mode
|
// Bundler mode
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
|
||||||
// Best practices
|
// Best practices
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
// Some stricter flags (disabled by default)
|
// Some stricter flags (disabled by default)
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"noPropertyAccessFromIndexSignature": false
|
"noPropertyAccessFromIndexSignature": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,10 +62,14 @@ export class WebCryptoSha256Hasher implements PassswordHasher {
|
|||||||
}
|
}
|
||||||
return btoa(binary).replace(/[+/=]/g, (char) => {
|
return btoa(binary).replace(/[+/=]/g, (char) => {
|
||||||
switch (char) {
|
switch (char) {
|
||||||
case "+": return "-"
|
case "+":
|
||||||
case "/": return "_"
|
return "-"
|
||||||
case "=": return ""
|
case "/":
|
||||||
default: return char
|
return "_"
|
||||||
|
case "=":
|
||||||
|
return ""
|
||||||
|
default:
|
||||||
|
return char
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "@drexa/auth",
|
"name": "@drexa/auth",
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest"
|
"@types/bun": "latest"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Environment setup & latest features
|
// Environment setup & latest features
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "Preserve",
|
"module": "Preserve",
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
|
||||||
// Bundler mode
|
// Bundler mode
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
|
||||||
// Best practices
|
// Best practices
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
// Some stricter flags (disabled by default)
|
// Some stricter flags (disabled by default)
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"noPropertyAccessFromIndexSignature": false
|
"noPropertyAccessFromIndexSignature": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { createApi } from "@convex-dev/better-auth";
|
import { createApi } from "@convex-dev/better-auth"
|
||||||
import schema from "./schema";
|
import { createAuth } from "../auth"
|
||||||
import { createAuth } from "../auth";
|
import schema from "./schema"
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
create,
|
create,
|
||||||
findOne,
|
findOne,
|
||||||
findMany,
|
findMany,
|
||||||
updateOne,
|
updateOne,
|
||||||
updateMany,
|
updateMany,
|
||||||
deleteOne,
|
deleteOne,
|
||||||
deleteMany,
|
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 a static instance for Better Auth schema generation
|
||||||
export const auth = getStaticAuth(createAuth)
|
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,69 +2,69 @@
|
|||||||
// To regenerate the schema, run:
|
// To regenerate the schema, run:
|
||||||
// `npx @better-auth/cli generate --output undefined -y`
|
// `npx @better-auth/cli generate --output undefined -y`
|
||||||
|
|
||||||
import { defineSchema, defineTable } from "convex/server";
|
import { defineSchema, defineTable } from "convex/server"
|
||||||
import { v } from "convex/values";
|
import { v } from "convex/values"
|
||||||
|
|
||||||
export const tables = {
|
export const tables = {
|
||||||
user: defineTable({
|
user: defineTable({
|
||||||
name: v.string(),
|
name: v.string(),
|
||||||
email: v.string(),
|
email: v.string(),
|
||||||
emailVerified: v.boolean(),
|
emailVerified: v.boolean(),
|
||||||
image: v.optional(v.union(v.null(), v.string())),
|
image: v.optional(v.union(v.null(), v.string())),
|
||||||
createdAt: v.number(),
|
createdAt: v.number(),
|
||||||
updatedAt: v.number(),
|
updatedAt: v.number(),
|
||||||
userId: v.optional(v.union(v.null(), v.string())),
|
userId: v.optional(v.union(v.null(), v.string())),
|
||||||
})
|
})
|
||||||
.index("email_name", ["email","name"])
|
.index("email_name", ["email", "name"])
|
||||||
.index("name", ["name"])
|
.index("name", ["name"])
|
||||||
.index("userId", ["userId"]),
|
.index("userId", ["userId"]),
|
||||||
session: defineTable({
|
session: defineTable({
|
||||||
expiresAt: v.number(),
|
expiresAt: v.number(),
|
||||||
token: v.string(),
|
token: v.string(),
|
||||||
createdAt: v.number(),
|
createdAt: v.number(),
|
||||||
updatedAt: v.number(),
|
updatedAt: v.number(),
|
||||||
ipAddress: v.optional(v.union(v.null(), v.string())),
|
ipAddress: v.optional(v.union(v.null(), v.string())),
|
||||||
userAgent: v.optional(v.union(v.null(), v.string())),
|
userAgent: v.optional(v.union(v.null(), v.string())),
|
||||||
userId: v.string(),
|
userId: v.string(),
|
||||||
})
|
})
|
||||||
.index("expiresAt", ["expiresAt"])
|
.index("expiresAt", ["expiresAt"])
|
||||||
.index("expiresAt_userId", ["expiresAt","userId"])
|
.index("expiresAt_userId", ["expiresAt", "userId"])
|
||||||
.index("token", ["token"])
|
.index("token", ["token"])
|
||||||
.index("userId", ["userId"]),
|
.index("userId", ["userId"]),
|
||||||
account: defineTable({
|
account: defineTable({
|
||||||
accountId: v.string(),
|
accountId: v.string(),
|
||||||
providerId: v.string(),
|
providerId: v.string(),
|
||||||
userId: v.string(),
|
userId: v.string(),
|
||||||
accessToken: v.optional(v.union(v.null(), v.string())),
|
accessToken: v.optional(v.union(v.null(), v.string())),
|
||||||
refreshToken: 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())),
|
idToken: v.optional(v.union(v.null(), v.string())),
|
||||||
accessTokenExpiresAt: v.optional(v.union(v.null(), v.number())),
|
accessTokenExpiresAt: v.optional(v.union(v.null(), v.number())),
|
||||||
refreshTokenExpiresAt: 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())),
|
scope: v.optional(v.union(v.null(), v.string())),
|
||||||
password: v.optional(v.union(v.null(), v.string())),
|
password: v.optional(v.union(v.null(), v.string())),
|
||||||
createdAt: v.number(),
|
createdAt: v.number(),
|
||||||
updatedAt: v.number(),
|
updatedAt: v.number(),
|
||||||
})
|
})
|
||||||
.index("accountId", ["accountId"])
|
.index("accountId", ["accountId"])
|
||||||
.index("accountId_providerId", ["accountId","providerId"])
|
.index("accountId_providerId", ["accountId", "providerId"])
|
||||||
.index("providerId_userId", ["providerId","userId"])
|
.index("providerId_userId", ["providerId", "userId"])
|
||||||
.index("userId", ["userId"]),
|
.index("userId", ["userId"]),
|
||||||
verification: defineTable({
|
verification: defineTable({
|
||||||
identifier: v.string(),
|
identifier: v.string(),
|
||||||
value: v.string(),
|
value: v.string(),
|
||||||
expiresAt: v.number(),
|
expiresAt: v.number(),
|
||||||
createdAt: v.number(),
|
createdAt: v.number(),
|
||||||
updatedAt: v.number(),
|
updatedAt: v.number(),
|
||||||
})
|
})
|
||||||
.index("expiresAt", ["expiresAt"])
|
.index("expiresAt", ["expiresAt"])
|
||||||
.index("identifier", ["identifier"]),
|
.index("identifier", ["identifier"]),
|
||||||
jwks: defineTable({
|
jwks: defineTable({
|
||||||
publicKey: v.string(),
|
publicKey: v.string(),
|
||||||
privateKey: v.string(),
|
privateKey: v.string(),
|
||||||
createdAt: v.number(),
|
createdAt: v.number(),
|
||||||
}),
|
}),
|
||||||
};
|
}
|
||||||
|
|
||||||
const schema = defineSchema(tables);
|
const schema = defineSchema(tables)
|
||||||
|
|
||||||
export default schema;
|
export default schema
|
||||||
|
|||||||
22
packages/convex/convex/_generated/api.d.ts
vendored
22
packages/convex/convex/_generated/api.d.ts
vendored
@@ -9,10 +9,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiFromModules,
|
ApiFromModules,
|
||||||
FilterApi,
|
FilterApi,
|
||||||
FunctionReference,
|
FunctionReference,
|
||||||
} from "convex/server";
|
} from "convex/server"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A utility for referencing Convex functions in your app's API.
|
* A utility for referencing Convex functions in your app's API.
|
||||||
@@ -22,12 +22,12 @@ import type {
|
|||||||
* const myFunctionReference = api.myModule.myFunction;
|
* const myFunctionReference = api.myModule.myFunction;
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
declare const fullApi: ApiFromModules<{}>;
|
declare const fullApi: ApiFromModules<{}>
|
||||||
export declare const api: FilterApi<
|
export declare const api: FilterApi<
|
||||||
typeof fullApi,
|
typeof fullApi,
|
||||||
FunctionReference<any, "public">
|
FunctionReference<any, "public">
|
||||||
>;
|
>
|
||||||
export declare const internal: FilterApi<
|
export declare const internal: FilterApi<
|
||||||
typeof fullApi,
|
typeof fullApi,
|
||||||
FunctionReference<any, "internal">
|
FunctionReference<any, "internal">
|
||||||
>;
|
>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { anyApi } from "convex/server";
|
import { anyApi } from "convex/server"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A utility for referencing Convex functions in your app's API.
|
* 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;
|
* const myFunctionReference = api.myModule.myFunction;
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const api = anyApi;
|
export const api = anyApi
|
||||||
export const internal = 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
|
* @module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AnyDataModel } from "convex/server";
|
import { AnyDataModel } from "convex/server"
|
||||||
import type { GenericId } from "convex/values";
|
import type { GenericId } from "convex/values"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No `schema.ts` file found!
|
* No `schema.ts` file found!
|
||||||
@@ -25,12 +25,12 @@ import type { GenericId } from "convex/values";
|
|||||||
/**
|
/**
|
||||||
* The names of all of your Convex tables.
|
* The names of all of your Convex tables.
|
||||||
*/
|
*/
|
||||||
export type TableNames = string;
|
export type TableNames = string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of a document stored in Convex.
|
* The type of a document stored in Convex.
|
||||||
*/
|
*/
|
||||||
export type Doc = any;
|
export type Doc = any
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An identifier for a document in Convex.
|
* 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
|
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
||||||
* strings when type checking.
|
* strings when type checking.
|
||||||
*/
|
*/
|
||||||
export type Id<TableName extends TableNames = TableNames> =
|
export type Id<TableName extends TableNames = TableNames> = GenericId<TableName>
|
||||||
GenericId<TableName>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A type describing your Convex data model.
|
* 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
|
* This type is used to parameterize methods like `queryGeneric` and
|
||||||
* `mutationGeneric` to make them type-safe.
|
* `mutationGeneric` to make them type-safe.
|
||||||
*/
|
*/
|
||||||
export type DataModel = AnyDataModel;
|
export type DataModel = AnyDataModel
|
||||||
|
|||||||
46
packages/convex/convex/_generated/server.d.ts
vendored
46
packages/convex/convex/_generated/server.d.ts
vendored
@@ -9,17 +9,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ActionBuilder,
|
ActionBuilder,
|
||||||
HttpActionBuilder,
|
HttpActionBuilder,
|
||||||
MutationBuilder,
|
MutationBuilder,
|
||||||
QueryBuilder,
|
QueryBuilder,
|
||||||
GenericActionCtx,
|
GenericActionCtx,
|
||||||
GenericMutationCtx,
|
GenericMutationCtx,
|
||||||
GenericQueryCtx,
|
GenericQueryCtx,
|
||||||
GenericDatabaseReader,
|
GenericDatabaseReader,
|
||||||
GenericDatabaseWriter,
|
GenericDatabaseWriter,
|
||||||
} from "convex/server";
|
} from "convex/server"
|
||||||
import type { DataModel } from "./dataModel.js";
|
import type { DataModel } from "./dataModel.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a query in this Convex app's public API.
|
* 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.
|
* @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.
|
* @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).
|
* 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.
|
* @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.
|
* @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.
|
* 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.
|
* @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.
|
* @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).
|
* 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.
|
* @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.
|
* @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.
|
* 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.
|
* @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.
|
* @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).
|
* 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.
|
* @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.
|
* @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.
|
* 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.
|
* @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.
|
* @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.
|
* 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
|
* This differs from the {@link MutationCtx} because all of the services are
|
||||||
* read-only.
|
* read-only.
|
||||||
*/
|
*/
|
||||||
export type QueryCtx = GenericQueryCtx<DataModel>;
|
export type QueryCtx = GenericQueryCtx<DataModel>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A set of services for use within Convex mutation functions.
|
* 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
|
* The mutation context is passed as the first argument to any Convex mutation
|
||||||
* function run on the server.
|
* 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.
|
* 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
|
* The action context is passed as the first argument to any Convex action
|
||||||
* function run on the server.
|
* 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.
|
* 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
|
* document by its {@link Id}, or {@link DatabaseReader.query}, which starts
|
||||||
* building a query.
|
* 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
|
* 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)
|
* 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.
|
* for the guarantees Convex provides your functions.
|
||||||
*/
|
*/
|
||||||
export type DatabaseWriter = GenericDatabaseWriter<DataModel>;
|
export type DatabaseWriter = GenericDatabaseWriter<DataModel>
|
||||||
|
|||||||
@@ -9,14 +9,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
actionGeneric,
|
actionGeneric,
|
||||||
httpActionGeneric,
|
httpActionGeneric,
|
||||||
queryGeneric,
|
internalActionGeneric,
|
||||||
mutationGeneric,
|
internalMutationGeneric,
|
||||||
internalActionGeneric,
|
internalQueryGeneric,
|
||||||
internalMutationGeneric,
|
mutationGeneric,
|
||||||
internalQueryGeneric,
|
queryGeneric,
|
||||||
} from "convex/server";
|
} from "convex/server"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a query in this Convex app's public API.
|
* 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.
|
* @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.
|
* @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).
|
* 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.
|
* @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.
|
* @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.
|
* 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.
|
* @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.
|
* @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).
|
* 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.
|
* @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.
|
* @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.
|
* 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.
|
* @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.
|
* @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).
|
* 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.
|
* @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.
|
* @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.
|
* Define a Convex HTTP action.
|
||||||
@@ -86,4 +86,4 @@ export const internalAction = internalActionGeneric;
|
|||||||
* as its second.
|
* as its second.
|
||||||
* @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`.
|
* @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 type { Id } from "@fileone/convex/dataModel"
|
||||||
import { v } from "convex/values"
|
import { ConvexError, v } from "convex/values"
|
||||||
import {
|
import {
|
||||||
authenticatedMutation,
|
authenticatedMutation,
|
||||||
authenticatedQuery,
|
authenticatedQuery,
|
||||||
@@ -8,19 +8,19 @@ import {
|
|||||||
import * as Directories from "./model/directories"
|
import * as Directories from "./model/directories"
|
||||||
import * as Files from "./model/files"
|
import * as Files from "./model/files"
|
||||||
import * as User from "./model/user"
|
import * as User from "./model/user"
|
||||||
import * as Err from "./shared/error"
|
import { ErrorCode, error } from "./shared/error"
|
||||||
|
|
||||||
export const generateUploadUrl = authenticatedMutation({
|
export const generateUploadUrl = authenticatedMutation({
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
const usageStatistics = await User.queryCachedUsageStatistics(ctx)
|
const userInfo = await User.queryInfo(ctx)
|
||||||
if (!usageStatistics) {
|
if (!userInfo) {
|
||||||
throw Err.create(Err.Code.Internal, "Internal server error")
|
throw new ConvexError({ message: "Internal server error" })
|
||||||
}
|
}
|
||||||
if (
|
if (userInfo.storageUsageBytes >= userInfo.storageQuotaBytes) {
|
||||||
usageStatistics.storageUsageBytes >=
|
throw new ConvexError({
|
||||||
usageStatistics.storageQuotaBytes
|
code: ErrorCode.StorageQuotaExceeded,
|
||||||
) {
|
message: "Storage quota exceeded",
|
||||||
throw Err.create(Err.Code.Forbidden, "Storage quota exceeded")
|
})
|
||||||
}
|
}
|
||||||
return await ctx.storage.generateUploadUrl()
|
return await ctx.storage.generateUploadUrl()
|
||||||
},
|
},
|
||||||
@@ -53,7 +53,10 @@ export const fetchDirectory = authenticatedQuery({
|
|||||||
handler: async (ctx, { directoryId }) => {
|
handler: async (ctx, { directoryId }) => {
|
||||||
const directory = await authorizedGet(ctx, directoryId)
|
const directory = await authorizedGet(ctx, directoryId)
|
||||||
if (!directory) {
|
if (!directory) {
|
||||||
throw new Error("Directory not found")
|
error({
|
||||||
|
code: ErrorCode.NotFound,
|
||||||
|
message: "Directory not found",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return await Directories.fetch(ctx, { directoryId })
|
return await Directories.fetch(ctx, { directoryId })
|
||||||
},
|
},
|
||||||
@@ -67,7 +70,10 @@ export const createDirectory = authenticatedMutation({
|
|||||||
handler: async (ctx, { name, directoryId }): Promise<Id<"directories">> => {
|
handler: async (ctx, { name, directoryId }): Promise<Id<"directories">> => {
|
||||||
const parentDirectory = await authorizedGet(ctx, directoryId)
|
const parentDirectory = await authorizedGet(ctx, directoryId)
|
||||||
if (!parentDirectory) {
|
if (!parentDirectory) {
|
||||||
throw new Error("Parent directory not found")
|
error({
|
||||||
|
code: ErrorCode.NotFound,
|
||||||
|
message: "Parent directory not found",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return await Directories.create(ctx, {
|
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 {
|
import {
|
||||||
apiKeyAuthenticatedQuery,
|
apiKeyAuthenticatedQuery,
|
||||||
authenticatedMutation,
|
authenticatedMutation,
|
||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
VDirectoryHandle,
|
VDirectoryHandle,
|
||||||
VFileSystemHandle,
|
VFileSystemHandle,
|
||||||
} from "./model/filesystem"
|
} from "./model/filesystem"
|
||||||
import * as Err from "./shared/error"
|
import { createErrorData, ErrorCode, error } from "./shared/error"
|
||||||
import type {
|
import type {
|
||||||
DirectoryHandle,
|
DirectoryHandle,
|
||||||
FileHandle,
|
FileHandle,
|
||||||
@@ -36,10 +36,10 @@ export const moveItems = authenticatedMutation({
|
|||||||
targetDirectoryHandle.id,
|
targetDirectoryHandle.id,
|
||||||
)
|
)
|
||||||
if (!targetDirectory) {
|
if (!targetDirectory) {
|
||||||
throw Err.create(
|
error({
|
||||||
Err.Code.DirectoryNotFound,
|
code: ErrorCode.NotFound,
|
||||||
`Directory ${targetDirectoryHandle.id} not found`,
|
message: `Directory ${targetDirectoryHandle.id} not found`,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const directoryHandles: DirectoryHandle[] = []
|
const directoryHandles: DirectoryHandle[] = []
|
||||||
@@ -81,10 +81,10 @@ export const moveToTrash = authenticatedMutation({
|
|||||||
for (const handle of handles) {
|
for (const handle of handles) {
|
||||||
const item = await authorizedGet(ctx, handle.id)
|
const item = await authorizedGet(ctx, handle.id)
|
||||||
if (!item) {
|
if (!item) {
|
||||||
throw Err.create(
|
error({
|
||||||
Err.Code.NotFound,
|
code: ErrorCode.NotFound,
|
||||||
`Item ${handle.id} not found`,
|
message: `Item ${handle.id} not found`,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ export const moveToTrash = authenticatedMutation({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const results = await Promise.allSettled(promises)
|
const results = await Promise.allSettled(promises)
|
||||||
const errors: Err.ApplicationErrorData[] = []
|
const errors = []
|
||||||
const okHandles: FileSystemHandle[] = []
|
const okHandles: FileSystemHandle[] = []
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
switch (result.status) {
|
switch (result.status) {
|
||||||
@@ -113,7 +113,7 @@ export const moveToTrash = authenticatedMutation({
|
|||||||
okHandles.push(result.value)
|
okHandles.push(result.value)
|
||||||
break
|
break
|
||||||
case "rejected":
|
case "rejected":
|
||||||
errors.push(Err.createJson(Err.Code.Internal))
|
errors.push(createErrorData(ErrorCode.Internal))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from "convex-helpers/server/customFunctions"
|
} from "convex-helpers/server/customFunctions"
|
||||||
import * as ApiKey from "./model/apikey"
|
import * as ApiKey from "./model/apikey"
|
||||||
import { type AuthUser, userIdentityOrThrow, userOrThrow } from "./model/user"
|
import { type AuthUser, userIdentityOrThrow, userOrThrow } from "./model/user"
|
||||||
import * as Err from "./shared/error"
|
import { ErrorCode, error } from "./shared/error"
|
||||||
|
|
||||||
export type AuthenticatedQueryCtx = QueryCtx & {
|
export type AuthenticatedQueryCtx = QueryCtx & {
|
||||||
user: AuthUser
|
user: AuthUser
|
||||||
@@ -65,7 +65,10 @@ export const apiKeyAuthenticatedQuery = customQuery(query, {
|
|||||||
},
|
},
|
||||||
input: async (ctx, args) => {
|
input: async (ctx, args) => {
|
||||||
if (!(await ApiKey.verifyApiKey(ctx, args.apiKey))) {
|
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 }
|
return { ctx: ctx as ApiKeyAuthenticatedQueryCtx, args }
|
||||||
},
|
},
|
||||||
@@ -80,7 +83,10 @@ export const apiKeyAuthenticatedMutation = customMutation(mutation, {
|
|||||||
},
|
},
|
||||||
input: async (ctx, args) => {
|
input: async (ctx, args) => {
|
||||||
if (!(await ApiKey.verifyApiKey(ctx, args.apiKey))) {
|
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 }
|
return { ctx, args }
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import type {
|
|||||||
AuthenticatedQueryCtx,
|
AuthenticatedQueryCtx,
|
||||||
} from "../functions"
|
} from "../functions"
|
||||||
import { authorizedGet } 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 {
|
import {
|
||||||
type DirectoryHandle,
|
type DirectoryHandle,
|
||||||
type DirectoryPath,
|
type DirectoryPath,
|
||||||
@@ -30,10 +31,10 @@ export async function fetchHandle(
|
|||||||
): Promise<Doc<"directories">> {
|
): Promise<Doc<"directories">> {
|
||||||
const directory = await authorizedGet(ctx, handle.id)
|
const directory = await authorizedGet(ctx, handle.id)
|
||||||
if (!directory) {
|
if (!directory) {
|
||||||
throw Err.create(
|
error({
|
||||||
Err.Code.DirectoryNotFound,
|
code: ErrorCode.NotFound,
|
||||||
`Directory ${handle.id} not found`,
|
message: `Directory ${handle.id} not found`,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
return directory
|
return directory
|
||||||
}
|
}
|
||||||
@@ -44,10 +45,10 @@ export async function fetch(
|
|||||||
): Promise<DirectoryInfo> {
|
): Promise<DirectoryInfo> {
|
||||||
const directory = await authorizedGet(ctx, directoryId)
|
const directory = await authorizedGet(ctx, directoryId)
|
||||||
if (!directory) {
|
if (!directory) {
|
||||||
throw Err.create(
|
error({
|
||||||
Err.Code.DirectoryNotFound,
|
code: ErrorCode.NotFound,
|
||||||
`Directory ${directoryId} not found`,
|
message: `Directory ${directoryId} not found`,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const path: DirectoryPath = [
|
const path: DirectoryPath = [
|
||||||
@@ -66,7 +67,10 @@ export async function fetch(
|
|||||||
})
|
})
|
||||||
parentDirId = parentDir.parentId
|
parentDirId = parentDir.parentId
|
||||||
} else {
|
} 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">> {
|
): Promise<Id<"directories">> {
|
||||||
const parentDir = await authorizedGet(ctx, parentId)
|
const parentDir = await authorizedGet(ctx, parentId)
|
||||||
if (!parentDir) {
|
if (!parentDir) {
|
||||||
throw Err.create(
|
error({
|
||||||
Err.Code.DirectoryNotFound,
|
code: ErrorCode.NotFound,
|
||||||
`Parent directory ${parentId} not found`,
|
message: `Parent directory ${parentId} not found`,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = await ctx.db
|
const existing = await ctx.db
|
||||||
@@ -153,10 +157,10 @@ export async function create(
|
|||||||
.first()
|
.first()
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
throw Err.create(
|
error({
|
||||||
Err.Code.DirectoryExists,
|
code: ErrorCode.DirectoryExists,
|
||||||
`Directory with name ${name} already exists in ${parentId ? `directory ${parentId}` : "root"}`,
|
message: `Directory with name ${name} already exists in ${parentId ? `directory ${parentId}` : "root"}`,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
@@ -183,10 +187,10 @@ export async function move(
|
|||||||
sourceDirectories.map((directory) =>
|
sourceDirectories.map((directory) =>
|
||||||
authorizedGet(ctx, directory.id).then((d) => {
|
authorizedGet(ctx, directory.id).then((d) => {
|
||||||
if (!d) {
|
if (!d) {
|
||||||
throw Err.create(
|
error({
|
||||||
Err.Code.DirectoryNotFound,
|
code: ErrorCode.NotFound,
|
||||||
`Directory ${directory.id} not found`,
|
message: `Directory ${directory.id} not found`,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
return ctx.db
|
return ctx.db
|
||||||
.query("directories")
|
.query("directories")
|
||||||
@@ -202,14 +206,14 @@ export async function move(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const errors: Err.ApplicationErrorData[] = []
|
const errors: ApplicationErrorData[] = []
|
||||||
const okDirectories: DirectoryHandle[] = []
|
const okDirectories: DirectoryHandle[] = []
|
||||||
conflictCheckResults.forEach((result, i) => {
|
conflictCheckResults.forEach((result, i) => {
|
||||||
if (result.status === "fulfilled") {
|
if (result.status === "fulfilled") {
|
||||||
if (result.value) {
|
if (result.value) {
|
||||||
errors.push(
|
errors.push(
|
||||||
Err.createJson(
|
createErrorData(
|
||||||
Err.Code.Conflict,
|
ErrorCode.Conflict,
|
||||||
`Directory ${targetDirectory.id} already contains a directory with name ${result.value.name}`,
|
`Directory ${targetDirectory.id} already contains a directory with name ${result.value.name}`,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -217,7 +221,7 @@ export async function move(
|
|||||||
okDirectories.push(sourceDirectories[i]!)
|
okDirectories.push(sourceDirectories[i]!)
|
||||||
}
|
}
|
||||||
} else if (result.status === "rejected") {
|
} 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) {
|
for (const updateResult of results) {
|
||||||
if (updateResult.status === "rejected") {
|
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 deleteResults = await Promise.allSettled(deleteDirectoryPromises)
|
||||||
|
|
||||||
const errors: Err.ApplicationErrorData[] = []
|
const errors: ApplicationErrorData[] = []
|
||||||
let successfulDeletions = 0
|
let successfulDeletions = 0
|
||||||
for (const result of deleteResults) {
|
for (const result of deleteResults) {
|
||||||
if (result.status === "rejected") {
|
if (result.status === "rejected") {
|
||||||
errors.push(Err.createJson(Err.Code.Internal))
|
errors.push(createErrorData(ErrorCode.Internal))
|
||||||
} else {
|
} else {
|
||||||
successfulDeletions += 1
|
successfulDeletions += 1
|
||||||
}
|
}
|
||||||
@@ -378,11 +382,11 @@ export async function restore(
|
|||||||
|
|
||||||
const restoreResults = await Promise.allSettled(restoreDirectoryPromises)
|
const restoreResults = await Promise.allSettled(restoreDirectoryPromises)
|
||||||
|
|
||||||
const errors: Err.ApplicationErrorData[] = []
|
const errors: ApplicationErrorData[] = []
|
||||||
let successfulRestorations = 0
|
let successfulRestorations = 0
|
||||||
for (const result of restoreResults) {
|
for (const result of restoreResults) {
|
||||||
if (result.status === "rejected") {
|
if (result.status === "rejected") {
|
||||||
errors.push(Err.createJson(Err.Code.Internal))
|
errors.push(createErrorData(ErrorCode.Internal))
|
||||||
} else {
|
} else {
|
||||||
successfulRestorations += 1
|
successfulRestorations += 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Doc, Id } from "@fileone/convex/dataModel"
|
import type { Doc, Id } from "@fileone/convex/dataModel"
|
||||||
import { type AuthenticatedMutationCtx, authorizedGet } from "../functions"
|
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"
|
import type { DirectoryHandle, FileHandle } from "../shared/filesystem"
|
||||||
|
|
||||||
export async function renameFile(
|
export async function renameFile(
|
||||||
@@ -27,10 +28,10 @@ export async function renameFile(
|
|||||||
.first()
|
.first()
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
throw Err.create(
|
error({
|
||||||
Err.Code.FileExists,
|
code: ErrorCode.FileExists,
|
||||||
`File with name ${newName} already exists in ${directoryId ? `directory ${directoryId}` : "root"}`,
|
message: `File with name ${newName} already exists in ${directoryId ? `directory ${directoryId}` : "root"}`,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.db.patch(itemId, { name: newName, updatedAt: Date.now() })
|
await ctx.db.patch(itemId, { name: newName, updatedAt: Date.now() })
|
||||||
@@ -50,10 +51,10 @@ export async function move(
|
|||||||
items.map((fileHandle) =>
|
items.map((fileHandle) =>
|
||||||
authorizedGet(ctx, fileHandle.id).then((f) => {
|
authorizedGet(ctx, fileHandle.id).then((f) => {
|
||||||
if (!f) {
|
if (!f) {
|
||||||
throw Err.create(
|
error({
|
||||||
Err.Code.FileNotFound,
|
code: ErrorCode.NotFound,
|
||||||
`File ${fileHandle.id} not found`,
|
message: `File ${fileHandle.id} not found`,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
return ctx.db
|
return ctx.db
|
||||||
.query("files")
|
.query("files")
|
||||||
@@ -69,14 +70,14 @@ export async function move(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const errors: Err.ApplicationErrorData[] = []
|
const errors: ApplicationErrorData[] = []
|
||||||
const okFiles: FileHandle[] = []
|
const okFiles: FileHandle[] = []
|
||||||
conflictCheckResults.forEach((result, i) => {
|
conflictCheckResults.forEach((result, i) => {
|
||||||
if (result.status === "fulfilled") {
|
if (result.status === "fulfilled") {
|
||||||
if (result.value) {
|
if (result.value) {
|
||||||
errors.push(
|
errors.push(
|
||||||
Err.createJson(
|
createErrorData(
|
||||||
Err.Code.Conflict,
|
ErrorCode.Conflict,
|
||||||
`Directory ${targetDirectoryHandle.id} already contains a file with name ${result.value.name}`,
|
`Directory ${targetDirectoryHandle.id} already contains a file with name ${result.value.name}`,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -84,7 +85,7 @@ export async function move(
|
|||||||
okFiles.push(items[i])
|
okFiles.push(items[i])
|
||||||
}
|
}
|
||||||
} else if (result.status === "rejected") {
|
} 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) {
|
for (const updateResult of results) {
|
||||||
if (updateResult.status === "rejected") {
|
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 deleteResults = await Promise.allSettled(deleteFilePromises)
|
||||||
|
|
||||||
const errors: Err.ApplicationErrorData[] = []
|
const errors: ApplicationErrorData[] = []
|
||||||
let successfulDeletions = 0
|
let successfulDeletions = 0
|
||||||
for (const result of deleteResults) {
|
for (const result of deleteResults) {
|
||||||
if (result.status === "rejected") {
|
if (result.status === "rejected") {
|
||||||
errors.push(Err.createJson(Err.Code.Internal))
|
errors.push(createErrorData(ErrorCode.Internal))
|
||||||
} else {
|
} else {
|
||||||
successfulDeletions += 1
|
successfulDeletions += 1
|
||||||
}
|
}
|
||||||
@@ -179,11 +180,11 @@ export async function restore(
|
|||||||
|
|
||||||
const restoreResults = await Promise.allSettled(restoreFilePromises)
|
const restoreResults = await Promise.allSettled(restoreFilePromises)
|
||||||
|
|
||||||
const errors: Err.ApplicationErrorData[] = []
|
const errors: ApplicationErrorData[] = []
|
||||||
let successfulRestorations = 0
|
let successfulRestorations = 0
|
||||||
for (const result of restoreResults) {
|
for (const result of restoreResults) {
|
||||||
if (result.status === "rejected") {
|
if (result.status === "rejected") {
|
||||||
errors.push(Err.createJson(Err.Code.Internal))
|
errors.push(createErrorData(ErrorCode.Internal))
|
||||||
} else {
|
} else {
|
||||||
successfulRestorations += 1
|
successfulRestorations += 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ConvexError } from "convex/values"
|
||||||
import type { Doc, Id } from "../_generated/dataModel"
|
import type { Doc, Id } from "../_generated/dataModel"
|
||||||
import type { MutationCtx } from "../_generated/server"
|
import type { MutationCtx } from "../_generated/server"
|
||||||
import type {
|
import type {
|
||||||
@@ -5,7 +6,7 @@ import type {
|
|||||||
AuthenticatedMutationCtx,
|
AuthenticatedMutationCtx,
|
||||||
AuthenticatedQueryCtx,
|
AuthenticatedQueryCtx,
|
||||||
} from "../functions"
|
} from "../functions"
|
||||||
import * as Err from "../shared/error"
|
import { ErrorCode, error } from "../shared/error"
|
||||||
|
|
||||||
export async function create(
|
export async function create(
|
||||||
ctx: MutationCtx,
|
ctx: MutationCtx,
|
||||||
@@ -22,7 +23,7 @@ export async function create(
|
|||||||
})
|
})
|
||||||
const doc = await ctx.db.get(id)
|
const doc = await ctx.db.get(id)
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
throw Err.create(Err.Code.Internal, "Failed to create file share")
|
throw new ConvexError({ message: "Failed to create file share" })
|
||||||
}
|
}
|
||||||
return doc
|
return doc
|
||||||
}
|
}
|
||||||
@@ -46,11 +47,17 @@ export async function find(
|
|||||||
.withIndex("byShareToken", (q) => q.eq("shareToken", shareToken))
|
.withIndex("byShareToken", (q) => q.eq("shareToken", shareToken))
|
||||||
.first()
|
.first()
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
throw Err.create(Err.Code.NotFound, "File share not found")
|
error({
|
||||||
|
code: ErrorCode.NotFound,
|
||||||
|
message: "File share not found",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasExpired(doc)) {
|
if (hasExpired(doc)) {
|
||||||
throw Err.create(Err.Code.NotFound, "File share not found")
|
error({
|
||||||
|
code: ErrorCode.NotFound,
|
||||||
|
message: "File share not found",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return doc
|
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 { Doc, Id } from "../_generated/dataModel"
|
||||||
import {
|
import {
|
||||||
type AuthenticatedMutationCtx,
|
type AuthenticatedMutationCtx,
|
||||||
type AuthenticatedQueryCtx,
|
type AuthenticatedQueryCtx,
|
||||||
authorizedGet,
|
authorizedGet,
|
||||||
} from "../functions"
|
} from "../functions"
|
||||||
import * as Err from "../shared/error"
|
import { ErrorCode, error } from "../shared/error"
|
||||||
import type {
|
import type {
|
||||||
DirectoryHandle,
|
DirectoryHandle,
|
||||||
FileHandle,
|
FileHandle,
|
||||||
@@ -174,7 +174,10 @@ export async function deleteItemsPermanently(
|
|||||||
export async function emptyTrash(ctx: AuthenticatedMutationCtx) {
|
export async function emptyTrash(ctx: AuthenticatedMutationCtx) {
|
||||||
const rootDir = await queryRootDirectory(ctx)
|
const rootDir = await queryRootDirectory(ctx)
|
||||||
if (!rootDir) {
|
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
|
const dirs = await ctx.db
|
||||||
@@ -221,12 +224,18 @@ export async function fetchFileUrl(
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const file = await authorizedGet(ctx, fileId)
|
const file = await authorizedGet(ctx, fileId)
|
||||||
if (!file) {
|
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)
|
const url = await ctx.storage.getUrl(file.storageId)
|
||||||
if (!url) {
|
if (!url) {
|
||||||
throw Err.create(Err.Code.NotFound, "file not found")
|
error({
|
||||||
|
code: ErrorCode.NotFound,
|
||||||
|
message: "file not found",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return url
|
return url
|
||||||
@@ -238,7 +247,10 @@ export async function openFile(
|
|||||||
) {
|
) {
|
||||||
const file = await authorizedGet(ctx, fileId)
|
const file = await authorizedGet(ctx, fileId)
|
||||||
if (!file) {
|
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, {
|
const fileShare = await FilePreview.find(ctx, {
|
||||||
@@ -281,7 +293,10 @@ export async function saveFile(
|
|||||||
) {
|
) {
|
||||||
const directory = await authorizedGet(ctx, directoryId)
|
const directory = await authorizedGet(ctx, directoryId)
|
||||||
if (!directory) {
|
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([
|
const [fileMetadata, userInfo] = await Promise.all([
|
||||||
@@ -289,7 +304,7 @@ export async function saveFile(
|
|||||||
User.queryInfo(ctx),
|
User.queryInfo(ctx),
|
||||||
])
|
])
|
||||||
if (!fileMetadata || !userInfo) {
|
if (!fileMetadata || !userInfo) {
|
||||||
throw Err.create(Err.Code.Internal, "Internal server error")
|
throw new ConvexError({ message: "Internal server error" })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -297,7 +312,10 @@ export async function saveFile(
|
|||||||
userInfo.storageQuotaBytes
|
userInfo.storageQuotaBytes
|
||||||
) {
|
) {
|
||||||
await ctx.storage.delete(storageId)
|
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()
|
const now = Date.now()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { MutationCtx, QueryCtx } from "@fileone/convex/server"
|
|||||||
import type { Doc } from "../_generated/dataModel"
|
import type { Doc } from "../_generated/dataModel"
|
||||||
import { authComponent } from "../auth"
|
import { authComponent } from "../auth"
|
||||||
import { type AuthenticatedQueryCtx, authorizedGet } from "../functions"
|
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>>
|
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) {
|
export async function userIdentityOrThrow(ctx: QueryCtx | MutationCtx) {
|
||||||
const identity = await ctx.auth.getUserIdentity()
|
const identity = await ctx.auth.getUserIdentity()
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
throw Err.create(Err.Code.Unauthenticated, "Not authenticated")
|
error({
|
||||||
|
code: ErrorCode.Unauthenticated,
|
||||||
|
message: "Not authenticated",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return identity
|
return identity
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,44 @@
|
|||||||
import { ConvexError } from "convex/values"
|
import { ConvexError } from "convex/values"
|
||||||
|
|
||||||
export enum Code {
|
export enum ErrorCode {
|
||||||
Conflict = "Conflict",
|
Conflict = "Conflict",
|
||||||
DirectoryExists = "DirectoryExists",
|
DirectoryExists = "DirectoryExists",
|
||||||
DirectoryNotFound = "DirectoryNotFound",
|
|
||||||
FileExists = "FileExists",
|
FileExists = "FileExists",
|
||||||
FileNotFound = "FileNotFound",
|
|
||||||
Internal = "Internal",
|
Internal = "Internal",
|
||||||
Unauthenticated = "Unauthenticated",
|
Unauthenticated = "Unauthenticated",
|
||||||
NotFound = "NotFound",
|
NotFound = "NotFound",
|
||||||
StorageQuotaExceeded = "StorageQuotaExceeded",
|
StorageQuotaExceeded = "StorageQuotaExceeded",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ApplicationErrorData = { code: Code; message?: string }
|
export type ApplicationErrorData = { code: ErrorCode; message?: string }
|
||||||
export type ApplicationError = ConvexError<ApplicationErrorData>
|
|
||||||
|
|
||||||
export function isApplicationError(error: unknown): error is ApplicationError {
|
export function isApplicationError(
|
||||||
return error instanceof ConvexError && "code" in error.data
|
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 {
|
export function createErrorData(
|
||||||
return new ConvexError({
|
code: ErrorCode,
|
||||||
code,
|
message?: string,
|
||||||
message:
|
): ApplicationErrorData {
|
||||||
code === Code.Internal ? "Internal application error" : message,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createJson(code: Code, message?: string): ApplicationErrorData {
|
|
||||||
return {
|
return {
|
||||||
code,
|
code,
|
||||||
message:
|
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 { Doc, Id } from "@fileone/convex/dataModel"
|
||||||
import type * as Err from "./error"
|
import type { ApplicationErrorData } from "./error"
|
||||||
|
|
||||||
export enum FileType {
|
export enum FileType {
|
||||||
File = "File",
|
File = "File",
|
||||||
@@ -67,7 +67,7 @@ export type DeleteResult = {
|
|||||||
files: number
|
files: number
|
||||||
directories: number
|
directories: number
|
||||||
}
|
}
|
||||||
errors: Err.ApplicationErrorData[]
|
errors: ApplicationErrorData[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function newFileSystemHandle(item: FileSystemItem): FileSystemHandle {
|
export function newFileSystemHandle(item: FileSystemItem): FileSystemHandle {
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
{
|
{
|
||||||
/* This TypeScript project config describes the environment that
|
/* This TypeScript project config describes the environment that
|
||||||
* Convex functions run in and is used to typecheck them.
|
* Convex functions run in and is used to typecheck them.
|
||||||
* You can modify it, but some settings are required to use Convex.
|
* You can modify it, but some settings are required to use Convex.
|
||||||
*/
|
*/
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
/* These settings are not required by Convex and can be modified. */
|
/* These settings are not required by Convex and can be modified. */
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
|
|
||||||
/* These compiler options are required by Convex */
|
/* These compiler options are required by Convex */
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"lib": ["ES2021", "dom"],
|
"lib": ["ES2021", "dom"],
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true
|
"noEmit": true
|
||||||
},
|
},
|
||||||
"include": ["./**/*"],
|
"include": ["./**/*"],
|
||||||
"exclude": ["./_generated"]
|
"exclude": ["./_generated"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user