Compare commits
No commits in common. "26b00bcf61c720bb136c4079d2dfd3ef315bff6e" and "d51a2f372fdb3cd30e27dba3c8795ff7c022eb16" have entirely different histories.
26b00bcf61
...
d51a2f372f
111
AGENTS.md
111
AGENTS.md
|
|
@ -15,29 +15,6 @@ 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**
|
||||
|
|
@ -77,15 +54,19 @@ Document your design decisions briefly before implementing.
|
|||
|
||||
#### Step 1: Set Up Server-Side Data Layer
|
||||
|
||||
Use querybuilder which is in **File: `apps/server/taylordb/query-builder.ts`**
|
||||
**File: `apps/server/taylordb/query-builder.ts`**
|
||||
|
||||
You can access the query builder from
|
||||
This file contains all database operations. Create type-safe CRUD functions for each table:
|
||||
|
||||
```typescript
|
||||
publicProcedure.input({}).query(({ input, ctx }) => {
|
||||
const queryBuilder = ctx.queryBuilder;
|
||||
import { createQueryBuilder } from "@taylordb/query-builder";
|
||||
import type { TaylorDatabase } from "./types.js";
|
||||
|
||||
export const queryBuilder = createQueryBuilder<TaylorDatabase>({
|
||||
baseUrl: process.env.TAYLORDB_BASE_URL!,
|
||||
baseId: process.env.TAYLORDB_SERVER_ID!,
|
||||
apiKey: process.env.TAYLORDB_API_TOKEN!,
|
||||
});
|
||||
```
|
||||
|
||||
// ============================================================================
|
||||
// READ Operations
|
||||
|
|
@ -258,16 +239,21 @@ 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 with:
|
||||
**Always use shadcn/ui components**. Install components as needed:
|
||||
|
||||
```bash
|
||||
pnpm dlx shadcn@latest add <component-name>
|
||||
```
|
||||
|
||||
For concrete install commands and patterns:
|
||||
**Available by default:**
|
||||
|
||||
- See `docs/SHADCN_INSTALLATION.md` for component install snippets
|
||||
- See `docs/SHADCN_DASHBOARD_PATTERNS.md` for tables, dialogs, forms, toasts, sheets, command palette, etc.
|
||||
- `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`
|
||||
|
||||
#### Step 3: Build Page Components
|
||||
|
||||
|
|
@ -594,17 +580,60 @@ apps/client/src/
|
|||
|
||||
## 🔧 TaylorDB Query Builder Reference
|
||||
|
||||
Instead of duplicating examples here, use the dedicated docs:
|
||||
### Common Query Patterns
|
||||
|
||||
- `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
|
||||
```typescript
|
||||
// SELECT with specific fields
|
||||
queryBuilder
|
||||
.selectFrom("tableName")
|
||||
.select(["id", "name", "status"])
|
||||
.execute();
|
||||
|
||||
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`.
|
||||
// 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` |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@ This template is designed to deploy to TaylorDB's platform using the included `t
|
|||
**Environment Variables Required:**
|
||||
|
||||
- `TAYLORDB_BASE_URL`
|
||||
- `TAYLORDB_API_TOKEN`
|
||||
- `TAYLORDB_SERVER_ID`
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -1,269 +0,0 @@
|
|||
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<File[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(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 (
|
||||
<DemoCard
|
||||
title="Submit User Data"
|
||||
description="Upload files via /api/upload, then submit data via tRPC"
|
||||
icon={Send}
|
||||
iconColorClass="bg-amber-500/10 text-amber-500"
|
||||
glowClass="glow-primary"
|
||||
>
|
||||
{/* Form Fields */}
|
||||
<div className="flex gap-3">
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Name"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Input
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Email"
|
||||
type="email"
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Drop Zone */}
|
||||
<div
|
||||
className={`relative rounded-lg border-2 border-dashed p-6 text-center transition-colors cursor-pointer ${
|
||||
isDragging
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border/60 hover:border-primary/40 hover:bg-muted/30"
|
||||
}`}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
}}
|
||||
onDragLeave={() => setIsDragging(false)}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
if (e.target.files) addFiles(e.target.files);
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<Upload className="mx-auto h-8 w-8 text-muted-foreground/60 mb-2" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isDragging ? (
|
||||
<span className="text-primary font-medium">Drop files here</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="font-medium text-foreground">Click to browse</span>{" "}
|
||||
or drag & drop files
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground/60 mt-1">
|
||||
Any file type • Max 10 MB per file • Up to 10 files
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Selected Files */}
|
||||
{files.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
{files.length} file{files.length > 1 ? "s" : ""} selected
|
||||
</p>
|
||||
{files.map((file, i) => (
|
||||
<div
|
||||
key={`${file.name}-${i}`}
|
||||
className="flex items-center gap-3 p-3 rounded-lg bg-muted/30 border border-border/50"
|
||||
>
|
||||
<FileIcon className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">{file.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{file.type || "unknown"} • {formatSize(file.size)}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 text-muted-foreground hover:text-destructive shrink-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeFile(i);
|
||||
}}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!canSubmit}
|
||||
className="w-full"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<InlineSpinner /> Submitting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="h-4 w-4 mr-2" />
|
||||
Submit User Data
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="p-3 rounded-lg bg-destructive/10 border border-destructive/30 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submissions List */}
|
||||
{submissions && submissions.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Recent submissions
|
||||
</p>
|
||||
{submissions.map((sub) => (
|
||||
<div
|
||||
key={sub.id}
|
||||
className="p-3 rounded-lg bg-muted/30 border border-border/50 space-y-1"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-3.5 w-3.5 text-green-500 shrink-0" />
|
||||
<span className="text-sm font-medium">{sub.name}</span>
|
||||
<span className="text-xs text-muted-foreground">{sub.email}</span>
|
||||
</div>
|
||||
{sub.files.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 ml-5.5">
|
||||
{sub.files.map((f, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary"
|
||||
>
|
||||
<FileIcon className="h-3 w-3" />
|
||||
{f.originalName}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</DemoCard>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
export { HelloExample } from "./HelloExample";
|
||||
export { UsersExample } from "./UsersExample";
|
||||
export { PostsExample } from "./PostsExample";
|
||||
export { FileUploadExample } from "./FileUploadExample";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
HelloExample,
|
||||
UsersExample,
|
||||
PostsExample,
|
||||
FileUploadExample,
|
||||
} from "@/components/demo/examples";
|
||||
|
||||
export default function TRPCDemoPage() {
|
||||
|
|
@ -30,7 +29,6 @@ export default function TRPCDemoPage() {
|
|||
<HelloExample />
|
||||
<UsersExample />
|
||||
<PostsExample />
|
||||
<FileUploadExample />
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,12 @@
|
|||
import express from "express";
|
||||
import cors from "cors";
|
||||
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;
|
||||
|
||||
// Parse cookies
|
||||
app.use(cookieParser());
|
||||
|
||||
// Enable CORS for frontend (adjust origin in production)
|
||||
app.use(
|
||||
cors({
|
||||
|
|
@ -28,9 +23,6 @@ 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",
|
||||
|
|
|
|||
|
|
@ -14,19 +14,15 @@
|
|||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@taylordb/query-builder": "^0.11.4",
|
||||
"@taylordb/query-builder": "^0.10.3",
|
||||
"@trpc/server": "^11.8.1",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { z } from "zod";
|
||||
import { router, publicProcedure } from "./trpc";
|
||||
import { usersRouter, postsRouter, submitUserDataRouter } from "./routers";
|
||||
import { usersRouter, postsRouter } from "./routers";
|
||||
|
||||
/**
|
||||
* Main tRPC Router
|
||||
|
|
@ -20,7 +20,6 @@ export const appRouter = router({
|
|||
// ============================================================================
|
||||
users: usersRouter,
|
||||
posts: postsRouter,
|
||||
submitUserData: submitUserDataRouter,
|
||||
|
||||
// ============================================================================
|
||||
// Global / Utility Procedures
|
||||
|
|
|
|||
|
|
@ -1,97 +0,0 @@
|
|||
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 };
|
||||
|
|
@ -7,4 +7,3 @@
|
|||
|
||||
export { usersRouter } from "./users";
|
||||
export { postsRouter } from "./posts";
|
||||
export { submitUserDataRouter } from "./submitUserData";
|
||||
|
|
|
|||
|
|
@ -6,39 +6,6 @@ import { router, publicProcedure } from "../trpc";
|
|||
*
|
||||
* Another example sub-router showing a different domain.
|
||||
* Demonstrates relationships (author references users).
|
||||
*
|
||||
* Example using TaylorDB queryBuilder (available via ctx.queryBuilder):
|
||||
*
|
||||
* // Query all posts
|
||||
* const posts = await ctx.queryBuilder.from("posts").select("*");
|
||||
*
|
||||
* // Query with filters
|
||||
* const publishedPosts = await ctx.queryBuilder
|
||||
* .from("posts")
|
||||
* .where({ published: true })
|
||||
* .select("*");
|
||||
*
|
||||
* // Create a new post
|
||||
* const newPost = await ctx.queryBuilder
|
||||
* .from("posts")
|
||||
* .insert({
|
||||
* title: "My Title",
|
||||
* content: "My Content",
|
||||
* authorId: 1,
|
||||
* published: false
|
||||
* });
|
||||
*
|
||||
* // Update a post
|
||||
* const updatedPost = await ctx.queryBuilder
|
||||
* .from("posts")
|
||||
* .where({ id: 1 })
|
||||
* .update({ published: true });
|
||||
*
|
||||
* // Delete a post
|
||||
* await ctx.queryBuilder
|
||||
* .from("posts")
|
||||
* .where({ id: 1 })
|
||||
* .delete();
|
||||
*/
|
||||
|
||||
// In-memory store for demonstration
|
||||
|
|
@ -69,7 +36,7 @@ export const postsRouter = router({
|
|||
published: z.boolean().optional(),
|
||||
authorId: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
.optional()
|
||||
)
|
||||
.query(({ input }) => {
|
||||
let result = posts;
|
||||
|
|
@ -121,7 +88,7 @@ export const postsRouter = router({
|
|||
title: z.string().min(1).max(200).optional(),
|
||||
content: z.string().min(1).optional(),
|
||||
published: z.boolean().optional(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.mutation(({ input }) => {
|
||||
const post = posts.find((p) => p.id === input.id);
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
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;
|
||||
}),
|
||||
});
|
||||
319
apps/server/taylordb/query-builder.ts
Normal file
319
apps/server/taylordb/query-builder.ts
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
import { createQueryBuilder } from "@taylordb/query-builder";
|
||||
import type { TaylorDatabase } from "./types.js";
|
||||
|
||||
/**
|
||||
* TaylorDB Query Builder Instance
|
||||
*
|
||||
* This is the main query builder instance configured with your TaylorDB credentials.
|
||||
* Use this to perform all database operations in a type-safe manner.
|
||||
*/
|
||||
export const queryBuilder = createQueryBuilder<TaylorDatabase>({
|
||||
baseUrl: process.env.TAYLORDB_BASE_URL!,
|
||||
baseId: process.env.TAYLORDB_SERVER_ID!,
|
||||
apiKey: process.env.TAYLORDB_API_TOKEN!,
|
||||
});
|
||||
|
||||
/**
|
||||
* ============================================================================
|
||||
* Example Query Functions
|
||||
* ============================================================================
|
||||
*
|
||||
* Below are example patterns for common database operations.
|
||||
* Replace these with your own functions based on your actual schema.
|
||||
*
|
||||
* For comprehensive examples, see: /docs/TAYLORDB_QUERY_REFERENCE.md
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// READ Operations (Queries)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Example: Get all records from a table
|
||||
*
|
||||
* @example
|
||||
* export async function getAllUsers() {
|
||||
* return await queryBuilder
|
||||
* .selectFrom("users")
|
||||
* .select(["id", "name", "email", "createdAt"])
|
||||
* .orderBy("createdAt", "desc")
|
||||
* .execute();
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* Example: Get a single record by ID
|
||||
*
|
||||
* @example
|
||||
* export async function getUserById(id: number) {
|
||||
* return await queryBuilder
|
||||
* .selectFrom("users")
|
||||
* .where("id", "=", id)
|
||||
* .executeTakeFirst();
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* Example: Get records with filtering
|
||||
*
|
||||
* @example
|
||||
* export async function getActiveUsers() {
|
||||
* return await queryBuilder
|
||||
* .selectFrom("users")
|
||||
* .where("status", "=", "active")
|
||||
* .orderBy("name", "asc")
|
||||
* .execute();
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* Example: Get records with date range filtering
|
||||
*
|
||||
* @example
|
||||
* export async function getRecordsInDateRange(startDate: string, endDate: string) {
|
||||
* return await queryBuilder
|
||||
* .selectFrom("records")
|
||||
* .where("date", ">=", ["exactDay", startDate])
|
||||
* .where("date", "<=", ["exactDay", endDate])
|
||||
* .orderBy("date", "asc")
|
||||
* .execute();
|
||||
* }
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// CREATE Operations (Insert)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Example: Insert a new record
|
||||
*
|
||||
* @example
|
||||
* export async function createUser(data: { name: string; email: string }) {
|
||||
* return await queryBuilder
|
||||
* .insertInto("users")
|
||||
* .values({
|
||||
* name: data.name,
|
||||
* email: data.email,
|
||||
* status: "active",
|
||||
* })
|
||||
* .executeTakeFirst();
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* Example: Insert with single-select field
|
||||
*
|
||||
* Note: Single-select fields must be wrapped in an array
|
||||
*
|
||||
* @example
|
||||
* export async function createTask(data: { title: string; priority: "low" | "medium" | "high" }) {
|
||||
* return await queryBuilder
|
||||
* .insertInto("tasks")
|
||||
* .values({
|
||||
* title: data.title,
|
||||
* priority: [data.priority], // Wrap in array for single-select
|
||||
* })
|
||||
* .executeTakeFirst();
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* Example: Insert with computed fields
|
||||
*
|
||||
* @example
|
||||
* export async function createOrder(data: { quantity: number; pricePerUnit: number }) {
|
||||
* const totalPrice = data.quantity * data.pricePerUnit;
|
||||
*
|
||||
* return await queryBuilder
|
||||
* .insertInto("orders")
|
||||
* .values({
|
||||
* quantity: data.quantity,
|
||||
* pricePerUnit: data.pricePerUnit,
|
||||
* totalPrice: totalPrice,
|
||||
* })
|
||||
* .executeTakeFirst();
|
||||
* }
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// UPDATE Operations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Example: Update a record
|
||||
*
|
||||
* @example
|
||||
* export async function updateUser(id: number, data: { name?: string; email?: string }) {
|
||||
* return await queryBuilder
|
||||
* .update("users")
|
||||
* .set(data)
|
||||
* .where("id", "=", id)
|
||||
* .execute();
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* Example: Update with conditional recalculation
|
||||
*
|
||||
* @example
|
||||
* export async function updateOrder(id: number, data: { quantity?: number; pricePerUnit?: number }) {
|
||||
* // Fetch current record to compute total
|
||||
* const currentOrder = await queryBuilder
|
||||
* .selectFrom("orders")
|
||||
* .select(["quantity", "pricePerUnit"])
|
||||
* .where("id", "=", id)
|
||||
* .executeTakeFirst();
|
||||
*
|
||||
* if (!currentOrder) {
|
||||
* throw new Error("Order not found");
|
||||
* }
|
||||
*
|
||||
* const newQuantity = data.quantity ?? currentOrder.quantity ?? 0;
|
||||
* const newPrice = data.pricePerUnit ?? currentOrder.pricePerUnit ?? 0;
|
||||
* const totalPrice = newQuantity * newPrice;
|
||||
*
|
||||
* return await queryBuilder
|
||||
* .update("orders")
|
||||
* .set({
|
||||
* ...data,
|
||||
* totalPrice,
|
||||
* })
|
||||
* .where("id", "=", id)
|
||||
* .execute();
|
||||
* }
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// DELETE Operations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Example: Delete a single record
|
||||
*
|
||||
* @example
|
||||
* export async function deleteUser(id: number) {
|
||||
* return await queryBuilder
|
||||
* .deleteFrom("users")
|
||||
* .where("id", "=", id)
|
||||
* .execute();
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* Example: Delete multiple records by IDs
|
||||
*
|
||||
* @example
|
||||
* export async function deleteUsers(ids: number[]) {
|
||||
* return await queryBuilder
|
||||
* .deleteFrom("users")
|
||||
* .where("id", "hasAnyOf", ids)
|
||||
* .execute();
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* Example: Delete with condition
|
||||
*
|
||||
* @example
|
||||
* export async function deleteInactiveUsers() {
|
||||
* return await queryBuilder
|
||||
* .deleteFrom("users")
|
||||
* .where("status", "=", "inactive")
|
||||
* .execute();
|
||||
* }
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// AGGREGATION Operations (Manual)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Example: Calculate statistics
|
||||
*
|
||||
* @example
|
||||
* 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),
|
||||
* };
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* Example: Sum totals for a date
|
||||
*
|
||||
* @example
|
||||
* export async function getTotalSalesForDate(date: string) {
|
||||
* const sales = await queryBuilder
|
||||
* .selectFrom("sales")
|
||||
* .select(["amount", "quantity"])
|
||||
* .where("date", "=", ["exactDay", date])
|
||||
* .execute();
|
||||
*
|
||||
* return {
|
||||
* totalAmount: sales.reduce((sum, s) => sum + (s.amount ?? 0), 0),
|
||||
* totalQuantity: sales.reduce((sum, s) => sum + (s.quantity ?? 0), 0),
|
||||
* };
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* ============================================================================
|
||||
* Query Builder Quick Reference
|
||||
* ============================================================================
|
||||
*
|
||||
* SELECT:
|
||||
* - .selectFrom("tableName")
|
||||
* - .select(["field1", "field2"])
|
||||
* - .execute() // Returns array
|
||||
* - .executeTakeFirst() // Returns single record or undefined
|
||||
*
|
||||
* WHERE:
|
||||
* - .where("field", "=", value)
|
||||
* - .where("field", ">", value)
|
||||
* - .where("field", "hasAnyOf", [value1, value2])
|
||||
* - .where("date", ">=", ["exactDay", "2024-01-01"])
|
||||
*
|
||||
* ORDER BY:
|
||||
* - .orderBy("field", "asc")
|
||||
* - .orderBy("field", "desc")
|
||||
*
|
||||
* INSERT:
|
||||
* - .insertInto("tableName")
|
||||
* - .values({ field1: value1, field2: value2 })
|
||||
* - .executeTakeFirst()
|
||||
*
|
||||
* UPDATE:
|
||||
* - .update("tableName")
|
||||
* - .set({ field1: value1 })
|
||||
* - .where("id", "=", id)
|
||||
* - .execute()
|
||||
*
|
||||
* DELETE:
|
||||
* - .deleteFrom("tableName")
|
||||
* - .where("id", "=", id)
|
||||
* - .execute()
|
||||
*
|
||||
* Field Types:
|
||||
* - Text: string
|
||||
* - Number: number
|
||||
* - Date: ["exactDay", "YYYY-MM-DD"]
|
||||
* - Single Select: ["option"]
|
||||
* - Multi Select: ["opt1", "opt2"]
|
||||
* - Boolean: true/false
|
||||
*
|
||||
* For comprehensive examples, see: /docs/TAYLORDB_QUERY_REFERENCE.md
|
||||
*/
|
||||
|
|
@ -1,30 +1,15 @@
|
|||
import { createQueryBuilder } from "@taylordb/query-builder";
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
||||
import type { TaylorDatabase } from "./taylordb/types";
|
||||
|
||||
/**
|
||||
* Create context for each tRPC request
|
||||
* This is where you can add user session, database clients, etc.
|
||||
*/
|
||||
export const createContext = ({ req, res }: CreateExpressContextOptions) => {
|
||||
// Extract app_access_token from cookies
|
||||
const appAccessToken = req.cookies?.app_access_token;
|
||||
|
||||
if (!appAccessToken) {
|
||||
throw new Error("Unauthorized: app_access_token cookie is required");
|
||||
}
|
||||
|
||||
const queryBuilder = createQueryBuilder<TaylorDatabase>({
|
||||
baseUrl: process.env.TAYLORDB_BASE_URL!,
|
||||
baseId: process.env.TAYLORDB_SERVER_ID!,
|
||||
apiKey: appAccessToken,
|
||||
});
|
||||
|
||||
return {
|
||||
req,
|
||||
res,
|
||||
queryBuilder,
|
||||
// Add any shared context here (e.g., database client, user session)
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,55 +1,651 @@
|
|||
# shadcn/ui Dashboard Components Guide
|
||||
|
||||
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.
|
||||
This guide provides examples of using shadcn/ui components to build modern dashboard interfaces for your TaylorDB applications.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Topics
|
||||
## 📦 Installation
|
||||
|
||||
- **Installation**
|
||||
See `SHADCN_INSTALLATION.md` for:
|
||||
- Installing individual components (core, dashboard, data display, advanced)
|
||||
- Command examples for `pnpm dlx shadcn@latest add`
|
||||
### Install Individual Components
|
||||
|
||||
- **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)
|
||||
```bash
|
||||
# Core components (already included)
|
||||
pnpm dlx shadcn@latest add button card input label textarea select tabs alert
|
||||
|
||||
- **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)
|
||||
# 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How Agents Should Use These Docs
|
||||
## 🎨 Common Dashboard Patterns
|
||||
|
||||
1. **Need to add a component?**
|
||||
Use `SHADCN_INSTALLATION.md` for the exact `pnpm dlx shadcn@latest add` commands.
|
||||
### 1. Dashboard Layout with Stats Cards
|
||||
|
||||
2. **Building a dashboard or CRUD UI?**
|
||||
Use `SHADCN_DASHBOARD_PATTERNS.md` for copy-paste patterns (tables, dialogs, forms, toasts, sheets, etc.).
|
||||
```typescript
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
3. **Styling and layout?**
|
||||
Use `SHADCN_DESIGN_AND_LAYOUT.md` for tokens, spacing, responsive patterns, and performance.
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<div className="container mx-auto p-8">
|
||||
<h1 className="text-3xl font-bold mb-6">Dashboard</h1>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
Total Users
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold">1,234</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
+20% from last month
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
Revenue
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold">$45,231</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
+12% from last month
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* More stats cards... */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Users</CardTitle>
|
||||
<CardDescription>Manage your user accounts</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell className="font-medium">{user.name}</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={user.status === "active" ? "default" : "secondary"}
|
||||
>
|
||||
{user.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-destructive">
|
||||
<Trash className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<PlusIcon className="mr-2 h-4 w-4" />
|
||||
Add User
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New User</DialogTitle>
|
||||
<DialogDescription>Add a new user to your database</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="john@example.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => createMutation.mutate({ name, email })}
|
||||
disabled={createMutation.isPending}
|
||||
>
|
||||
Create User
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<div className="container mx-auto p-8">
|
||||
<Skeleton className="h-10 w-48 mb-6" />
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-8 w-20 mb-2" />
|
||||
<Skeleton className="h-3 w-32" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage in main component
|
||||
export default function DashboardPage() {
|
||||
const { data: stats, isLoading } = trpc.dashboard.getStats.useQuery();
|
||||
|
||||
if (isLoading) {
|
||||
return <DashboardSkeleton />;
|
||||
}
|
||||
|
||||
return <div>{/* Actual dashboard */}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Tabs for Different Views
|
||||
|
||||
```typescript
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
export default function DataExplorer() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Data Explorer</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="overview">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
||||
<TabsTrigger value="reports">Reports</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-4">
|
||||
{/* Overview content */}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="analytics" className="space-y-4">
|
||||
{/* Analytics content */}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="reports" className="space-y-4">
|
||||
{/* Reports content */}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<Badge variant={variants[status as keyof typeof variants] || "outline"}>
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
<StatusBadge status="active" />;
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<Button onClick={() => deleteMutation.mutate({ id: 1 })}>
|
||||
Delete Item
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// Don't forget to add <Toaster /> 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<typeof formSchema>) => {
|
||||
createMutation.mutate(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="John Doe" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your full name as it appears on documents.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" placeholder="john@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={createMutation.isPending}>
|
||||
{createMutation.isPending ? "Submitting..." : "Submit"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline">View Details</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>User Details</SheetTitle>
|
||||
<SheetDescription>Information about this user</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
{user && (
|
||||
<div className="mt-6 space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">
|
||||
Name
|
||||
</h4>
|
||||
<p className="text-base">{user.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">
|
||||
Email
|
||||
</h4>
|
||||
<p className="text-base">{user.email}</p>
|
||||
</div>
|
||||
{/* More fields... */}
|
||||
</div>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<CommandInput placeholder="Search..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup heading="Users">
|
||||
<CommandItem>John Doe</CommandItem>
|
||||
<CommandItem>Jane Smith</CommandItem>
|
||||
</CommandGroup>
|
||||
<CommandGroup heading="Projects">
|
||||
<CommandItem>Project Alpha</CommandItem>
|
||||
<CommandItem>Project Beta</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
## 🎨 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";
|
||||
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Item
|
||||
</Button>;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📱 Responsive Design
|
||||
|
||||
### Grid Layouts
|
||||
|
||||
```typescript
|
||||
// 1 column on mobile, 2 on tablet, 4 on desktop
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{/* cards */}
|
||||
</div>
|
||||
|
||||
// 1 column on mobile, 3 on desktop
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
{/* cards */}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Hide on Mobile
|
||||
|
||||
```typescript
|
||||
// Hide text on mobile, show on desktop
|
||||
<span className="hidden sm:inline">Dashboard</span>
|
||||
|
||||
// Different layout on mobile
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
{/* content */}
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 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
|
||||
|
||||
- **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!
|
||||
|
|
|
|||
|
|
@ -1,566 +0,0 @@
|
|||
# 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 (
|
||||
<div className="container mx-auto p-8">
|
||||
<h1 className="text-3xl font-bold mb-6">Dashboard</h1>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
Total Users
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold">1,234</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
+20% from last month
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
Revenue
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold">$45,231</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
+12% from last month
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* More stats cards... */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Users</CardTitle>
|
||||
<CardDescription>Manage your user accounts</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell className="font-medium">{user.name}</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={user.status === "active" ? "default" : "secondary"}
|
||||
>
|
||||
{user.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-destructive">
|
||||
<Trash className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<PlusIcon className="mr-2 h-4 w-4" />
|
||||
Add User
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New User</DialogTitle>
|
||||
<DialogDescription>Add a new user to your database</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="john@example.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => createMutation.mutate({ name, email })}
|
||||
disabled={createMutation.isPending}
|
||||
>
|
||||
Create User
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 (
|
||||
<div className="container mx-auto p-8">
|
||||
<Skeleton className="h-10 w-48 mb-6" />
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-8 w-20 mb-2" />
|
||||
<Skeleton className="h-3 w-32" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage in main component
|
||||
export default function DashboardPage() {
|
||||
const { data: stats, isLoading } = trpc.dashboard.getStats.useQuery();
|
||||
|
||||
if (isLoading) {
|
||||
return <DashboardSkeleton />;
|
||||
}
|
||||
|
||||
return <div>{/* Actual dashboard */}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Tabs for Different Views
|
||||
|
||||
```typescript
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
export default function DataExplorer() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Data Explorer</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="overview">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
||||
<TabsTrigger value="reports">Reports</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-4">
|
||||
{/* Overview content */}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="analytics" className="space-y-4">
|
||||
{/* Analytics content */}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="reports" className="space-y-4">
|
||||
{/* Reports content */}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 (
|
||||
<Badge variant={variants[status as keyof typeof variants] || "outline"}>
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
<StatusBadge status="active" />;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 (
|
||||
<Button onClick={() => deleteMutation.mutate({ id: 1 })}>
|
||||
Delete Item
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// Don't forget to add <Toaster /> 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<typeof formSchema>) => {
|
||||
createMutation.mutate(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="John Doe" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your full name as it appears on documents.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" placeholder="john@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={createMutation.isPending}>
|
||||
{createMutation.isPending ? "Submitting..." : "Submit"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline">View Details</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>User Details</SheetTitle>
|
||||
<SheetDescription>Information about this user</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
{user && (
|
||||
<div className="mt-6 space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">
|
||||
Name
|
||||
</h4>
|
||||
<p className="text-base">{user.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">
|
||||
Email
|
||||
</h4>
|
||||
<p className="text-base">{user.email}</p>
|
||||
</div>
|
||||
{/* More fields... */}
|
||||
</div>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 (
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<CommandInput placeholder="Search..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup heading="Users">
|
||||
<CommandItem>John Doe</CommandItem>
|
||||
<CommandItem>Jane Smith</CommandItem>
|
||||
</CommandGroup>
|
||||
<CommandGroup heading="Projects">
|
||||
<CommandItem>Project Alpha</CommandItem>
|
||||
<CommandItem>Project Beta</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For installation and design/layout, see:
|
||||
|
||||
- **Installation**: `SHADCN_INSTALLATION.md`
|
||||
- **Design & layout**: `SHADCN_DESIGN_AND_LAYOUT.md`
|
||||
- **Main guide index**: `SHADCN_COMPONENTS_GUIDE.md`
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
# 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";
|
||||
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Item
|
||||
</Button>;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Responsive Design
|
||||
|
||||
### Grid Layouts
|
||||
|
||||
```typescript
|
||||
// 1 column on mobile, 2 on tablet, 4 on desktop
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{/* cards */}
|
||||
</div>
|
||||
|
||||
// 1 column on mobile, 3 on desktop
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
{/* cards */}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Hide on Mobile
|
||||
|
||||
```typescript
|
||||
// Hide text on mobile, show on desktop
|
||||
<span className="hidden sm:inline">Dashboard</span>
|
||||
|
||||
// Different layout on mobile
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
{/* content */}
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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`
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
# 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`
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
# 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
|
||||
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
# 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
|
||||
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
# 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
|
||||
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
# 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
|
||||
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
# 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
|
||||
|
||||
|
|
@ -1,78 +1,700 @@
|
|||
# TaylorDB Query Builder Reference
|
||||
|
||||
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.
|
||||
This document provides comprehensive examples of how to use the TaylorDB query builder for all common database operations.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Topics
|
||||
## 📚 Table of Contents
|
||||
|
||||
- **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
|
||||
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. [Common Pitfalls](#common-pitfalls)
|
||||
|
||||
---
|
||||
|
||||
## How Agents Should Use These Docs
|
||||
## Setup & Configuration
|
||||
|
||||
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`.
|
||||
### Initialize Query Builder
|
||||
|
||||
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)
|
||||
```typescript
|
||||
import { createQueryBuilder } from "@taylordb/query-builder";
|
||||
import type { TaylorDatabase } from "./types.js";
|
||||
|
||||
3. **Check pitfalls before finalizing**
|
||||
Before shipping queries, skim `TAYLORDB_PITFALLS_BEST_PRACTICES.md` to avoid common errors.
|
||||
export const queryBuilder = createQueryBuilder<TaylorDatabase>({
|
||||
baseUrl: process.env.TAYLORDB_BASE_URL!,
|
||||
baseId: process.env.TAYLORDB_SERVER_ID!,
|
||||
apiKey: process.env.TAYLORDB_API_TOKEN!,
|
||||
});
|
||||
```
|
||||
|
||||
The type parameter `<TaylorDatabase>` provides full type safety based on your generated schema.
|
||||
|
||||
---
|
||||
|
||||
## 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"]);
|
||||
```
|
||||
|
||||
### Single Select Filtering
|
||||
|
||||
```typescript
|
||||
// For single-select fields (stored as arrays in TaylorDB)
|
||||
export async function getUsersByRole(role: string) {
|
||||
return await queryBuilder
|
||||
.selectFrom("users")
|
||||
.where("role", "=", role)
|
||||
.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
|
||||
|
||||
```typescript
|
||||
export async function createTask(data: {
|
||||
title: string;
|
||||
priority: "low" | "medium" | "high";
|
||||
}) {
|
||||
return await queryBuilder
|
||||
.insertInto("tasks")
|
||||
.values({
|
||||
title: data.title,
|
||||
priority: [data.priority], // Wrap in array for single-select
|
||||
})
|
||||
.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: [priority] }) // Wrap in array
|
||||
.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"]` | `tags: ["opt1", "opt2"]` |
|
||||
| **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], // Single select as array
|
||||
})
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
export async function getTasksByStatus(
|
||||
status: (typeof TaskStatusOptions)[number],
|
||||
) {
|
||||
return await queryBuilder
|
||||
.selectFrom("tasks")
|
||||
.where("status", "=", status)
|
||||
.execute();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### ❌ Pitfall 1: Not Wrapping Single-Select in Array
|
||||
|
||||
```typescript
|
||||
// ❌ WRONG
|
||||
.values({ priority: "high" })
|
||||
|
||||
// ✅ CORRECT
|
||||
.values({ priority: ["high"] })
|
||||
```
|
||||
|
||||
### ❌ 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
|
||||
|
||||
---
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- **Generated Types**: `apps/server/taylordb/types.ts`
|
||||
- **Example Queries in This Template**: `apps/server/taylordb/query-builder.ts`
|
||||
- **tRPC Integration**: `apps/server/router.ts`
|
||||
- **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`
|
||||
|
||||
---
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
|
|
|||
|
|
@ -1,265 +0,0 @@
|
|||
# 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
|
||||
|
||||
199
pnpm-lock.yaml
199
pnpm-lock.yaml
|
|
@ -139,39 +139,27 @@ importers:
|
|||
apps/server:
|
||||
dependencies:
|
||||
'@taylordb/query-builder':
|
||||
specifier: ^0.11.4
|
||||
version: 0.11.4
|
||||
specifier: ^0.10.3
|
||||
version: 0.10.3
|
||||
'@trpc/server':
|
||||
specifier: ^11.8.1
|
||||
version: 11.8.1(typescript@5.9.3)
|
||||
cookie-parser:
|
||||
specifier: ^1.4.7
|
||||
version: 1.4.7
|
||||
cors:
|
||||
specifier: ^2.8.5
|
||||
version: 2.8.5
|
||||
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
|
||||
devDependencies:
|
||||
'@types/cookie-parser':
|
||||
specifier: ^1.4.10
|
||||
version: 1.4.10(@types/express@5.0.6)
|
||||
'@types/cors':
|
||||
specifier: ^2.8.19
|
||||
version: 2.8.19
|
||||
'@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
|
||||
|
|
@ -1285,15 +1273,9 @@ 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:
|
||||
|
|
@ -1333,11 +1315,6 @@ packages:
|
|||
'@types/connect@3.4.38':
|
||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||
|
||||
'@types/cookie-parser@1.4.10':
|
||||
resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==}
|
||||
peerDependencies:
|
||||
'@types/express': '*'
|
||||
|
||||
'@types/cors@2.8.19':
|
||||
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
|
||||
|
||||
|
|
@ -1383,9 +1360,6 @@ 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==}
|
||||
|
||||
|
|
@ -1502,9 +1476,6 @@ 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==}
|
||||
|
||||
|
|
@ -1538,13 +1509,6 @@ 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'}
|
||||
|
|
@ -1589,10 +1553,6 @@ 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'}
|
||||
|
|
@ -1609,13 +1569,6 @@ packages:
|
|||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
cookie-parser@1.4.7:
|
||||
resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
cookie-signature@1.0.6:
|
||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||
|
||||
cookie-signature@1.2.2:
|
||||
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
||||
engines: {node: '>=6.6.0'}
|
||||
|
|
@ -2171,10 +2124,6 @@ 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'}
|
||||
|
|
@ -2191,18 +2140,10 @@ 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'}
|
||||
|
|
@ -2214,20 +2155,9 @@ 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}
|
||||
|
|
@ -2402,10 +2332,6 @@ 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'}
|
||||
|
|
@ -2455,9 +2381,6 @@ 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==}
|
||||
|
||||
|
|
@ -2528,17 +2451,10 @@ 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'}
|
||||
|
|
@ -2605,17 +2521,10 @@ 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}
|
||||
|
|
@ -2669,9 +2578,6 @@ 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'}
|
||||
|
|
@ -2751,10 +2657,6 @@ 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'}
|
||||
|
|
@ -3684,28 +3586,10 @@ 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)
|
||||
|
|
@ -3754,10 +3638,6 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/node': 24.10.1
|
||||
|
||||
'@types/cookie-parser@1.4.10(@types/express@5.0.6)':
|
||||
dependencies:
|
||||
'@types/express': 5.0.6
|
||||
|
||||
'@types/cors@2.8.19':
|
||||
dependencies:
|
||||
'@types/node': 24.10.1
|
||||
|
|
@ -3805,10 +3685,6 @@ 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
|
||||
|
|
@ -3965,8 +3841,6 @@ snapshots:
|
|||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
append-field@1.0.0: {}
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
aria-hidden@1.2.6:
|
||||
|
|
@ -4012,12 +3886,6 @@ 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:
|
||||
|
|
@ -4059,13 +3927,6 @@ 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
|
||||
|
|
@ -4081,13 +3942,6 @@ snapshots:
|
|||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
cookie-parser@1.4.7:
|
||||
dependencies:
|
||||
cookie: 0.7.2
|
||||
cookie-signature: 1.0.6
|
||||
|
||||
cookie-signature@1.0.6: {}
|
||||
|
||||
cookie-signature@1.2.2: {}
|
||||
|
||||
cookie@0.7.2: {}
|
||||
|
|
@ -4659,8 +4513,6 @@ snapshots:
|
|||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
media-typer@0.3.0: {}
|
||||
|
||||
media-typer@1.1.0: {}
|
||||
|
||||
merge-descriptors@2.0.0: {}
|
||||
|
|
@ -4672,14 +4524,8 @@ 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
|
||||
|
|
@ -4692,24 +4538,8 @@ 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: {}
|
||||
|
|
@ -4854,12 +4684,6 @@ 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)
|
||||
|
|
@ -4944,8 +4768,6 @@ snapshots:
|
|||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
safe-buffer@5.2.1: {}
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
|
|
@ -5039,18 +4861,12 @@ 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
|
||||
|
|
@ -5105,19 +4921,12 @@ 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)
|
||||
|
|
@ -5164,8 +4973,6 @@ snapshots:
|
|||
dependencies:
|
||||
react: 19.2.0
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
victory-vendor@37.3.6:
|
||||
|
|
@ -5218,8 +5025,6 @@ snapshots:
|
|||
|
||||
xmlhttprequest-ssl@2.1.2: {}
|
||||
|
||||
xtend@4.0.2: {}
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ services:
|
|||
env:
|
||||
vars:
|
||||
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
|
||||
TAYLORDB_API_TOKEN: secrets.TAYLORDB_API_TOKEN
|
||||
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
|
||||
FRONTEND_URL: routing.client.url
|
||||
|
||||
|
|
@ -30,6 +31,7 @@ services:
|
|||
env:
|
||||
vars:
|
||||
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
|
||||
TAYLORDB_API_TOKEN: secrets.TAYLORDB_API_TOKEN
|
||||
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
|
||||
FRONTEND_URL: routing.client.url
|
||||
|
||||
|
|
@ -39,6 +41,7 @@ services:
|
|||
env:
|
||||
vars:
|
||||
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
|
||||
TAYLORDB_API_TOKEN: secrets.TAYLORDB_API_TOKEN
|
||||
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
|
||||
FRONTEND_URL: routing.client.url
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user