From 26b00bcf61c720bb136c4079d2dfd3ef315bff6e Mon Sep 17 00:00:00 2001 From: Thet Aung Date: Fri, 13 Feb 2026 18:37:04 +0300 Subject: [PATCH] improvements: dos & fileUpload example --- AGENTS.md | 97 +-- .../demo/examples/FileUploadExample.tsx | 269 ++++++ .../src/components/demo/examples/index.ts | 1 + apps/client/src/pages/TRPCDemoPage.tsx | 2 + apps/server/index.ts | 4 + apps/server/package.json | 2 + apps/server/router.ts | 3 +- apps/server/routers/fileUpload.ts | 97 +++ apps/server/routers/index.ts | 1 + apps/server/routers/submitUserData.ts | 66 ++ docs/SHADCN_COMPONENTS_GUIDE.md | 662 +-------------- docs/SHADCN_DASHBOARD_PATTERNS.md | 566 +++++++++++++ docs/SHADCN_DESIGN_AND_LAYOUT.md | 91 ++ docs/SHADCN_INSTALLATION.md | 29 + docs/TAYLORDB_ADVANCED_PATTERNS.md | 117 +++ docs/TAYLORDB_ATTACHMENTS.md | 68 ++ docs/TAYLORDB_BASIC_QUERIES.md | 187 +++++ docs/TAYLORDB_FIELD_TYPES.md | 84 ++ docs/TAYLORDB_PITFALLS_BEST_PRACTICES.md | 98 +++ docs/TAYLORDB_QUERY_REFERENCE.md | 777 ++---------------- docs/TAYLORDB_WRITE_OPERATIONS.md | 265 ++++++ pnpm-lock.yaml | 170 +++- 22 files changed, 2244 insertions(+), 1412 deletions(-) create mode 100644 apps/client/src/components/demo/examples/FileUploadExample.tsx create mode 100644 apps/server/routers/fileUpload.ts create mode 100644 apps/server/routers/submitUserData.ts create mode 100644 docs/SHADCN_DASHBOARD_PATTERNS.md create mode 100644 docs/SHADCN_DESIGN_AND_LAYOUT.md create mode 100644 docs/SHADCN_INSTALLATION.md create mode 100644 docs/TAYLORDB_ADVANCED_PATTERNS.md create mode 100644 docs/TAYLORDB_ATTACHMENTS.md create mode 100644 docs/TAYLORDB_BASIC_QUERIES.md create mode 100644 docs/TAYLORDB_FIELD_TYPES.md create mode 100644 docs/TAYLORDB_PITFALLS_BEST_PRACTICES.md create mode 100644 docs/TAYLORDB_WRITE_OPERATIONS.md diff --git a/AGENTS.md b/AGENTS.md index adc26cd..9c47779 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,29 @@ Build production-ready, modern web applications (primarily dashboards and CRUD i --- +## 📚 Docs for Agents + +Before you start implementing, skim these docs in the `docs/` folder: + +- **TaylorDB Query Builder** + - `docs/TAYLORDB_QUERY_REFERENCE.md` – index for all query examples + - `docs/TAYLORDB_BASIC_QUERIES.md` – basic reads, filtering, dates + - `docs/TAYLORDB_WRITE_OPERATIONS.md` – inserts, updates, deletes + - `docs/TAYLORDB_ADVANCED_PATTERNS.md` – aggregations, pagination, conditional queries + - `docs/TAYLORDB_FIELD_TYPES.md` – field type mapping & enums + - `docs/TAYLORDB_ATTACHMENTS.md` – working with attachment fields + - `docs/TAYLORDB_PITFALLS_BEST_PRACTICES.md` – common mistakes & best practices + +- **shadcn/ui Components & Dashboard Patterns** + - `docs/SHADCN_COMPONENTS_GUIDE.md` – index for all shadcn/ui docs + - `docs/SHADCN_INSTALLATION.md` – how to install shadcn/ui components + - `docs/SHADCN_DASHBOARD_PATTERNS.md` – ready-made dashboard/layout patterns + - `docs/SHADCN_DESIGN_AND_LAYOUT.md` – design tokens, layout, responsive & performance tips + +Use `AGENTS.md` for **workflow and rules** and the `docs/` files for **detailed code examples**. + +--- + ## 📋 Development Workflow ### **Phase 1: Understand Requirements & Design** @@ -235,21 +258,16 @@ Update the design tokens based on your chosen color scheme: #### Step 2: Create shadcn/ui Components -**Always use shadcn/ui components**. Install components as needed: +**Always use shadcn/ui components**. Install components as needed with: ```bash pnpm dlx shadcn@latest add ``` -**Available by default:** +For concrete install commands and patterns: -- `button`, `card`, `input`, `label`, `textarea`, `select`, `tabs`, `alert` - -**Common additions for dashboards:** - -- `table`, `dialog`, `dropdown-menu`, `toast`, `sheet`, `form`, `badge`, `avatar`, `skeleton` - -Install with: `pnpm dlx shadcn@latest add table dialog dropdown-menu toast sheet form badge avatar skeleton` +- See `docs/SHADCN_INSTALLATION.md` for component install snippets +- See `docs/SHADCN_DASHBOARD_PATTERNS.md` for tables, dialogs, forms, toasts, sheets, command palette, etc. #### Step 3: Build Page Components @@ -576,60 +594,17 @@ apps/client/src/ ## 🔧 TaylorDB Query Builder Reference -### Common Query Patterns +Instead of duplicating examples here, use the dedicated docs: -```typescript -// SELECT with specific fields -queryBuilder - .selectFrom("tableName") - .select(["id", "name", "status"]) - .execute(); +- `docs/TAYLORDB_QUERY_REFERENCE.md` – index for all query builder docs +- `docs/TAYLORDB_BASIC_QUERIES.md` – `selectFrom`, filtering, ordering, date filters +- `docs/TAYLORDB_WRITE_OPERATIONS.md` – `insertInto`, `update`, `deleteFrom` +- `docs/TAYLORDB_ADVANCED_PATTERNS.md` – aggregations, totals, conditional queries, pagination +- `docs/TAYLORDB_FIELD_TYPES.md` – field type mapping, nullable handling, enums +- `docs/TAYLORDB_ATTACHMENTS.md` – selecting and writing attachment fields +- `docs/TAYLORDB_PITFALLS_BEST_PRACTICES.md` – pitfalls and best practices -// WHERE conditions -.where("status", "=", "active") -.where("createdAt", ">=", ["exactDay", "2024-01-01"]) -.where("tags", "hasAnyOf", ["tag1", "tag2"]) - -// ORDER BY -.orderBy("createdAt", "desc") -.orderBy("name", "asc") - -// INSERT -queryBuilder - .insertInto("tableName") - .values({ name: "John", status: "active" }) - .executeTakeFirst(); - -// UPDATE -queryBuilder - .update("tableName") - .set({ status: "inactive" }) - .where("id", "=", 123) - .execute(); - -// DELETE -queryBuilder - .deleteFrom("tableName") - .where("id", "=", 123) - .execute(); - -// DELETE multiple -queryBuilder - .deleteFrom("tableName") - .where("id", "hasAnyOf", [1, 2, 3]) - .execute(); -``` - -### Field Type Handling - -| TaylorDB Field Type | TypeScript Type | Query Value Format | -| ------------------- | --------------- | ---------------------------- | -| Text | `string` | `"value"` | -| Number | `number` | `42` | -| Date | `string` | `["exactDay", "2024-01-01"]` | -| Single Select | `string[]` | `["option"]` | -| Multi Select | `string[]` | `["opt1", "opt2"]` | -| Checkbox | `boolean` | `true` / `false` | +When writing queries in `apps/server/taylordb/query-builder.ts`, mirror the patterns from these docs and keep everything **strongly typed** using `taylordb/types.ts`. --- diff --git a/apps/client/src/components/demo/examples/FileUploadExample.tsx b/apps/client/src/components/demo/examples/FileUploadExample.tsx new file mode 100644 index 0000000..4999a7e --- /dev/null +++ b/apps/client/src/components/demo/examples/FileUploadExample.tsx @@ -0,0 +1,269 @@ +import { useState, useRef, useCallback } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Upload, X, FileIcon, CheckCircle2, Send } from "lucide-react"; +import { DemoCard, InlineSpinner } from "@/components/demo"; +import { trpc } from "@/lib/trpc"; + +interface FileMetadata { + originalName: string; + mimeType: string; + size: number; + sizeFormatted: string; +} + +const BASE_URL = + import.meta.env.VITE_TRPC_URL || "http://localhost:3001/api"; + +export function FileUploadExample() { + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [files, setFiles] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const fileInputRef = useRef(null); + + const utils = trpc.useUtils(); + const { data: submissions } = trpc.submitUserData.getAll.useQuery(); + + const submitMutation = trpc.submitUserData.submit.useMutation({ + onSuccess: () => { + utils.submitUserData.getAll.invalidate(); + setName(""); + setEmail(""); + setFiles([]); + }, + }); + + const addFiles = useCallback((newFiles: FileList | File[]) => { + setFiles((prev) => [...prev, ...Array.from(newFiles)]); + }, []); + + const removeFile = (index: number) => { + setFiles((prev) => prev.filter((_, i) => i !== index)); + }; + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + if (e.dataTransfer.files.length) { + addFiles(e.dataTransfer.files); + } + }, + [addFiles] + ); + + const handleSubmit = async () => { + setIsLoading(true); + setError(null); + + try { + // Step 1: Upload files via /api/upload + let uploadedFiles: FileMetadata[] = []; + + if (files.length > 0) { + const formData = new FormData(); + files.forEach((file) => formData.append("files", file)); + + const uploadRes = await fetch(`${BASE_URL}/upload`, { + method: "POST", + body: formData, + credentials: "include", + }); + const uploadJson = await uploadRes.json(); + + if (!uploadRes.ok || !uploadJson.success) { + throw new Error(uploadJson.error || "File upload failed"); + } + uploadedFiles = uploadJson.data.files; + } + + // Step 2: Submit user data with file references via tRPC + await submitMutation.mutateAsync({ + name, + email, + files: uploadedFiles.map((f) => ({ + originalName: f.originalName, + mimeType: f.mimeType, + size: f.size, + })), + }); + } catch (err) { + setError(err instanceof Error ? err.message : "Something went wrong"); + } finally { + setIsLoading(false); + } + }; + + const formatSize = (bytes: number) => { + if (bytes === 0) return "0 B"; + const units = ["B", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`; + }; + + const canSubmit = name.trim() && email.trim() && !isLoading; + + return ( + + {/* Form Fields */} +
+ setName(e.target.value)} + placeholder="Name" + className="flex-1" + /> + setEmail(e.target.value)} + placeholder="Email" + type="email" + className="flex-1" + /> +
+ + {/* Drop Zone */} +
{ + e.preventDefault(); + setIsDragging(true); + }} + onDragLeave={() => setIsDragging(false)} + onDrop={handleDrop} + onClick={() => fileInputRef.current?.click()} + > + { + if (e.target.files) addFiles(e.target.files); + e.target.value = ""; + }} + /> + +

+ {isDragging ? ( + Drop files here + ) : ( + <> + Click to browse{" "} + or drag & drop files + + )} +

+

+ Any file type • Max 10 MB per file • Up to 10 files +

+
+ + {/* Selected Files */} + {files.length > 0 && ( +
+

+ {files.length} file{files.length > 1 ? "s" : ""} selected +

+ {files.map((file, i) => ( +
+ +
+

{file.name}

+

+ {file.type || "unknown"} • {formatSize(file.size)} +

+
+ +
+ ))} +
+ )} + + {/* Submit Button */} + + + {/* Error */} + {error && ( +
+ {error} +
+ )} + + {/* Submissions List */} + {submissions && submissions.length > 0 && ( +
+

+ Recent submissions +

+ {submissions.map((sub) => ( +
+
+ + {sub.name} + {sub.email} +
+ {sub.files.length > 0 && ( +
+ {sub.files.map((f, i) => ( + + + {f.originalName} + + ))} +
+ )} +
+ ))} +
+ )} +
+ ); +} diff --git a/apps/client/src/components/demo/examples/index.ts b/apps/client/src/components/demo/examples/index.ts index 7f0b425..1407755 100644 --- a/apps/client/src/components/demo/examples/index.ts +++ b/apps/client/src/components/demo/examples/index.ts @@ -1,3 +1,4 @@ export { HelloExample } from "./HelloExample"; export { UsersExample } from "./UsersExample"; export { PostsExample } from "./PostsExample"; +export { FileUploadExample } from "./FileUploadExample"; diff --git a/apps/client/src/pages/TRPCDemoPage.tsx b/apps/client/src/pages/TRPCDemoPage.tsx index f864a03..c2030f4 100644 --- a/apps/client/src/pages/TRPCDemoPage.tsx +++ b/apps/client/src/pages/TRPCDemoPage.tsx @@ -3,6 +3,7 @@ import { HelloExample, UsersExample, PostsExample, + FileUploadExample, } from "@/components/demo/examples"; export default function TRPCDemoPage() { @@ -29,6 +30,7 @@ export default function TRPCDemoPage() { + {/* Footer */} diff --git a/apps/server/index.ts b/apps/server/index.ts index 4b802c7..0885958 100644 --- a/apps/server/index.ts +++ b/apps/server/index.ts @@ -4,6 +4,7 @@ import cookieParser from "cookie-parser"; import * as trpcExpress from "@trpc/server/adapters/express"; import { appRouter } from "./router.js"; import { createContext } from "./trpc.js"; +import { fileUploadRouter } from "./routers/fileUpload.js"; const app = express(); const PORT = process.env.PORT || 3001; @@ -27,6 +28,9 @@ app.get("/api/health", (_req, res) => { res.json({ status: "ok", timestamp: new Date().toISOString() }); }); +// File upload endpoint (multipart/form-data — not supported by tRPC Express adapter) +app.use("/api/upload", fileUploadRouter); + // tRPC middleware app.use( "/api/trpc", diff --git a/apps/server/package.json b/apps/server/package.json index 1fcd0e1..6e734a3 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -19,12 +19,14 @@ "cookie-parser": "^1.4.7", "cors": "^2.8.5", "express": "^5.2.1", + "multer": "^2.0.2", "zod": "^4.3.5" }, "devDependencies": { "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", + "@types/multer": "^2.0.0", "@types/node": "^24.10.1", "concurrently": "^9.2.1", "esbuild": "^0.27.2", diff --git a/apps/server/router.ts b/apps/server/router.ts index beba861..e288724 100644 --- a/apps/server/router.ts +++ b/apps/server/router.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { router, publicProcedure } from "./trpc"; -import { usersRouter, postsRouter } from "./routers"; +import { usersRouter, postsRouter, submitUserDataRouter } from "./routers"; /** * Main tRPC Router @@ -20,6 +20,7 @@ export const appRouter = router({ // ============================================================================ users: usersRouter, posts: postsRouter, + submitUserData: submitUserDataRouter, // ============================================================================ // Global / Utility Procedures diff --git a/apps/server/routers/fileUpload.ts b/apps/server/routers/fileUpload.ts new file mode 100644 index 0000000..37a679c --- /dev/null +++ b/apps/server/routers/fileUpload.ts @@ -0,0 +1,97 @@ +import { Router } from "express"; +import multer from "multer"; + +/** + * File Upload Router + * + * Express router for handling multipart FormData file uploads. + * tRPC v11's FormData support is designed for fetch-based adapters (Next.js, etc.), + * not the Express adapter. This dedicated Express route is the recommended pattern + * for handling file uploads in Express + tRPC stacks. + * + * Uses TaylorDB's uploadAttachments pattern to store files. + * See docs/TAYLORDB_ATTACHMENTS.md for details. + */ + +// Configure multer with memory storage (files available as Buffer) +const upload = multer({ + storage: multer.memoryStorage(), + limits: { + fileSize: 10 * 1024 * 1024, // 10 MB per file + files: 10, // max 10 files + }, +}); + +const fileUploadRouter = Router(); + +/** + * POST /api/upload + * + * Accepts multipart/form-data with: + * - files (File[], multiple files of any type) + * + * Returns metadata about each uploaded file. + */ +fileUploadRouter.post("/", upload.array("files", 10), (req, res) => { + const files = (req.files as Express.Multer.File[]) || []; + + if (files.length === 0) { + res.status(400).json({ + success: false, + error: "At least one file is required.", + }); + return; + } + + const fileMetadata = files.map((file) => ({ + originalName: file.originalname, + mimeType: file.mimetype, + size: file.size, + sizeFormatted: formatFileSize(file.size), + })); + + // ──────────────────────────────────────────────────────────────────────── + // TaylorDB Attachment Example (see docs/TAYLORDB_ATTACHMENTS.md) + // + // To store uploaded files in a TaylorDB record, convert multer buffers + // to Blobs and use qb.uploadAttachments(): + // + // const attachments = await qb.uploadAttachments( + // files.map((file) => ({ + // file: new Blob([file.buffer], { type: file.mimetype }), + // name: file.originalname, + // })) + // ); + // + // // Then use the attachments when inserting/updating a record: + // await qb.insertInto("tableName").values({ + // name: "Some record", + // documents: attachments, // attachment column + // }).execute(); + // + // // Or when updating an existing record: + // await qb.update("tableName").set({ + // documents: attachments, + // }).where("id", "=", recordId).execute(); + // ──────────────────────────────────────────────────────────────────────── + + res.json({ + success: true, + data: { + filesCount: files.length, + files: fileMetadata, + totalSize: formatFileSize(files.reduce((sum, f) => sum + f.size, 0)), + uploadedAt: new Date().toISOString(), + }, + }); +}); + +/** Format bytes to human-readable string */ +function formatFileSize(bytes: number): string { + if (bytes === 0) return "0 B"; + const units = ["B", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`; +} + +export { fileUploadRouter }; diff --git a/apps/server/routers/index.ts b/apps/server/routers/index.ts index 174edbe..45e797d 100644 --- a/apps/server/routers/index.ts +++ b/apps/server/routers/index.ts @@ -7,3 +7,4 @@ export { usersRouter } from "./users"; export { postsRouter } from "./posts"; +export { submitUserDataRouter } from "./submitUserData"; diff --git a/apps/server/routers/submitUserData.ts b/apps/server/routers/submitUserData.ts new file mode 100644 index 0000000..71ec3f6 --- /dev/null +++ b/apps/server/routers/submitUserData.ts @@ -0,0 +1,66 @@ +import { z } from "zod"; +import { router, publicProcedure } from "../trpc"; + +/** + * Submit User Data Router + * + * tRPC router for submitting user data (name, email) with file attachment references. + * Files should first be uploaded via /api/upload, then referenced here. + * + * In a real app, you'd use qb.uploadAttachments() to store files in TaylorDB + * and insert the record with the attachment column. This demo uses in-memory storage. + * + * TaylorDB attachment pattern (see docs/TAYLORDB_ATTACHMENTS.md): + * await qb.insertInto("users").values({ + * name: "Jane", + * avatar: await qb.uploadAttachments([ + * { file: new Blob([buffer]), name: "photo.png" }, + * ]), + * }).execute(); + */ + +// In-memory store for demonstration +interface Submission { + id: number; + name: string; + email: string; + files: { originalName: string; mimeType: string; size: number }[]; + submittedAt: string; +} + +const submissions: Submission[] = []; +let nextId = 1; + +export const submitUserDataRouter = router({ + /** Submit user data with file metadata */ + submit: publicProcedure + .input( + z.object({ + name: z.string().min(1, "Name is required"), + email: z.string().email("Valid email is required"), + files: z.array( + z.object({ + originalName: z.string(), + mimeType: z.string(), + size: z.number(), + }) + ), + }) + ) + .mutation(({ input }) => { + const submission: Submission = { + id: nextId++, + name: input.name, + email: input.email, + files: input.files, + submittedAt: new Date().toISOString(), + }; + submissions.push(submission); + return submission; + }), + + /** Get all submissions */ + getAll: publicProcedure.query(() => { + return submissions; + }), +}); diff --git a/docs/SHADCN_COMPONENTS_GUIDE.md b/docs/SHADCN_COMPONENTS_GUIDE.md index d1521a3..f59d95c 100644 --- a/docs/SHADCN_COMPONENTS_GUIDE.md +++ b/docs/SHADCN_COMPONENTS_GUIDE.md @@ -1,651 +1,55 @@ # shadcn/ui Dashboard Components Guide -This guide provides examples of using shadcn/ui components to build modern dashboard interfaces for your TaylorDB applications. +This is the **entry point** for shadcn/ui documentation in this template. The guide is split into smaller files so agents (and humans) can jump to the topic they need. --- -## 📦 Installation +## 📚 Topics -### Install Individual Components +- **Installation** + See `SHADCN_INSTALLATION.md` for: + - Installing individual components (core, dashboard, data display, advanced) + - Command examples for `pnpm dlx shadcn@latest add` -```bash -# Core components (already included) -pnpm dlx shadcn@latest add button card input label textarea select tabs alert +- **Dashboard Patterns** + See `SHADCN_DASHBOARD_PATTERNS.md` for: + - Stats cards layout + - Data table with dropdown actions + - Create/edit form in a dialog + - Loading states with Skeleton + - Tabs for different views + - Status badges + - Toast notifications (with tRPC mutations) + - Form with validation (react-hook-form + zod) + - Side sheet for details + - Command palette (search, Cmd/Ctrl+K) -# Dashboard-specific components -pnpm dlx shadcn@latest add table dialog dropdown-menu toast sheet form badge avatar skeleton - -# Data display components -pnpm dlx shadcn@latest add separator progress scroll-area tooltip - -# Advanced components -pnpm dlx shadcn@latest add command popover calendar date-picker -``` +- **Design & Layout** + See `SHADCN_DESIGN_AND_LAYOUT.md` for: + - Color schemes (semantic tokens) + - Spacing conventions + - Icons (lucide-react) + - Responsive grids and hide-on-mobile + - Performance tips (lazy dialogs, virtualization, skeletons, optimistic updates, debounce) --- -## 🎨 Common Dashboard Patterns +## How Agents Should Use These Docs -### 1. Dashboard Layout with Stats Cards +1. **Need to add a component?** + Use `SHADCN_INSTALLATION.md` for the exact `pnpm dlx shadcn@latest add` commands. -```typescript -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +2. **Building a dashboard or CRUD UI?** + Use `SHADCN_DASHBOARD_PATTERNS.md` for copy-paste patterns (tables, dialogs, forms, toasts, sheets, etc.). -export default function DashboardPage() { - return ( -
-

Dashboard

- -
- - - - Total Users - - - -
1,234
-

- +20% from last month -

-
-
- - - - - Revenue - - - -
$45,231
-

- +12% from last month -

-
-
- - {/* More stats cards... */} -
-
- ); -} -``` - -### 2. Data Table with Actions - -```typescript -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { MoreHorizontal, Pencil, Trash } from "lucide-react"; - -export default function UsersTable({ users }: { users: User[] }) { - return ( - - - Users - Manage your user accounts - - - - - - Name - Email - Status - Actions - - - - {users.map((user) => ( - - {user.name} - {user.email} - - - {user.status} - - - - - - - - - - - Edit - - - - Delete - - - - - - ))} - -
-
-
- ); -} -``` - -### 3. Create/Edit Form in Dialog - -```typescript -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { useState } from "react"; -import { PlusIcon } from "lucide-react"; - -export default function CreateUserDialog() { - const [open, setOpen] = useState(false); - const [name, setName] = useState(""); - const [email, setEmail] = useState(""); - - const createMutation = trpc.users.create.useMutation({ - onSuccess: () => { - setOpen(false); - setName(""); - setEmail(""); - }, - }); - - return ( - - - - - - - Create New User - Add a new user to your database - - -
-
- - setName(e.target.value)} - placeholder="John Doe" - /> -
-
- - setEmail(e.target.value)} - placeholder="john@example.com" - /> -
-
- - - - - -
-
- ); -} -``` - -### 4. Loading States with Skeleton - -```typescript -import { Skeleton } from "@/components/ui/skeleton"; -import { Card, CardContent, CardHeader } from "@/components/ui/card"; - -export default function DashboardSkeleton() { - return ( -
- - -
- {[...Array(4)].map((_, i) => ( - - - - - - - - - - ))} -
-
- ); -} - -// Usage in main component -export default function DashboardPage() { - const { data: stats, isLoading } = trpc.dashboard.getStats.useQuery(); - - if (isLoading) { - return ; - } - - return
{/* Actual dashboard */}
; -} -``` - -### 5. Tabs for Different Views - -```typescript -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; - -export default function DataExplorer() { - return ( - - - Data Explorer - - - - - Overview - Analytics - Reports - - - - {/* Overview content */} - - - - {/* Analytics content */} - - - - {/* Reports content */} - - - - - ); -} -``` - -### 6. Status Badges - -```typescript -import { Badge } from "@/components/ui/badge"; - -function StatusBadge({ status }: { status: string }) { - const variants = { - active: "default", - pending: "secondary", - inactive: "outline", - error: "destructive", - } as const; - - return ( - - {status} - - ); -} - -// Usage -; -``` - -### 7. Toast Notifications - -```typescript -import { useToast } from "@/hooks/use-toast"; -import { Button } from "@/components/ui/button"; - -export default function ActionsPage() { - const { toast } = useToast(); - - const deleteMutation = trpc.items.delete.useMutation({ - onSuccess: () => { - toast({ - title: "Success!", - description: "Item deleted successfully", - }); - }, - onError: (error) => { - toast({ - title: "Error", - description: error.message, - variant: "destructive", - }); - }, - }); - - return ( - - ); -} - -// Don't forget to add in your App.tsx or layout -``` - -### 8. Form with Validation - -```typescript -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; - -const formSchema = z.object({ - name: z.string().min(2, "Name must be at least 2 characters"), - email: z.string().email("Invalid email address"), - age: z.number().min(0).max(120), -}); - -export default function UserForm() { - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - name: "", - email: "", - age: 0, - }, - }); - - const createMutation = trpc.users.create.useMutation({ - onSuccess: () => { - form.reset(); - }, - }); - - const onSubmit = (data: z.infer) => { - createMutation.mutate(data); - }; - - return ( -
- - ( - - Name - - - - - Your full name as it appears on documents. - - - - )} - /> - - ( - - Email - - - - - - )} - /> - - - - - ); -} -``` - -### 9. Side Sheet for Details - -```typescript -import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, - SheetTrigger, -} from "@/components/ui/sheet"; -import { Button } from "@/components/ui/button"; - -export default function UserDetailsSheet({ userId }: { userId: number }) { - const { data: user } = trpc.users.getById.useQuery({ id: userId }); - - return ( - - - - - - - User Details - Information about this user - - - {user && ( -
-
-

- Name -

-

{user.name}

-
-
-

- Email -

-

{user.email}

-
- {/* More fields... */} -
- )} -
-
- ); -} -``` - -### 10. Command Palette (Search) - -```typescript -import { - Command, - CommandDialog, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command"; -import { useState, useEffect } from "react"; - -export default function GlobalSearch() { - const [open, setOpen] = useState(false); - - useEffect(() => { - const down = (e: KeyboardEvent) => { - if (e.key === "k" && (e.metaKey || e.ctrlKey)) { - e.preventDefault(); - setOpen((open) => !open); - } - }; - - document.addEventListener("keydown", down); - return () => document.removeEventListener("keydown", down); - }, []); - - return ( - - - - No results found. - - John Doe - Jane Smith - - - Project Alpha - Project Beta - - - - ); -} -``` +3. **Styling and layout?** + Use `SHADCN_DESIGN_AND_LAYOUT.md` for tokens, spacing, responsive patterns, and performance. --- -## 🎨 Design Tips - -### Color Schemes - -Use semantic color tokens: - -- `bg-background` / `text-foreground` - Main background and text -- `bg-card` / `text-card-foreground` - Card surfaces -- `bg-primary` / `text-primary-foreground` - Primary actions -- `bg-destructive` - Destructive actions (delete, etc.) -- `bg-muted` / `text-muted-foreground` - Subtle UI elements - -### Spacing - -Use consistent spacing: - -- `space-y-4` / `gap-4` - Between related items -- `space-y-6` / `gap-6` - Between sections -- `p-4` / `p-6` - Card padding -- `p-8` - Page padding - -### Icons - -Use `lucide-react` for consistent icons: - -```typescript -import { Home, Users, Settings, Plus, Edit, Trash, Search } from "lucide-react"; - -; -``` - ---- - -## 📱 Responsive Design - -### Grid Layouts - -```typescript -// 1 column on mobile, 2 on tablet, 4 on desktop -
- {/* cards */} -
- -// 1 column on mobile, 3 on desktop -
- {/* cards */} -
-``` - -### Hide on Mobile - -```typescript -// Hide text on mobile, show on desktop -Dashboard - -// Different layout on mobile -
- {/* content */} -
-``` - ---- - -## 🚀 Performance Tips - -1. **Lazy load dialogs**: Only render dialog content when open -2. **Virtualize long lists**: Use libraries like `react-window` -3. **Skeleton loaders**: Always show loading states -4. **Optimistic updates**: Update UI before server confirms -5. **Debounce search**: Don't query on every keystroke - ---- - -## 📚 Resources +## Resources - **shadcn/ui**: https://ui.shadcn.com/ - **Tailwind CSS**: https://tailwindcss.com/ - **Lucide Icons**: https://lucide.dev/ - **React Hook Form**: https://react-hook-form.com/ - ---- - -**Remember**: Always test your components in both light and dark mode, and on different screen sizes! diff --git a/docs/SHADCN_DASHBOARD_PATTERNS.md b/docs/SHADCN_DASHBOARD_PATTERNS.md new file mode 100644 index 0000000..71cda5c --- /dev/null +++ b/docs/SHADCN_DASHBOARD_PATTERNS.md @@ -0,0 +1,566 @@ +# shadcn/ui — Dashboard Patterns + +Examples of using shadcn/ui components to build dashboard interfaces for TaylorDB applications. + +--- + +## 1. Dashboard Layout with Stats Cards + +```typescript +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +export default function DashboardPage() { + return ( +
+

Dashboard

+ +
+ + + + Total Users + + + +
1,234
+

+ +20% from last month +

+
+
+ + + + + Revenue + + + +
$45,231
+

+ +12% from last month +

+
+
+ + {/* More stats cards... */} +
+
+ ); +} +``` + +--- + +## 2. Data Table with Actions + +```typescript +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { MoreHorizontal, Pencil, Trash } from "lucide-react"; + +export default function UsersTable({ users }: { users: User[] }) { + return ( + + + Users + Manage your user accounts + + + + + + Name + Email + Status + Actions + + + + {users.map((user) => ( + + {user.name} + {user.email} + + + {user.status} + + + + + + + + + + + Edit + + + + Delete + + + + + + ))} + +
+
+
+ ); +} +``` + +--- + +## 3. Create/Edit Form in Dialog + +```typescript +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useState } from "react"; +import { PlusIcon } from "lucide-react"; + +export default function CreateUserDialog() { + const [open, setOpen] = useState(false); + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + + const createMutation = trpc.users.create.useMutation({ + onSuccess: () => { + setOpen(false); + setName(""); + setEmail(""); + }, + }); + + return ( + + + + + + + Create New User + Add a new user to your database + + +
+
+ + setName(e.target.value)} + placeholder="John Doe" + /> +
+
+ + setEmail(e.target.value)} + placeholder="john@example.com" + /> +
+
+ + + + + +
+
+ ); +} +``` + +--- + +## 4. Loading States with Skeleton + +```typescript +import { Skeleton } from "@/components/ui/skeleton"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; + +export default function DashboardSkeleton() { + return ( +
+ + +
+ {[...Array(4)].map((_, i) => ( + + + + + + + + + + ))} +
+
+ ); +} + +// Usage in main component +export default function DashboardPage() { + const { data: stats, isLoading } = trpc.dashboard.getStats.useQuery(); + + if (isLoading) { + return ; + } + + return
{/* Actual dashboard */}
; +} +``` + +--- + +## 5. Tabs for Different Views + +```typescript +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +export default function DataExplorer() { + return ( + + + Data Explorer + + + + + Overview + Analytics + Reports + + + + {/* Overview content */} + + + + {/* Analytics content */} + + + + {/* Reports content */} + + + + + ); +} +``` + +--- + +## 6. Status Badges + +```typescript +import { Badge } from "@/components/ui/badge"; + +function StatusBadge({ status }: { status: string }) { + const variants = { + active: "default", + pending: "secondary", + inactive: "outline", + error: "destructive", + } as const; + + return ( + + {status} + + ); +} + +// Usage +; +``` + +--- + +## 7. Toast Notifications + +```typescript +import { useToast } from "@/hooks/use-toast"; +import { Button } from "@/components/ui/button"; + +export default function ActionsPage() { + const { toast } = useToast(); + + const deleteMutation = trpc.items.delete.useMutation({ + onSuccess: () => { + toast({ + title: "Success!", + description: "Item deleted successfully", + }); + }, + onError: (error) => { + toast({ + title: "Error", + description: error.message, + variant: "destructive", + }); + }, + }); + + return ( + + ); +} + +// Don't forget to add in your App.tsx or layout +``` + +--- + +## 8. Form with Validation + +```typescript +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +const formSchema = z.object({ + name: z.string().min(2, "Name must be at least 2 characters"), + email: z.string().email("Invalid email address"), + age: z.number().min(0).max(120), +}); + +export default function UserForm() { + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + email: "", + age: 0, + }, + }); + + const createMutation = trpc.users.create.useMutation({ + onSuccess: () => { + form.reset(); + }, + }); + + const onSubmit = (data: z.infer) => { + createMutation.mutate(data); + }; + + return ( +
+ + ( + + Name + + + + + Your full name as it appears on documents. + + + + )} + /> + + ( + + Email + + + + + + )} + /> + + + + + ); +} +``` + +--- + +## 9. Side Sheet for Details + +```typescript +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; +import { Button } from "@/components/ui/button"; + +export default function UserDetailsSheet({ userId }: { userId: number }) { + const { data: user } = trpc.users.getById.useQuery({ id: userId }); + + return ( + + + + + + + User Details + Information about this user + + + {user && ( +
+
+

+ Name +

+

{user.name}

+
+
+

+ Email +

+

{user.email}

+
+ {/* More fields... */} +
+ )} +
+
+ ); +} +``` + +--- + +## 10. Command Palette (Search) + +```typescript +import { + Command, + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { useState, useEffect } from "react"; + +export default function GlobalSearch() { + const [open, setOpen] = useState(false); + + useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === "k" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setOpen((open) => !open); + } + }; + + document.addEventListener("keydown", down); + return () => document.removeEventListener("keydown", down); + }, []); + + return ( + + + + No results found. + + John Doe + Jane Smith + + + Project Alpha + Project Beta + + + + ); +} +``` + +--- + +For installation and design/layout, see: + +- **Installation**: `SHADCN_INSTALLATION.md` +- **Design & layout**: `SHADCN_DESIGN_AND_LAYOUT.md` +- **Main guide index**: `SHADCN_COMPONENTS_GUIDE.md` diff --git a/docs/SHADCN_DESIGN_AND_LAYOUT.md b/docs/SHADCN_DESIGN_AND_LAYOUT.md new file mode 100644 index 0000000..634add0 --- /dev/null +++ b/docs/SHADCN_DESIGN_AND_LAYOUT.md @@ -0,0 +1,91 @@ +# shadcn/ui — Design & Layout + +Design tokens, spacing, icons, responsive layouts, and performance tips for shadcn/ui dashboards. + +--- + +## Design Tips + +### Color Schemes + +Use semantic color tokens: + +- `bg-background` / `text-foreground` — Main background and text +- `bg-card` / `text-card-foreground` — Card surfaces +- `bg-primary` / `text-primary-foreground` — Primary actions +- `bg-destructive` — Destructive actions (delete, etc.) +- `bg-muted` / `text-muted-foreground` — Subtle UI elements + +### Spacing + +Use consistent spacing: + +- `space-y-4` / `gap-4` — Between related items +- `space-y-6` / `gap-6` — Between sections +- `p-4` / `p-6` — Card padding +- `p-8` — Page padding + +### Icons + +Use `lucide-react` for consistent icons: + +```typescript +import { Home, Users, Settings, Plus, Edit, Trash, Search } from "lucide-react"; + +; +``` + +--- + +## Responsive Design + +### Grid Layouts + +```typescript +// 1 column on mobile, 2 on tablet, 4 on desktop +
+ {/* cards */} +
+ +// 1 column on mobile, 3 on desktop +
+ {/* cards */} +
+``` + +### Hide on Mobile + +```typescript +// Hide text on mobile, show on desktop +Dashboard + +// Different layout on mobile +
+ {/* content */} +
+``` + +--- + +## Performance Tips + +1. **Lazy load dialogs** — Only render dialog content when open +2. **Virtualize long lists** — Use libraries like `react-window` +3. **Skeleton loaders** — Always show loading states +4. **Optimistic updates** — Update UI before server confirms +5. **Debounce search** — Don't query on every keystroke + +--- + +**Remember**: Always test your components in both light and dark mode, and on different screen sizes. + +--- + +For more, see: + +- **Installation**: `SHADCN_INSTALLATION.md` +- **Dashboard patterns**: `SHADCN_DASHBOARD_PATTERNS.md` +- **Main guide index**: `SHADCN_COMPONENTS_GUIDE.md` diff --git a/docs/SHADCN_INSTALLATION.md b/docs/SHADCN_INSTALLATION.md new file mode 100644 index 0000000..07fc966 --- /dev/null +++ b/docs/SHADCN_INSTALLATION.md @@ -0,0 +1,29 @@ +# shadcn/ui — Installation + +How to add shadcn/ui components to your TaylorDB app. + +--- + +## Install Individual Components + +```bash +# Core components (often already included) +pnpm dlx shadcn@latest add button card input label textarea select tabs alert + +# Dashboard-specific components +pnpm dlx shadcn@latest add table dialog dropdown-menu toast sheet form badge avatar skeleton + +# Data display components +pnpm dlx shadcn@latest add separator progress scroll-area tooltip + +# Advanced components +pnpm dlx shadcn@latest add command popover calendar date-picker +``` + +--- + +For patterns and examples, see: + +- **Dashboard patterns**: `SHADCN_DASHBOARD_PATTERNS.md` +- **Design & layout**: `SHADCN_DESIGN_AND_LAYOUT.md` +- **Main guide index**: `SHADCN_COMPONENTS_GUIDE.md` diff --git a/docs/TAYLORDB_ADVANCED_PATTERNS.md b/docs/TAYLORDB_ADVANCED_PATTERNS.md new file mode 100644 index 0000000..730d92e --- /dev/null +++ b/docs/TAYLORDB_ADVANCED_PATTERNS.md @@ -0,0 +1,117 @@ +# TaylorDB Advanced Patterns + +This document covers **advanced query patterns** using the TaylorDB query builder: + +- Manual aggregations +- Summation helpers +- Conditional queries +- Pagination + +--- + +## Aggregations (Manual) + +Since TaylorDB query builder might not have built-in aggregations, compute manually: + +```typescript +export async function getUserStats() { + const users = await queryBuilder + .selectFrom("users") + .select(["age"]) + .execute(); + + if (users.length === 0) { + return { count: 0, average: null, min: null, max: null }; + } + + const ages = users + .map((u) => u.age) + .filter((a): a is number => a !== undefined); + + return { + count: ages.length, + average: ages.reduce((a, b) => a + b, 0) / ages.length, + min: Math.min(...ages), + max: Math.max(...ages), + }; +} +``` + +--- + +## Sum Totals + +```typescript +export async function getTotalCaloriesForDate(date: string) { + const entries = await queryBuilder + .selectFrom("meals") + .select(["calories", "protein", "carbs", "fats"]) + .where("date", "=", ["exactDay", date]) + .execute(); + + return { + totalCalories: entries.reduce((sum, e) => sum + (e.calories ?? 0), 0), + totalProtein: entries.reduce((sum, e) => sum + (e.protein ?? 0), 0), + totalCarbs: entries.reduce((sum, e) => sum + (e.carbs ?? 0), 0), + totalFats: entries.reduce((sum, e) => sum + (e.fats ?? 0), 0), + }; +} +``` + +--- + +## Conditional Queries + +```typescript +export async function searchTasks(filters: { + projectId?: number; + status?: string; + dueAfter?: string; +}) { + let query = queryBuilder + .selectFrom("tasks") + .select(["id", "title", "status", "dueDate"]); + + if (filters.projectId) { + query = query.where("projectId", "=", filters.projectId); + } + + if (filters.status) { + query = query.where("status", "=", filters.status); + } + + if (filters.dueAfter) { + query = query.where("dueDate", ">=", ["exactDay", filters.dueAfter]); + } + + return await query.execute(); +} +``` + +--- + +## Pagination + +```typescript +export async function getPaginatedUsers(page: number, pageSize: number) { + const offset = (page - 1) * pageSize; + + return await queryBuilder + .selectFrom("users") + .select(["id", "name", "email"]) + .orderBy("createdAt", "desc") + .limit(pageSize) + .offset(offset) + .execute(); +} +``` + +--- + +For more topics, see: + +- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering +- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes +- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums +- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices + diff --git a/docs/TAYLORDB_ATTACHMENTS.md b/docs/TAYLORDB_ATTACHMENTS.md new file mode 100644 index 0000000..e430526 --- /dev/null +++ b/docs/TAYLORDB_ATTACHMENTS.md @@ -0,0 +1,68 @@ +# TaylorDB Attachments + +Attachments are treated as **standard columns** and can be selected and written like other fields, using helper utilities for uploads. + +This document covers: + +- Selecting attachment fields +- Creating records with attachments +- Updating attachments + +--- + +## Select Attachments + +```typescript +// New Standard: Use regular .select() like any other field. +const expenses = await qb + .selectFrom("expenses") + .select(["id", "amount", "receipt"]) + .execute(); +``` + +--- + +## Create with Attachments + +Use `qb.uploadAttachments` to upload files before inserting. + +```typescript +await qb + .insertInto("customers") + .values({ + firstName: "Jane", + lastName: "Doe", + avatar: await qb.uploadAttachments([ + { file: new Blob([""]), name: "test.png" }, + ]), + }) + .execute(); +``` + +--- + +## Update with Attachments + +```typescript +await qb + .update("customers") + .set({ + lastName: "Smith", + avatar: await qb.uploadAttachments([ + { file: new Blob([""]), name: "test.png" }, + ]), + }) + .where("id", "=", 1) + .execute(); +``` + +--- + +For more topics, see: + +- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering +- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes +- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries +- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums +- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices + diff --git a/docs/TAYLORDB_BASIC_QUERIES.md b/docs/TAYLORDB_BASIC_QUERIES.md new file mode 100644 index 0000000..65c4708 --- /dev/null +++ b/docs/TAYLORDB_BASIC_QUERIES.md @@ -0,0 +1,187 @@ +# TaylorDB Basic Queries & Filtering + +This document covers **core read operations** using the TaylorDB query builder: + +- **Basic select queries** +- **Ordering** +- **Filtering with where clauses** +- **Date filters** +- **Array and select field filters** +- **Simple text search** + +--- + +## Basic Queries + +### Get All Records + +```typescript +export async function getAllUsers() { + return await queryBuilder + .selectFrom("users") + .select(["id", "name", "email", "createdAt"]) + .execute(); +} +``` + +### Get All Records (All Fields) + +```typescript +export async function getAllUsers() { + return await queryBuilder.selectFrom("users").execute(); +} +``` + +### Get Single Record by ID + +```typescript +export async function getUserById(id: number) { + return await queryBuilder + .selectFrom("users") + .where("id", "=", id) + .executeTakeFirst(); +} +``` + +**Note**: `.executeTakeFirst()` returns a single record or `undefined`. + +### Get Records with Ordering + +```typescript +// Descending order (newest first) +export async function getRecentUsers() { + return await queryBuilder + .selectFrom("users") + .orderBy("createdAt", "desc") + .execute(); +} + +// Ascending order (oldest first) +export async function getOldestUsers() { + return await queryBuilder + .selectFrom("users") + .orderBy("createdAt", "asc") + .execute(); +} +``` + +--- + +## Filtering & Conditions + +### Basic Where Clauses + +```typescript +// Exact match +.where("status", "=", "active") + +// Not equal +.where("status", "!=", "deleted") + +// Greater than / Less than +.where("age", ">", 18) +.where("age", ">=", 18) +.where("age", "<", 65) +.where("age", "<=", 65) +``` + +### Multiple Conditions (AND logic) + +```typescript +export async function getActiveAdults() { + return await queryBuilder + .selectFrom("users") + .where("status", "=", "active") + .where("age", ">=", 18) + .execute(); +} +``` + +### Date Filtering + +```typescript +// Exact date +export async function getUsersForDate(date: string) { + return await queryBuilder + .selectFrom("users") + .where("createdAt", "=", ["exactDay", date]) + .execute(); +} + +// Date range +export async function getUsersInRange(startDate: string, endDate: string) { + return await queryBuilder + .selectFrom("users") + .where("createdAt", ">=", ["exactDay", startDate]) + .where("createdAt", "<=", ["exactDay", endDate]) + .execute(); +} + +// Before/After a date +.where("dueDate", "<", ["exactDay", "2024-01-01"]) +.where("startDate", ">", ["exactDay", "2024-12-31"]) +``` + +### Array/Multi-Select Filtering + +```typescript +// Check if array contains any of the values +export async function getUsersByTags(tags: string[]) { + return await queryBuilder + .selectFrom("users") + .where("tags", "hasAnyOf", tags) + .execute(); +} + +// Example: Get users tagged with "admin" OR "moderator" +const adminUsers = await getUsersByTags(["admin", "moderator"]); +``` + +### Select Field Filtering + +#### Single Select + +For single-select fields, the query builder now returns a single string value. + +```typescript +export async function getUsersByRole(role: string) { + return await queryBuilder + .selectFrom("users") + .where("role", "=", role) + .execute(); +} +``` + +#### Multi Select + +For multi-select fields, the query builder returns and accepts multiple values. + +```typescript +export async function getUsersByInterests(interests: string[]) { + return await queryBuilder + .selectFrom("users") + .where("interests", "hasAnyOf", interests) + .execute(); +} +``` + +### Text Search (Contains) + +```typescript +export async function searchUsersByName(query: string) { + return await queryBuilder + .selectFrom("users") + .where("name", "contains", query) + .execute(); +} +``` + +--- + +For more topics, see: + +- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes +- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries +- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums +- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices + diff --git a/docs/TAYLORDB_FIELD_TYPES.md b/docs/TAYLORDB_FIELD_TYPES.md new file mode 100644 index 0000000..e7cd7e4 --- /dev/null +++ b/docs/TAYLORDB_FIELD_TYPES.md @@ -0,0 +1,84 @@ +# TaylorDB Field Types & Enums + +This document explains how TaylorDB field types map to TypeScript and how to handle them correctly: + +- Field type reference +- Nullable fields +- Enums and options from generated types + +--- + +## Field Type Reference + +| TaylorDB Field Type | TypeScript Type | Insert Value | Query Value | +| ------------------- | ----------------------- | --------------------- | ---------------------------- | +| **Text** | `string` | `"Hello"` | `"Hello"` | +| **Number** | `number` | `42` | `42` | +| **Date** | `string` (ISO) | `"2024-01-15"` | `["exactDay", "2024-01-15"]` | +| **Checkbox** | `boolean` | `true` | `true` | +| **Single Select** | `string` | `"option"` | `"option"` | +| **Multi Select** | `string[]` | `["opt1", "opt2"]` | `["opt1", "opt2"]` | +| **Attachment** | `string[]` (File Paths) | `uploadAttachments()` | `"file-path"` | +| **Email** | `string` | `"user@example.com"` | `"user@example.com"` | + +--- + +## Handling Nullable Fields + +```typescript +export async function createUserSafe(data: { + name: string; + email?: string | null; + age?: number | null; +}) { + return await queryBuilder + .insertInto("users") + .values({ + name: data.name, + email: data.email ?? "", // Default to empty string + age: data.age ?? 0, // Default to 0 + }) + .executeTakeFirst(); +} +``` + +--- + +## Working with Enums + +```typescript +// Import from generated types +import type { TaskStatusOptions } from "./types"; + +export async function createTask(data: { + title: string; + status: (typeof TaskStatusOptions)[number]; // e.g., "todo" | "in-progress" | "done" +}) { + return await queryBuilder + .insertInto("tasks") + .values({ + title: data.title, + status: data.status, + }) + .executeTakeFirst(); +} + +export async function getTasksByStatus( + status: (typeof TaskStatusOptions)[number], +) { + return await queryBuilder + .selectFrom("tasks") + .where("status", "=", status) + .execute(); +} +``` + +--- + +For more topics, see: + +- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering +- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes +- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries +- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices + diff --git a/docs/TAYLORDB_PITFALLS_BEST_PRACTICES.md b/docs/TAYLORDB_PITFALLS_BEST_PRACTICES.md new file mode 100644 index 0000000..a34144e --- /dev/null +++ b/docs/TAYLORDB_PITFALLS_BEST_PRACTICES.md @@ -0,0 +1,98 @@ +# TaylorDB Pitfalls & Best Practices + +This document captures **common mistakes** and **recommended patterns** when using the TaylorDB query builder. + +--- + +## Common Pitfalls + +### ❌ Pitfall: Not Using exactDay for Dates + +```typescript +// ❌ WRONG +.where("date", "=", "2024-01-15") + +// ✅ CORRECT +.where("date", "=", ["exactDay", "2024-01-15"]) +``` + +### ❌ Pitfall: Ignoring Nullable Fields + +```typescript +// ❌ WRONG (assumes field is always present) +const user = await queryBuilder + .selectFrom("users") + .where("id", "=", 1) + .executeTakeFirst(); +console.log(user.email); // Could be undefined! + +// ✅ CORRECT +const user = await queryBuilder + .selectFrom("users") + .where("id", "=", 1) + .executeTakeFirst(); +if (user && user.email) { + console.log(user.email); +} +``` + +### ❌ Pitfall: Using execute() for Single Record + +```typescript +// ❌ WRONG (returns array) +const user = await queryBuilder + .selectFrom("users") + .where("id", "=", 1) + .execute(); +console.log(user.name); // Error: user is an array! + +// ✅ CORRECT +const user = await queryBuilder + .selectFrom("users") + .where("id", "=", 1) + .executeTakeFirst(); +if (user) { + console.log(user.name); +} +``` + +### ❌ Pitfall: Not Handling Empty Arrays + +```typescript +// ❌ WRONG (fails if users is empty) +const ages = users.map((u) => u.age); +const avg = ages.reduce((a, b) => a + b) / ages.length; // Division by zero! + +// ✅ CORRECT +if (users.length === 0) { + return { average: null }; +} +const ages = users + .map((u) => u.age) + .filter((a): a is number => a !== undefined); +const avg = ages.reduce((a, b) => a + b, 0) / ages.length; +``` + +--- + +## Best Practices + +1. **Always handle `undefined` and `null`** when working with query results. +2. **Use TypeScript types** from `taylordb/types.ts` for type safety. +3. **Use `executeTakeFirst()`** when you expect a single record. +4. **Filter nullish values** before aggregations. +5. **Provide defaults** for optional fields. +6. **Use `["exactDay", date]`** format for date comparisons. +7. **Group related queries** in the same function file. +8. **Export functions**, not raw queries. +9. **Document complex queries** with JSDoc comments. + +--- + +For more topics, see: + +- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering +- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes +- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries +- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums + diff --git a/docs/TAYLORDB_QUERY_REFERENCE.md b/docs/TAYLORDB_QUERY_REFERENCE.md index 36b04f0..41a9c09 100644 --- a/docs/TAYLORDB_QUERY_REFERENCE.md +++ b/docs/TAYLORDB_QUERY_REFERENCE.md @@ -1,739 +1,78 @@ # TaylorDB Query Builder Reference -This document provides comprehensive examples of how to use the TaylorDB query builder for all common database operations. +This is the **entry point** for all TaylorDB query builder docs in this template. +The content has been split into smaller, focused files to make it easier for agents (and humans) to scan and reuse. --- -## 📚 Table of Contents +## 📚 Topics -1. [Setup & Configuration](#setup--configuration) -2. [Basic Queries](#basic-queries) -3. [Filtering & Conditions](#filtering--conditions) -4. [Inserting Data](#inserting-data) -5. [Updating Data](#updating-data) -6. [Deleting Data](#deleting-data) -7. [Advanced Patterns](#advanced-patterns) -8. [Field Type Handling](#field-type-handling) -9. [Attachments](#attachments) -10. [Common Pitfalls](#common-pitfalls) +- **Basic Reads & Filtering** + See `TAYLORDB_BASIC_QUERIES.md` for: + - Basic `selectFrom` usage + - Ordering + - `where` clauses + - Date filters + - Array/select field filters + - Text search (`contains`) + +- **Write Operations (Insert, Update, Delete)** + See `TAYLORDB_WRITE_OPERATIONS.md` for: + - Inserting records (including single-/multi-select fields) + - Updates (single/multiple fields, conditional updates) + - Bulk updates + - Deleting single/multiple records and conditional deletes + +- **Advanced Patterns** + See `TAYLORDB_ADVANCED_PATTERNS.md` for: + - Manual aggregations + - Summation helpers + - Conditional query builders + - Pagination patterns + +- **Field Types & Enums** + See `TAYLORDB_FIELD_TYPES.md` for: + - TaylorDB field type → TypeScript mappings + - Nullable field handling + - Using generated enum options (`...Options`) types + +- **Attachments** + See `TAYLORDB_ATTACHMENTS.md` for: + - Selecting attachment fields + - Creating/updating records with attachments via `uploadAttachments` + +- **Pitfalls & Best Practices** + See `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for: + - Common mistakes (e.g., forgetting `["exactDay", date]`, misusing `execute`) + - Recommended patterns for safe, type-accurate queries --- -## Basic Queries +## How Agents Should Use These Docs -### Get All Records +1. **Start from your use case** + - Need a simple read? Open `TAYLORDB_BASIC_QUERIES.md`. + - Doing writes? Use `TAYLORDB_WRITE_OPERATIONS.md`. + - Need aggregations or pagination? Use `TAYLORDB_ADVANCED_PATTERNS.md`. -```typescript -export async function getAllUsers() { - return await queryBuilder - .selectFrom("users") - .select(["id", "name", "email", "createdAt"]) - .execute(); -} -``` +2. **Combine with generated types** + Always cross-reference: + - `apps/server/taylordb/types.ts` (schema-derived types) + - `apps/server/taylordb/query-builder.ts` (project-specific query functions) -### Get All Records (All Fields) - -```typescript -export async function getAllUsers() { - return await queryBuilder.selectFrom("users").execute(); -} -``` - -### Get Single Record by ID - -```typescript -export async function getUserById(id: number) { - return await queryBuilder - .selectFrom("users") - .where("id", "=", id) - .executeTakeFirst(); -} -``` - -**Note**: `.executeTakeFirst()` returns a single record or `undefined`. - -### Get Records with Ordering - -```typescript -// Descending order (newest first) -export async function getRecentUsers() { - return await queryBuilder - .selectFrom("users") - .orderBy("createdAt", "desc") - .execute(); -} - -// Ascending order (oldest first) -export async function getOldestUsers() { - return await queryBuilder - .selectFrom("users") - .orderBy("createdAt", "asc") - .execute(); -} -``` - ---- - -## Filtering & Conditions - -### Basic Where Clauses - -```typescript -// Exact match -.where("status", "=", "active") - -// Not equal -.where("status", "!=", "deleted") - -// Greater than / Less than -.where("age", ">", 18) -.where("age", ">=", 18) -.where("age", "<", 65) -.where("age", "<=", 65) -``` - -### Multiple Conditions (AND logic) - -```typescript -export async function getActiveAdults() { - return await queryBuilder - .selectFrom("users") - .where("status", "=", "active") - .where("age", ">=", 18) - .execute(); -} -``` - -### Date Filtering - -```typescript -// Exact date -export async function getUsersForDate(date: string) { - return await queryBuilder - .selectFrom("users") - .where("createdAt", "=", ["exactDay", date]) - .execute(); -} - -// Date range -export async function getUsersInRange(startDate: string, endDate: string) { - return await queryBuilder - .selectFrom("users") - .where("createdAt", ">=", ["exactDay", startDate]) - .where("createdAt", "<=", ["exactDay", endDate]) - .execute(); -} - -// Before/After a date -.where("dueDate", "<", ["exactDay", "2024-01-01"]) -.where("startDate", ">", ["exactDay", "2024-12-31"]) -``` - -### Array/Multi-Select Filtering - -```typescript -// Check if array contains any of the values -export async function getUsersByTags(tags: string[]) { - return await queryBuilder - .selectFrom("users") - .where("tags", "hasAnyOf", tags) - .execute(); -} - -// Example: Get users tagged with "admin" OR "moderator" -const adminUsers = await getUsersByTags(["admin", "moderator"]); -``` - -### Select Field Filtering - -#### Single Select - -For single-select fields, the query builder now returns a single string value. - -```typescript -export async function getUsersByRole(role: string) { - return await queryBuilder - .selectFrom("users") - .where("role", "=", role) - .execute(); -} -``` - -#### Multi Select - -For multi-select fields, the query builder returns and accepts multiple values. - -```typescript -export async function getUsersByInterests(interests: string[]) { - return await queryBuilder - .selectFrom("users") - .where("interests", "hasAnyOf", interests) - .execute(); -} -``` - -### Text Search (Contains) - -```typescript -export async function searchUsersByName(query: string) { - return await queryBuilder - .selectFrom("users") - .where("name", "contains", query) - .execute(); -} -``` - ---- - -## Inserting Data - -### Insert Single Record - -```typescript -export async function createUser(data: { - name: string; - email: string; - age: number; -}) { - return await queryBuilder - .insertInto("users") - .values({ - name: data.name, - email: data.email, - age: data.age, - status: "active", // Default value - }) - .executeTakeFirst(); -} -``` - -**Returns**: The created record with its generated `id`. - -### Insert with Single-Select Field - -Single-select fields now accept a single string value directly. - -```typescript -export async function createTask(data: { - title: string; - priority: "low" | "medium" | "high"; -}) { - return await queryBuilder - .insertInto("tasks") - .values({ - title: data.title, - priority: data.priority, - }) - .executeTakeFirst(); -} -``` - -### Insert with Multi-Select Field - -```typescript -export async function createProject(data: { name: string; tags: string[] }) { - return await queryBuilder - .insertInto("projects") - .values({ - name: data.name, - tags: data.tags, // Already an array - }) - .executeTakeFirst(); -} -``` - -### Insert with Computed Fields - -```typescript -export async function createCardioSession(data: { - distance: number; - duration: number; // in minutes -}) { - const speed = data.distance / (data.duration / 60); // km/h - - return await queryBuilder - .insertInto("cardio") - .values({ - distance: data.distance, - duration: data.duration, - speed: speed, // Computed field - }) - .executeTakeFirst(); -} -``` - -### Insert with Optional Fields - -```typescript -export async function createPost(data: { - title: string; - content: string; - tags?: string[]; -}) { - return await queryBuilder - .insertInto("posts") - .values({ - title: data.title, - content: data.content, - tags: data.tags || [], // Default to empty array - }) - .executeTakeFirst(); -} -``` - ---- - -## Updating Data - -### Update Single Field - -```typescript -export async function updateUserName(id: number, name: string) { - return await queryBuilder - .update("users") - .set({ name }) - .where("id", "=", id) - .execute(); -} -``` - -### Update Multiple Fields - -```typescript -export async function updateUser( - id: number, - data: { - name?: string; - email?: string; - age?: number; - }, -) { - return await queryBuilder - .update("users") - .set(data) - .where("id", "=", id) - .execute(); -} -``` - -**Note**: Only provided fields will be updated. - -### Update with Single-Select Field - -```typescript -export async function updateTaskPriority( - id: number, - priority: "low" | "medium" | "high", -) { - return await queryBuilder - .update("tasks") - .set({ priority }) - .where("id", "=", id) - .execute(); -} -``` - -### Update with Conditional Logic - -```typescript -export async function updateCardioSession( - id: number, - data: { - distance?: number; - duration?: number; - }, -) { - // Fetch current record to compute speed - const currentRecord = await queryBuilder - .selectFrom("cardio") - .select(["distance", "duration"]) - .where("id", "=", id) - .executeTakeFirst(); - - if (!currentRecord) { - throw new Error("Record not found"); - } - - const newDistance = data.distance ?? currentRecord.distance ?? 0; - const newDuration = data.duration ?? currentRecord.duration ?? 0; - const speed = newDistance / (newDuration / 60); - - return await queryBuilder - .update("cardio") - .set({ - ...data, - speed, - }) - .where("id", "=", id) - .execute(); -} -``` - -### Update Multiple Records - -```typescript -export async function activateAllUsers() { - return await queryBuilder.update("users").set({ status: "active" }).execute(); // No where clause = update all -} - -// Update with condition -export async function activateInactiveUsers() { - return await queryBuilder - .update("users") - .set({ status: "active" }) - .where("status", "=", "inactive") - .execute(); -} -``` - ---- - -## Deleting Data - -### Delete Single Record - -```typescript -export async function deleteUser(id: number) { - return await queryBuilder.deleteFrom("users").where("id", "=", id).execute(); -} -``` - -### Delete Multiple Records by IDs - -```typescript -export async function deleteUsers(ids: number[]) { - return await queryBuilder - .deleteFrom("users") - .where("id", "hasAnyOf", ids) - .execute(); -} -``` - -### Delete with Condition - -```typescript -export async function deleteInactiveUsers() { - return await queryBuilder - .deleteFrom("users") - .where("status", "=", "inactive") - .execute(); -} -``` - -### Delete Old Records - -```typescript -export async function deleteOldLogs(beforeDate: string) { - return await queryBuilder - .deleteFrom("logs") - .where("createdAt", "<", ["exactDay", beforeDate]) - .execute(); -} -``` - ---- - -## Advanced Patterns - -### Aggregations (Manual) - -Since TaylorDB query builder might not have built-in aggregations, compute manually: - -```typescript -export async function getUserStats() { - const users = await queryBuilder - .selectFrom("users") - .select(["age"]) - .execute(); - - if (users.length === 0) { - return { count: 0, average: null, min: null, max: null }; - } - - const ages = users - .map((u) => u.age) - .filter((a): a is number => a !== undefined); - - return { - count: ages.length, - average: ages.reduce((a, b) => a + b, 0) / ages.length, - min: Math.min(...ages), - max: Math.max(...ages), - }; -} -``` - -### Sum Totals - -```typescript -export async function getTotalCaloriesForDate(date: string) { - const entries = await queryBuilder - .selectFrom("meals") - .select(["calories", "protein", "carbs", "fats"]) - .where("date", "=", ["exactDay", date]) - .execute(); - - return { - totalCalories: entries.reduce((sum, e) => sum + (e.calories ?? 0), 0), - totalProtein: entries.reduce((sum, e) => sum + (e.protein ?? 0), 0), - totalCarbs: entries.reduce((sum, e) => sum + (e.carbs ?? 0), 0), - totalFats: entries.reduce((sum, e) => sum + (e.fats ?? 0), 0), - }; -} -``` - -### Conditional Queries - -```typescript -export async function searchTasks(filters: { - projectId?: number; - status?: string; - dueAfter?: string; -}) { - let query = queryBuilder - .selectFrom("tasks") - .select(["id", "title", "status", "dueDate"]); - - if (filters.projectId) { - query = query.where("projectId", "=", filters.projectId); - } - - if (filters.status) { - query = query.where("status", "=", filters.status); - } - - if (filters.dueAfter) { - query = query.where("dueDate", ">=", ["exactDay", filters.dueAfter]); - } - - return await query.execute(); -} -``` - -### Pagination - -```typescript -export async function getPaginatedUsers(page: number, pageSize: number) { - const offset = (page - 1) * pageSize; - - return await queryBuilder - .selectFrom("users") - .select(["id", "name", "email"]) - .orderBy("createdAt", "desc") - .limit(pageSize) - .offset(offset) - .execute(); -} -``` - ---- - -## Field Type Handling - -### Field Type Reference - -| TaylorDB Field Type | TypeScript Type | Insert Value | Query Value | -| ------------------- | ----------------------- | --------------------- | ---------------------------- | -| **Text** | `string` | `"Hello"` | `"Hello"` | -| **Number** | `number` | `42` | `42` | -| **Date** | `string` (ISO) | `"2024-01-15"` | `["exactDay", "2024-01-15"]` | -| **Checkbox** | `boolean` | `true` | `true` | -| **Single Select** | `string` | `"option"` | `"option"` | -| **Multi Select** | `string[]` | `["opt1", "opt2"]` | `["opt1", "opt2"]` | -| **Attachment** | `string[]` (File Paths) | `uploadAttachments()` | `"file-path"` | -| **Email** | `string` | `"user@example.com"` | `"user@example.com"` | - -### Handling Nullable Fields - -```typescript -export async function createUserSafe(data: { - name: string; - email?: string | null; - age?: number | null; -}) { - return await queryBuilder - .insertInto("users") - .values({ - name: data.name, - email: data.email ?? "", // Default to empty string - age: data.age ?? 0, // Default to 0 - }) - .executeTakeFirst(); -} -``` - -### Working with Enums - -```typescript -// Import from generated types -import type { TaskStatusOptions } from "./types"; - -export async function createTask(data: { - title: string; - status: (typeof TaskStatusOptions)[number]; // e.g., "todo" | "in-progress" | "done" -}) { - return await queryBuilder - .insertInto("tasks") - .values({ - title: data.title, - status: data.status, - }) - .executeTakeFirst(); -} - -export async function getTasksByStatus( - status: (typeof TaskStatusOptions)[number], -) { - return await queryBuilder - .selectFrom("tasks") - .where("status", "=", status) - .execute(); -} -``` - ---- - -## Common Pitfalls - -### ❌ Pitfall 2: Not Using exactDay for Dates - -```typescript -// ❌ WRONG -.where("date", "=", "2024-01-15") - -// ✅ CORRECT -.where("date", "=", ["exactDay", "2024-01-15"]) -``` - -### ❌ Pitfall 3: Ignoring Nullable Fields - -```typescript -// ❌ WRONG (assumes field is always present) -const user = await queryBuilder - .selectFrom("users") - .where("id", "=", 1) - .executeTakeFirst(); -console.log(user.email); // Could be undefined! - -// ✅ CORRECT -const user = await queryBuilder - .selectFrom("users") - .where("id", "=", 1) - .executeTakeFirst(); -if (user && user.email) { - console.log(user.email); -} -``` - -### ❌ Pitfall 4: Using execute() for Single Record - -```typescript -// ❌ WRONG (returns array) -const user = await queryBuilder - .selectFrom("users") - .where("id", "=", 1) - .execute(); -console.log(user.name); // Error: user is an array! - -// ✅ CORRECT -const user = await queryBuilder - .selectFrom("users") - .where("id", "=", 1) - .executeTakeFirst(); -if (user) { - console.log(user.name); -} -``` - -### ❌ Pitfall 5: Not Handling Empty Arrays - -```typescript -// ❌ WRONG (fails if users is empty) -const ages = users.map((u) => u.age); -const avg = ages.reduce((a, b) => a + b) / ages.length; // Division by zero! - -// ✅ CORRECT -if (users.length === 0) { - return { average: null }; -} -const ages = users - .map((u) => u.age) - .filter((a): a is number => a !== undefined); -const avg = ages.reduce((a, b) => a + b, 0) / ages.length; -``` - ---- - -## Best Practices - -1. **Always handle `undefined` and `null`** when working with query results -2. **Use TypeScript types** from `taylordb/types.ts` for type safety -3. **Wrap single-select values** in arrays when inserting/updating -4. **Use `executeTakeFirst()`** when you expect a single record -5. **Filter nullish values** before aggregations -6. **Provide defaults** for optional fields -7. **Use `exactDay`** format for date comparisons -8. **Group related queries** in the same function file -9. **Export functions**, not raw queries -10. **Document complex queries** with JSDoc comments - ---- - -## Attachments - -Attachments are no longer treated as relations. They are now standard columns and can be selected directly. - -### Select Attachments - -```typescript -// New Standard: Use regular .select() like any other field. -const expenses = await qb - .selectFrom("expenses") - .select(["id", "amount", "receipt"]) - .execute(); -``` - -### Create with Attachments - -Use `qb.uploadAttachments` to upload files before inserting. - -```typescript -await qb - .insertInto("customers") - .values({ - firstName: "Jane", - lastName: "Doe", - avatar: await qb.uploadAttachments([ - { file: new Blob([""]), name: "test.png" }, - ]), - }) - .execute(); -``` - -### Update with Attachments - -```typescript -await qb - .update("customers") - .set({ - lastName: "Smith", - avatar: await qb.uploadAttachments([ - { file: new Blob([""]), name: "test.png" }, - ]), - }) - .where("id", "=", 1) - .execute(); -``` +3. **Check pitfalls before finalizing** + Before shipping queries, skim `TAYLORDB_PITFALLS_BEST_PRACTICES.md` to avoid common errors. --- ## Additional Resources -- **Generated Types**: Check `apps/server/taylordb/types.ts` for your schema -- **Example Queries**: See `apps/server/taylordb/query-builder.ts` -- **tRPC Integration**: See `apps/server/router.ts` +- **Generated Types**: `apps/server/taylordb/types.ts` +- **Example Queries in This Template**: `apps/server/taylordb/query-builder.ts` +- **tRPC Integration**: `apps/server/router.ts` --- -**Note**: This reference is based on the TaylorDB query builder patterns used in this template. Always refer to the official TaylorDB documentation for the most up-to-date API details. +**Note**: These docs mirror the TaylorDB query builder patterns used in this template. +For the most up-to-date API details, always refer to the official TaylorDB documentation. + diff --git a/docs/TAYLORDB_WRITE_OPERATIONS.md b/docs/TAYLORDB_WRITE_OPERATIONS.md new file mode 100644 index 0000000..a943e92 --- /dev/null +++ b/docs/TAYLORDB_WRITE_OPERATIONS.md @@ -0,0 +1,265 @@ +# TaylorDB Write Operations (Insert, Update, Delete) + +This document covers **write operations** using the TaylorDB query builder: + +- **Inserting data** +- **Updating single/multiple records** +- **Deleting records** + +--- + +## Inserting Data + +### Insert Single Record + +```typescript +export async function createUser(data: { + name: string; + email: string; + age: number; +}) { + return await queryBuilder + .insertInto("users") + .values({ + name: data.name, + email: data.email, + age: data.age, + status: "active", // Default value + }) + .executeTakeFirst(); +} +``` + +**Returns**: The created record with its generated `id`. + +### Insert with Single-Select Field + +Single-select fields now accept a single string value directly. + +```typescript +export async function createTask(data: { + title: string; + priority: "low" | "medium" | "high"; +}) { + return await queryBuilder + .insertInto("tasks") + .values({ + title: data.title, + priority: data.priority, + }) + .executeTakeFirst(); +} +``` + +### Insert with Multi-Select Field + +```typescript +export async function createProject(data: { name: string; tags: string[] }) { + return await queryBuilder + .insertInto("projects") + .values({ + name: data.name, + tags: data.tags, // Already an array + }) + .executeTakeFirst(); +} +``` + +### Insert with Computed Fields + +```typescript +export async function createCardioSession(data: { + distance: number; + duration: number; // in minutes +}) { + const speed = data.distance / (data.duration / 60); // km/h + + return await queryBuilder + .insertInto("cardio") + .values({ + distance: data.distance, + duration: data.duration, + speed: speed, // Computed field + }) + .executeTakeFirst(); +} +``` + +### Insert with Optional Fields + +```typescript +export async function createPost(data: { + title: string; + content: string; + tags?: string[]; +}) { + return await queryBuilder + .insertInto("posts") + .values({ + title: data.title, + content: data.content, + tags: data.tags || [], // Default to empty array + }) + .executeTakeFirst(); +} +``` + +--- + +## Updating Data + +### Update Single Field + +```typescript +export async function updateUserName(id: number, name: string) { + return await queryBuilder + .update("users") + .set({ name }) + .where("id", "=", id) + .execute(); +} +``` + +### Update Multiple Fields + +```typescript +export async function updateUser( + id: number, + data: { + name?: string; + email?: string; + age?: number; + }, +) { + return await queryBuilder + .update("users") + .set(data) + .where("id", "=", id) + .execute(); +} +``` + +**Note**: Only provided fields will be updated. + +### Update with Single-Select Field + +```typescript +export async function updateTaskPriority( + id: number, + priority: "low" | "medium" | "high", +) { + return await queryBuilder + .update("tasks") + .set({ priority }) + .where("id", "=", id) + .execute(); +} +``` + +### Update with Conditional Logic + +```typescript +export async function updateCardioSession( + id: number, + data: { + distance?: number; + duration?: number; + }, +) { + // Fetch current record to compute speed + const currentRecord = await queryBuilder + .selectFrom("cardio") + .select(["distance", "duration"]) + .where("id", "=", id) + .executeTakeFirst(); + + if (!currentRecord) { + throw new Error("Record not found"); + } + + const newDistance = data.distance ?? currentRecord.distance ?? 0; + const newDuration = data.duration ?? currentRecord.duration ?? 0; + const speed = newDistance / (newDuration / 60); + + return await queryBuilder + .update("cardio") + .set({ + ...data, + speed, + }) + .where("id", "=", id) + .execute(); +} +``` + +### Update Multiple Records + +```typescript +export async function activateAllUsers() { + return await queryBuilder.update("users").set({ status: "active" }).execute(); // No where clause = update all +} + +// Update with condition +export async function activateInactiveUsers() { + return await queryBuilder + .update("users") + .set({ status: "active" }) + .where("status", "=", "inactive") + .execute(); +} +``` + +--- + +## Deleting Data + +### Delete Single Record + +```typescript +export async function deleteUser(id: number) { + return await queryBuilder.deleteFrom("users").where("id", "=", id).execute(); +} +``` + +### Delete Multiple Records by IDs + +```typescript +export async function deleteUsers(ids: number[]) { + return await queryBuilder + .deleteFrom("users") + .where("id", "hasAnyOf", ids) + .execute(); +} +``` + +### Delete with Condition + +```typescript +export async function deleteInactiveUsers() { + return await queryBuilder + .deleteFrom("users") + .where("status", "=", "inactive") + .execute(); +} +``` + +### Delete Old Records + +```typescript +export async function deleteOldLogs(beforeDate: string) { + return await queryBuilder + .deleteFrom("logs") + .where("createdAt", "<", ["exactDay", beforeDate]) + .execute(); +} +``` + +--- + +For more topics, see: + +- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering +- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries +- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums +- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33fbd3b..9409159 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -139,8 +139,8 @@ importers: apps/server: dependencies: '@taylordb/query-builder': - specifier: ^0.10.3 - version: 0.10.3 + specifier: ^0.11.4 + version: 0.11.4 '@trpc/server': specifier: ^11.8.1 version: 11.8.1(typescript@5.9.3) @@ -153,6 +153,9 @@ importers: express: specifier: ^5.2.1 version: 5.2.1 + multer: + specifier: ^2.0.2 + version: 2.0.2 zod: specifier: ^4.3.5 version: 4.3.5 @@ -166,6 +169,9 @@ importers: '@types/express': specifier: ^5.0.6 version: 5.0.6 + '@types/multer': + specifier: ^2.0.0 + version: 2.0.0 '@types/node': specifier: ^24.10.1 version: 24.10.1 @@ -1279,9 +1285,15 @@ packages: '@taylordb/query-builder@0.10.3': resolution: {integrity: sha512-GwsrSdCQelTvghPY4uzoSWHI2vsWFOFSOb4IKRS0qQT8Z6jG8VWMOSRwocvcMoQfOf2RqCDt9Wew1dK6FZNBHA==} + '@taylordb/query-builder@0.11.4': + resolution: {integrity: sha512-5hGFtxTKtRK76BUq08c95lqzYBcCTNRm1W04z1P8ufAVvSrr9Iuq1sPf8D7ayBJrAZ8KHThwvAR9w4n0vqiwYQ==} + '@taylordb/shared@0.4.4': resolution: {integrity: sha512-Xykr4I26JapNLePkapBGjz15t9Ep1iLs30VbfCcc2z30x8Qy/2tm+sSa5LcacCP2EaxNVDp2UuYKhZ7kOWBLBQ==} + '@taylordb/shared@0.5.4': + resolution: {integrity: sha512-85tqzVQXsbx3rUAhpcdeA/BKHaHEFgpMxjmRAb12bD4U4ghGrcFw1eyaq+/YtoBfiWChPCruCK+d03e+rEMoiA==} + '@trpc/client@11.8.1': resolution: {integrity: sha512-L/SJFGanr9xGABmuDoeXR4xAdHJmsXsiF9OuH+apecJ+8sUITzVT1EPeqp0ebqA6lBhEl5pPfg3rngVhi/h60Q==} peerDependencies: @@ -1371,6 +1383,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/multer@2.0.0': + resolution: {integrity: sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==} + '@types/node@24.10.1': resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} @@ -1487,6 +1502,9 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1520,6 +1538,13 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1564,6 +1589,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + concurrently@9.2.1: resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} engines: {node: '>=18'} @@ -2142,6 +2171,10 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -2158,10 +2191,18 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mime-types@3.0.2: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} @@ -2173,9 +2214,20 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + multer@2.0.2: + resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==} + engines: {node: '>= 10.16.0'} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2350,6 +2402,10 @@ packages: resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} engines: {node: '>=0.10.0'} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + recharts@3.5.0: resolution: {integrity: sha512-jWqBtu8L3VICXWa3g/y+bKjL8DDHSRme7DHD/70LQ/Tk0di1h11Y0kKC0nPh6YJ2oaa0k6anIFNhg6SfzHWdEA==} engines: {node: '>=18'} @@ -2399,6 +2455,9 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2469,10 +2528,17 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -2539,10 +2605,17 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + type-is@2.0.1: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + typescript-eslint@8.47.0: resolution: {integrity: sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2596,6 +2669,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -2675,6 +2751,10 @@ packages: resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} engines: {node: '>=0.4.0'} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -3604,10 +3684,28 @@ snapshots: - supports-color - utf-8-validate + '@taylordb/query-builder@0.11.4': + dependencies: + '@taylordb/shared': 0.5.4 + eventemitter3: 5.0.1 + fast-json-patch: 3.1.1 + json-to-graphql-query: 2.3.0 + lodash: 4.17.21 + socket.io-client: 4.8.3 + zod: 4.3.5 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@taylordb/shared@0.4.4': dependencies: lodash: 4.17.21 + '@taylordb/shared@0.5.4': + dependencies: + lodash: 4.17.21 + '@trpc/client@11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3)': dependencies: '@trpc/server': 11.8.1(typescript@5.9.3) @@ -3707,6 +3805,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/multer@2.0.0': + dependencies: + '@types/express': 5.0.6 + '@types/node@24.10.1': dependencies: undici-types: 7.16.0 @@ -3863,6 +3965,8 @@ snapshots: dependencies: color-convert: 2.0.1 + append-field@1.0.0: {} + argparse@2.0.1: {} aria-hidden@1.2.6: @@ -3908,6 +4012,12 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.1.4(browserslist@4.28.0) + buffer-from@1.1.2: {} + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + bytes@3.1.2: {} call-bind-apply-helpers@1.0.2: @@ -3949,6 +4059,13 @@ snapshots: concat-map@0.0.1: {} + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + concurrently@9.2.1: dependencies: chalk: 4.1.2 @@ -4542,6 +4659,8 @@ snapshots: math-intrinsics@1.1.0: {} + media-typer@0.3.0: {} + media-typer@1.1.0: {} merge-descriptors@2.0.0: {} @@ -4553,8 +4672,14 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mime-types@3.0.2: dependencies: mime-db: 1.54.0 @@ -4567,8 +4692,24 @@ snapshots: dependencies: brace-expansion: 2.0.2 + minimist@1.2.8: {} + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + ms@2.1.3: {} + multer@2.0.2: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + mkdirp: 0.5.6 + object-assign: 4.1.1 + type-is: 1.6.18 + xtend: 4.0.2 + nanoid@3.3.11: {} natural-compare@1.4.0: {} @@ -4713,6 +4854,12 @@ snapshots: react@19.2.0: {} + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + recharts@3.5.0(@types/react@19.2.6)(eslint@9.39.1(jiti@2.6.1))(react-dom@19.2.0(react@19.2.0))(react-is@19.2.0)(react@19.2.0)(redux@5.0.1): dependencies: '@reduxjs/toolkit': 2.11.0(react-redux@9.2.0(@types/react@19.2.6)(react@19.2.0)(redux@5.0.1))(react@19.2.0) @@ -4797,6 +4944,8 @@ snapshots: dependencies: tslib: 2.8.1 + safe-buffer@5.2.1: {} + safer-buffer@2.1.2: {} scheduler@0.27.0: {} @@ -4890,12 +5039,18 @@ snapshots: statuses@2.0.2: {} + streamsearch@1.1.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -4950,12 +5105,19 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + type-is@2.0.1: dependencies: content-type: 1.0.5 media-typer: 1.1.0 mime-types: 3.0.2 + typedarray@0.0.6: {} + typescript-eslint@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) @@ -5002,6 +5164,8 @@ snapshots: dependencies: react: 19.2.0 + util-deprecate@1.0.2: {} + vary@1.1.2: {} victory-vendor@37.3.6: @@ -5054,6 +5218,8 @@ snapshots: xmlhttprequest-ssl@2.1.2: {} + xtend@4.0.2: {} + y18n@5.0.8: {} yallist@3.1.1: {}