Compare commits
12 Commits
d51a2f372f
...
26b00bcf61
| Author | SHA1 | Date | |
|---|---|---|---|
| 26b00bcf61 | |||
|
|
62bd7b0a6c | ||
|
|
71df6a350d | ||
|
|
f6c614016d | ||
|
|
0050023af0 | ||
|
|
90fc22e55c | ||
|
|
1c33189fca | ||
|
|
73a4cf2ca0 | ||
|
|
f9d49df4d6 | ||
|
|
474e99c253 | ||
| 81a2e1f87d | |||
| 091e06fead |
111
AGENTS.md
111
AGENTS.md
|
|
@ -15,6 +15,29 @@ Build production-ready, modern web applications (primarily dashboards and CRUD i
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 📚 Docs for Agents
|
||||||
|
|
||||||
|
Before you start implementing, skim these docs in the `docs/` folder:
|
||||||
|
|
||||||
|
- **TaylorDB Query Builder**
|
||||||
|
- `docs/TAYLORDB_QUERY_REFERENCE.md` – index for all query examples
|
||||||
|
- `docs/TAYLORDB_BASIC_QUERIES.md` – basic reads, filtering, dates
|
||||||
|
- `docs/TAYLORDB_WRITE_OPERATIONS.md` – inserts, updates, deletes
|
||||||
|
- `docs/TAYLORDB_ADVANCED_PATTERNS.md` – aggregations, pagination, conditional queries
|
||||||
|
- `docs/TAYLORDB_FIELD_TYPES.md` – field type mapping & enums
|
||||||
|
- `docs/TAYLORDB_ATTACHMENTS.md` – working with attachment fields
|
||||||
|
- `docs/TAYLORDB_PITFALLS_BEST_PRACTICES.md` – common mistakes & best practices
|
||||||
|
|
||||||
|
- **shadcn/ui Components & Dashboard Patterns**
|
||||||
|
- `docs/SHADCN_COMPONENTS_GUIDE.md` – index for all shadcn/ui docs
|
||||||
|
- `docs/SHADCN_INSTALLATION.md` – how to install shadcn/ui components
|
||||||
|
- `docs/SHADCN_DASHBOARD_PATTERNS.md` – ready-made dashboard/layout patterns
|
||||||
|
- `docs/SHADCN_DESIGN_AND_LAYOUT.md` – design tokens, layout, responsive & performance tips
|
||||||
|
|
||||||
|
Use `AGENTS.md` for **workflow and rules** and the `docs/` files for **detailed code examples**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📋 Development Workflow
|
## 📋 Development Workflow
|
||||||
|
|
||||||
### **Phase 1: Understand Requirements & Design**
|
### **Phase 1: Understand Requirements & Design**
|
||||||
|
|
@ -54,19 +77,15 @@ Document your design decisions briefly before implementing.
|
||||||
|
|
||||||
#### Step 1: Set Up Server-Side Data Layer
|
#### Step 1: Set Up Server-Side Data Layer
|
||||||
|
|
||||||
**File: `apps/server/taylordb/query-builder.ts`**
|
Use querybuilder which is in **File: `apps/server/taylordb/query-builder.ts`**
|
||||||
|
|
||||||
This file contains all database operations. Create type-safe CRUD functions for each table:
|
You can access the query builder from
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { createQueryBuilder } from "@taylordb/query-builder";
|
publicProcedure.input({}).query(({ input, ctx }) => {
|
||||||
import type { TaylorDatabase } from "./types.js";
|
const queryBuilder = ctx.queryBuilder;
|
||||||
|
|
||||||
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
|
// READ Operations
|
||||||
|
|
@ -239,21 +258,16 @@ Update the design tokens based on your chosen color scheme:
|
||||||
|
|
||||||
#### Step 2: Create shadcn/ui Components
|
#### Step 2: Create shadcn/ui Components
|
||||||
|
|
||||||
**Always use shadcn/ui components**. Install components as needed:
|
**Always use shadcn/ui components**. Install components as needed with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dlx shadcn@latest add <component-name>
|
pnpm dlx shadcn@latest add <component-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
**Available by default:**
|
For concrete install commands and patterns:
|
||||||
|
|
||||||
- `button`, `card`, `input`, `label`, `textarea`, `select`, `tabs`, `alert`
|
- See `docs/SHADCN_INSTALLATION.md` for component install snippets
|
||||||
|
- See `docs/SHADCN_DASHBOARD_PATTERNS.md` for tables, dialogs, forms, toasts, sheets, command palette, etc.
|
||||||
**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
|
#### Step 3: Build Page Components
|
||||||
|
|
||||||
|
|
@ -580,60 +594,17 @@ apps/client/src/
|
||||||
|
|
||||||
## 🔧 TaylorDB Query Builder Reference
|
## 🔧 TaylorDB Query Builder Reference
|
||||||
|
|
||||||
### Common Query Patterns
|
Instead of duplicating examples here, use the dedicated docs:
|
||||||
|
|
||||||
```typescript
|
- `docs/TAYLORDB_QUERY_REFERENCE.md` – index for all query builder docs
|
||||||
// SELECT with specific fields
|
- `docs/TAYLORDB_BASIC_QUERIES.md` – `selectFrom`, filtering, ordering, date filters
|
||||||
queryBuilder
|
- `docs/TAYLORDB_WRITE_OPERATIONS.md` – `insertInto`, `update`, `deleteFrom`
|
||||||
.selectFrom("tableName")
|
- `docs/TAYLORDB_ADVANCED_PATTERNS.md` – aggregations, totals, conditional queries, pagination
|
||||||
.select(["id", "name", "status"])
|
- `docs/TAYLORDB_FIELD_TYPES.md` – field type mapping, nullable handling, enums
|
||||||
.execute();
|
- `docs/TAYLORDB_ATTACHMENTS.md` – selecting and writing attachment fields
|
||||||
|
- `docs/TAYLORDB_PITFALLS_BEST_PRACTICES.md` – pitfalls and best practices
|
||||||
|
|
||||||
// WHERE conditions
|
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("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,7 +174,6 @@ This template is designed to deploy to TaylorDB's platform using the included `t
|
||||||
**Environment Variables Required:**
|
**Environment Variables Required:**
|
||||||
|
|
||||||
- `TAYLORDB_BASE_URL`
|
- `TAYLORDB_BASE_URL`
|
||||||
- `TAYLORDB_API_TOKEN`
|
|
||||||
- `TAYLORDB_SERVER_ID`
|
- `TAYLORDB_SERVER_ID`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
269
apps/client/src/components/demo/examples/FileUploadExample.tsx
Normal file
269
apps/client/src/components/demo/examples/FileUploadExample.tsx
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
import { useState, useRef, useCallback } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Upload, X, FileIcon, CheckCircle2, Send } from "lucide-react";
|
||||||
|
import { DemoCard, InlineSpinner } from "@/components/demo";
|
||||||
|
import { trpc } from "@/lib/trpc";
|
||||||
|
|
||||||
|
interface FileMetadata {
|
||||||
|
originalName: string;
|
||||||
|
mimeType: string;
|
||||||
|
size: number;
|
||||||
|
sizeFormatted: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BASE_URL =
|
||||||
|
import.meta.env.VITE_TRPC_URL || "http://localhost:3001/api";
|
||||||
|
|
||||||
|
export function FileUploadExample() {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [files, setFiles] = useState<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,3 +1,4 @@
|
||||||
export { HelloExample } from "./HelloExample";
|
export { HelloExample } from "./HelloExample";
|
||||||
export { UsersExample } from "./UsersExample";
|
export { UsersExample } from "./UsersExample";
|
||||||
export { PostsExample } from "./PostsExample";
|
export { PostsExample } from "./PostsExample";
|
||||||
|
export { FileUploadExample } from "./FileUploadExample";
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
HelloExample,
|
HelloExample,
|
||||||
UsersExample,
|
UsersExample,
|
||||||
PostsExample,
|
PostsExample,
|
||||||
|
FileUploadExample,
|
||||||
} from "@/components/demo/examples";
|
} from "@/components/demo/examples";
|
||||||
|
|
||||||
export default function TRPCDemoPage() {
|
export default function TRPCDemoPage() {
|
||||||
|
|
@ -29,6 +30,7 @@ export default function TRPCDemoPage() {
|
||||||
<HelloExample />
|
<HelloExample />
|
||||||
<UsersExample />
|
<UsersExample />
|
||||||
<PostsExample />
|
<PostsExample />
|
||||||
|
<FileUploadExample />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
|
import cookieParser from "cookie-parser";
|
||||||
import * as trpcExpress from "@trpc/server/adapters/express";
|
import * as trpcExpress from "@trpc/server/adapters/express";
|
||||||
import { appRouter } from "./router.js";
|
import { appRouter } from "./router.js";
|
||||||
import { createContext } from "./trpc.js";
|
import { createContext } from "./trpc.js";
|
||||||
|
import { fileUploadRouter } from "./routers/fileUpload.js";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3001;
|
||||||
|
|
||||||
|
// Parse cookies
|
||||||
|
app.use(cookieParser());
|
||||||
|
|
||||||
// Enable CORS for frontend (adjust origin in production)
|
// Enable CORS for frontend (adjust origin in production)
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
|
|
@ -23,6 +28,9 @@ app.get("/api/health", (_req, res) => {
|
||||||
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
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
|
// tRPC middleware
|
||||||
app.use(
|
app.use(
|
||||||
"/api/trpc",
|
"/api/trpc",
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,19 @@
|
||||||
"start": "node dist/index.js"
|
"start": "node dist/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@taylordb/query-builder": "^0.10.3",
|
"@taylordb/query-builder": "^0.11.4",
|
||||||
"@trpc/server": "^11.8.1",
|
"@trpc/server": "^11.8.1",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"multer": "^2.0.2",
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/cookie-parser": "^1.4.10",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
|
"@types/multer": "^2.0.0",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"esbuild": "^0.27.2",
|
"esbuild": "^0.27.2",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { router, publicProcedure } from "./trpc";
|
import { router, publicProcedure } from "./trpc";
|
||||||
import { usersRouter, postsRouter } from "./routers";
|
import { usersRouter, postsRouter, submitUserDataRouter } from "./routers";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main tRPC Router
|
* Main tRPC Router
|
||||||
|
|
@ -20,6 +20,7 @@ export const appRouter = router({
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
users: usersRouter,
|
users: usersRouter,
|
||||||
posts: postsRouter,
|
posts: postsRouter,
|
||||||
|
submitUserData: submitUserDataRouter,
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Global / Utility Procedures
|
// Global / Utility Procedures
|
||||||
|
|
|
||||||
97
apps/server/routers/fileUpload.ts
Normal file
97
apps/server/routers/fileUpload.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
import { Router } from "express";
|
||||||
|
import multer from "multer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File Upload Router
|
||||||
|
*
|
||||||
|
* Express router for handling multipart FormData file uploads.
|
||||||
|
* tRPC v11's FormData support is designed for fetch-based adapters (Next.js, etc.),
|
||||||
|
* not the Express adapter. This dedicated Express route is the recommended pattern
|
||||||
|
* for handling file uploads in Express + tRPC stacks.
|
||||||
|
*
|
||||||
|
* Uses TaylorDB's uploadAttachments pattern to store files.
|
||||||
|
* See docs/TAYLORDB_ATTACHMENTS.md for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Configure multer with memory storage (files available as Buffer)
|
||||||
|
const upload = multer({
|
||||||
|
storage: multer.memoryStorage(),
|
||||||
|
limits: {
|
||||||
|
fileSize: 10 * 1024 * 1024, // 10 MB per file
|
||||||
|
files: 10, // max 10 files
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileUploadRouter = Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/upload
|
||||||
|
*
|
||||||
|
* Accepts multipart/form-data with:
|
||||||
|
* - files (File[], multiple files of any type)
|
||||||
|
*
|
||||||
|
* Returns metadata about each uploaded file.
|
||||||
|
*/
|
||||||
|
fileUploadRouter.post("/", upload.array("files", 10), (req, res) => {
|
||||||
|
const files = (req.files as Express.Multer.File[]) || [];
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: "At least one file is required.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileMetadata = files.map((file) => ({
|
||||||
|
originalName: file.originalname,
|
||||||
|
mimeType: file.mimetype,
|
||||||
|
size: file.size,
|
||||||
|
sizeFormatted: formatFileSize(file.size),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────
|
||||||
|
// TaylorDB Attachment Example (see docs/TAYLORDB_ATTACHMENTS.md)
|
||||||
|
//
|
||||||
|
// To store uploaded files in a TaylorDB record, convert multer buffers
|
||||||
|
// to Blobs and use qb.uploadAttachments():
|
||||||
|
//
|
||||||
|
// const attachments = await qb.uploadAttachments(
|
||||||
|
// files.map((file) => ({
|
||||||
|
// file: new Blob([file.buffer], { type: file.mimetype }),
|
||||||
|
// name: file.originalname,
|
||||||
|
// }))
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// // Then use the attachments when inserting/updating a record:
|
||||||
|
// await qb.insertInto("tableName").values({
|
||||||
|
// name: "Some record",
|
||||||
|
// documents: attachments, // attachment column
|
||||||
|
// }).execute();
|
||||||
|
//
|
||||||
|
// // Or when updating an existing record:
|
||||||
|
// await qb.update("tableName").set({
|
||||||
|
// documents: attachments,
|
||||||
|
// }).where("id", "=", recordId).execute();
|
||||||
|
// ────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
filesCount: files.length,
|
||||||
|
files: fileMetadata,
|
||||||
|
totalSize: formatFileSize(files.reduce((sum, f) => sum + f.size, 0)),
|
||||||
|
uploadedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Format bytes to human-readable string */
|
||||||
|
function formatFileSize(bytes: number): string {
|
||||||
|
if (bytes === 0) return "0 B";
|
||||||
|
const units = ["B", "KB", "MB", "GB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { fileUploadRouter };
|
||||||
|
|
@ -7,3 +7,4 @@
|
||||||
|
|
||||||
export { usersRouter } from "./users";
|
export { usersRouter } from "./users";
|
||||||
export { postsRouter } from "./posts";
|
export { postsRouter } from "./posts";
|
||||||
|
export { submitUserDataRouter } from "./submitUserData";
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,39 @@ import { router, publicProcedure } from "../trpc";
|
||||||
*
|
*
|
||||||
* Another example sub-router showing a different domain.
|
* Another example sub-router showing a different domain.
|
||||||
* Demonstrates relationships (author references users).
|
* 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
|
// In-memory store for demonstration
|
||||||
|
|
@ -36,7 +69,7 @@ export const postsRouter = router({
|
||||||
published: z.boolean().optional(),
|
published: z.boolean().optional(),
|
||||||
authorId: z.number().optional(),
|
authorId: z.number().optional(),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional(),
|
||||||
)
|
)
|
||||||
.query(({ input }) => {
|
.query(({ input }) => {
|
||||||
let result = posts;
|
let result = posts;
|
||||||
|
|
@ -88,7 +121,7 @@ export const postsRouter = router({
|
||||||
title: z.string().min(1).max(200).optional(),
|
title: z.string().min(1).max(200).optional(),
|
||||||
content: z.string().min(1).optional(),
|
content: z.string().min(1).optional(),
|
||||||
published: z.boolean().optional(),
|
published: z.boolean().optional(),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.mutation(({ input }) => {
|
.mutation(({ input }) => {
|
||||||
const post = posts.find((p) => p.id === input.id);
|
const post = posts.find((p) => p.id === input.id);
|
||||||
|
|
|
||||||
66
apps/server/routers/submitUserData.ts
Normal file
66
apps/server/routers/submitUserData.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
import { router, publicProcedure } from "../trpc";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit User Data Router
|
||||||
|
*
|
||||||
|
* tRPC router for submitting user data (name, email) with file attachment references.
|
||||||
|
* Files should first be uploaded via /api/upload, then referenced here.
|
||||||
|
*
|
||||||
|
* In a real app, you'd use qb.uploadAttachments() to store files in TaylorDB
|
||||||
|
* and insert the record with the attachment column. This demo uses in-memory storage.
|
||||||
|
*
|
||||||
|
* TaylorDB attachment pattern (see docs/TAYLORDB_ATTACHMENTS.md):
|
||||||
|
* await qb.insertInto("users").values({
|
||||||
|
* name: "Jane",
|
||||||
|
* avatar: await qb.uploadAttachments([
|
||||||
|
* { file: new Blob([buffer]), name: "photo.png" },
|
||||||
|
* ]),
|
||||||
|
* }).execute();
|
||||||
|
*/
|
||||||
|
|
||||||
|
// In-memory store for demonstration
|
||||||
|
interface Submission {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
files: { originalName: string; mimeType: string; size: number }[];
|
||||||
|
submittedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const submissions: Submission[] = [];
|
||||||
|
let nextId = 1;
|
||||||
|
|
||||||
|
export const submitUserDataRouter = router({
|
||||||
|
/** Submit user data with file metadata */
|
||||||
|
submit: publicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
name: z.string().min(1, "Name is required"),
|
||||||
|
email: z.string().email("Valid email is required"),
|
||||||
|
files: z.array(
|
||||||
|
z.object({
|
||||||
|
originalName: z.string(),
|
||||||
|
mimeType: z.string(),
|
||||||
|
size: z.number(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(({ input }) => {
|
||||||
|
const submission: Submission = {
|
||||||
|
id: nextId++,
|
||||||
|
name: input.name,
|
||||||
|
email: input.email,
|
||||||
|
files: input.files,
|
||||||
|
submittedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
submissions.push(submission);
|
||||||
|
return submission;
|
||||||
|
}),
|
||||||
|
|
||||||
|
/** Get all submissions */
|
||||||
|
getAll: publicProcedure.query(() => {
|
||||||
|
return submissions;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
@ -1,319 +0,0 @@
|
||||||
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,15 +1,30 @@
|
||||||
|
import { createQueryBuilder } from "@taylordb/query-builder";
|
||||||
import { initTRPC } from "@trpc/server";
|
import { initTRPC } from "@trpc/server";
|
||||||
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
||||||
|
import type { TaylorDatabase } from "./taylordb/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create context for each tRPC request
|
* Create context for each tRPC request
|
||||||
* This is where you can add user session, database clients, etc.
|
* This is where you can add user session, database clients, etc.
|
||||||
*/
|
*/
|
||||||
export const createContext = ({ req, res }: CreateExpressContextOptions) => {
|
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 {
|
return {
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
// Add any shared context here (e.g., database client, user session)
|
queryBuilder,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,651 +1,55 @@
|
||||||
# shadcn/ui Dashboard Components Guide
|
# shadcn/ui Dashboard Components Guide
|
||||||
|
|
||||||
This guide provides examples of using shadcn/ui components to build modern dashboard interfaces for your TaylorDB applications.
|
This is the **entry point** for shadcn/ui documentation in this template. The guide is split into smaller files so agents (and humans) can jump to the topic they need.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📦 Installation
|
## 📚 Topics
|
||||||
|
|
||||||
### Install Individual Components
|
- **Installation**
|
||||||
|
See `SHADCN_INSTALLATION.md` for:
|
||||||
|
- Installing individual components (core, dashboard, data display, advanced)
|
||||||
|
- Command examples for `pnpm dlx shadcn@latest add`
|
||||||
|
|
||||||
```bash
|
- **Dashboard Patterns**
|
||||||
# Core components (already included)
|
See `SHADCN_DASHBOARD_PATTERNS.md` for:
|
||||||
pnpm dlx shadcn@latest add button card input label textarea select tabs alert
|
- Stats cards layout
|
||||||
|
- Data table with dropdown actions
|
||||||
|
- Create/edit form in a dialog
|
||||||
|
- Loading states with Skeleton
|
||||||
|
- Tabs for different views
|
||||||
|
- Status badges
|
||||||
|
- Toast notifications (with tRPC mutations)
|
||||||
|
- Form with validation (react-hook-form + zod)
|
||||||
|
- Side sheet for details
|
||||||
|
- Command palette (search, Cmd/Ctrl+K)
|
||||||
|
|
||||||
# Dashboard-specific components
|
- **Design & Layout**
|
||||||
pnpm dlx shadcn@latest add table dialog dropdown-menu toast sheet form badge avatar skeleton
|
See `SHADCN_DESIGN_AND_LAYOUT.md` for:
|
||||||
|
- Color schemes (semantic tokens)
|
||||||
# Data display components
|
- Spacing conventions
|
||||||
pnpm dlx shadcn@latest add separator progress scroll-area tooltip
|
- Icons (lucide-react)
|
||||||
|
- Responsive grids and hide-on-mobile
|
||||||
# Advanced components
|
- Performance tips (lazy dialogs, virtualization, skeletons, optimistic updates, debounce)
|
||||||
pnpm dlx shadcn@latest add command popover calendar date-picker
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎨 Common Dashboard Patterns
|
## How Agents Should Use These Docs
|
||||||
|
|
||||||
### 1. Dashboard Layout with Stats Cards
|
1. **Need to add a component?**
|
||||||
|
Use `SHADCN_INSTALLATION.md` for the exact `pnpm dlx shadcn@latest add` commands.
|
||||||
|
|
||||||
```typescript
|
2. **Building a dashboard or CRUD UI?**
|
||||||
import {
|
Use `SHADCN_DASHBOARD_PATTERNS.md` for copy-paste patterns (tables, dialogs, forms, toasts, sheets, etc.).
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
|
|
||||||
export default function DashboardPage() {
|
3. **Styling and layout?**
|
||||||
return (
|
Use `SHADCN_DESIGN_AND_LAYOUT.md` for tokens, spacing, responsive patterns, and performance.
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎨 Design Tips
|
## Resources
|
||||||
|
|
||||||
### 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/
|
- **shadcn/ui**: https://ui.shadcn.com/
|
||||||
- **Tailwind CSS**: https://tailwindcss.com/
|
- **Tailwind CSS**: https://tailwindcss.com/
|
||||||
- **Lucide Icons**: https://lucide.dev/
|
- **Lucide Icons**: https://lucide.dev/
|
||||||
- **React Hook Form**: https://react-hook-form.com/
|
- **React Hook Form**: https://react-hook-form.com/
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Remember**: Always test your components in both light and dark mode, and on different screen sizes!
|
|
||||||
|
|
|
||||||
566
docs/SHADCN_DASHBOARD_PATTERNS.md
Normal file
566
docs/SHADCN_DASHBOARD_PATTERNS.md
Normal file
|
|
@ -0,0 +1,566 @@
|
||||||
|
# shadcn/ui — Dashboard Patterns
|
||||||
|
|
||||||
|
Examples of using shadcn/ui components to build dashboard interfaces for TaylorDB applications.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Dashboard Layout with Stats Cards
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
|
||||||
|
export default function DashboardPage() {
|
||||||
|
return (
|
||||||
|
<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`
|
||||||
91
docs/SHADCN_DESIGN_AND_LAYOUT.md
Normal file
91
docs/SHADCN_DESIGN_AND_LAYOUT.md
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
# shadcn/ui — Design & Layout
|
||||||
|
|
||||||
|
Design tokens, spacing, icons, responsive layouts, and performance tips for shadcn/ui dashboards.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Tips
|
||||||
|
|
||||||
|
### Color Schemes
|
||||||
|
|
||||||
|
Use semantic color tokens:
|
||||||
|
|
||||||
|
- `bg-background` / `text-foreground` — Main background and text
|
||||||
|
- `bg-card` / `text-card-foreground` — Card surfaces
|
||||||
|
- `bg-primary` / `text-primary-foreground` — Primary actions
|
||||||
|
- `bg-destructive` — Destructive actions (delete, etc.)
|
||||||
|
- `bg-muted` / `text-muted-foreground` — Subtle UI elements
|
||||||
|
|
||||||
|
### Spacing
|
||||||
|
|
||||||
|
Use consistent spacing:
|
||||||
|
|
||||||
|
- `space-y-4` / `gap-4` — Between related items
|
||||||
|
- `space-y-6` / `gap-6` — Between sections
|
||||||
|
- `p-4` / `p-6` — Card padding
|
||||||
|
- `p-8` — Page padding
|
||||||
|
|
||||||
|
### Icons
|
||||||
|
|
||||||
|
Use `lucide-react` for consistent icons:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Home, Users, Settings, Plus, Edit, Trash, Search } from "lucide-react";
|
||||||
|
|
||||||
|
<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`
|
||||||
29
docs/SHADCN_INSTALLATION.md
Normal file
29
docs/SHADCN_INSTALLATION.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# shadcn/ui — Installation
|
||||||
|
|
||||||
|
How to add shadcn/ui components to your TaylorDB app.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Install Individual Components
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Core components (often already included)
|
||||||
|
pnpm dlx shadcn@latest add button card input label textarea select tabs alert
|
||||||
|
|
||||||
|
# Dashboard-specific components
|
||||||
|
pnpm dlx shadcn@latest add table dialog dropdown-menu toast sheet form badge avatar skeleton
|
||||||
|
|
||||||
|
# Data display components
|
||||||
|
pnpm dlx shadcn@latest add separator progress scroll-area tooltip
|
||||||
|
|
||||||
|
# Advanced components
|
||||||
|
pnpm dlx shadcn@latest add command popover calendar date-picker
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For patterns and examples, see:
|
||||||
|
|
||||||
|
- **Dashboard patterns**: `SHADCN_DASHBOARD_PATTERNS.md`
|
||||||
|
- **Design & layout**: `SHADCN_DESIGN_AND_LAYOUT.md`
|
||||||
|
- **Main guide index**: `SHADCN_COMPONENTS_GUIDE.md`
|
||||||
117
docs/TAYLORDB_ADVANCED_PATTERNS.md
Normal file
117
docs/TAYLORDB_ADVANCED_PATTERNS.md
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
# TaylorDB Advanced Patterns
|
||||||
|
|
||||||
|
This document covers **advanced query patterns** using the TaylorDB query builder:
|
||||||
|
|
||||||
|
- Manual aggregations
|
||||||
|
- Summation helpers
|
||||||
|
- Conditional queries
|
||||||
|
- Pagination
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Aggregations (Manual)
|
||||||
|
|
||||||
|
Since TaylorDB query builder might not have built-in aggregations, compute manually:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function getUserStats() {
|
||||||
|
const users = await queryBuilder
|
||||||
|
.selectFrom("users")
|
||||||
|
.select(["age"])
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
return { count: 0, average: null, min: null, max: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ages = users
|
||||||
|
.map((u) => u.age)
|
||||||
|
.filter((a): a is number => a !== undefined);
|
||||||
|
|
||||||
|
return {
|
||||||
|
count: ages.length,
|
||||||
|
average: ages.reduce((a, b) => a + b, 0) / ages.length,
|
||||||
|
min: Math.min(...ages),
|
||||||
|
max: Math.max(...ages),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sum Totals
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function getTotalCaloriesForDate(date: string) {
|
||||||
|
const entries = await queryBuilder
|
||||||
|
.selectFrom("meals")
|
||||||
|
.select(["calories", "protein", "carbs", "fats"])
|
||||||
|
.where("date", "=", ["exactDay", date])
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalCalories: entries.reduce((sum, e) => sum + (e.calories ?? 0), 0),
|
||||||
|
totalProtein: entries.reduce((sum, e) => sum + (e.protein ?? 0), 0),
|
||||||
|
totalCarbs: entries.reduce((sum, e) => sum + (e.carbs ?? 0), 0),
|
||||||
|
totalFats: entries.reduce((sum, e) => sum + (e.fats ?? 0), 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conditional Queries
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function searchTasks(filters: {
|
||||||
|
projectId?: number;
|
||||||
|
status?: string;
|
||||||
|
dueAfter?: string;
|
||||||
|
}) {
|
||||||
|
let query = queryBuilder
|
||||||
|
.selectFrom("tasks")
|
||||||
|
.select(["id", "title", "status", "dueDate"]);
|
||||||
|
|
||||||
|
if (filters.projectId) {
|
||||||
|
query = query.where("projectId", "=", filters.projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.status) {
|
||||||
|
query = query.where("status", "=", filters.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.dueAfter) {
|
||||||
|
query = query.where("dueDate", ">=", ["exactDay", filters.dueAfter]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query.execute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pagination
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function getPaginatedUsers(page: number, pageSize: number) {
|
||||||
|
const offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
|
return await queryBuilder
|
||||||
|
.selectFrom("users")
|
||||||
|
.select(["id", "name", "email"])
|
||||||
|
.orderBy("createdAt", "desc")
|
||||||
|
.limit(pageSize)
|
||||||
|
.offset(offset)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For more topics, see:
|
||||||
|
|
||||||
|
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
|
||||||
|
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
|
||||||
|
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums
|
||||||
|
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices
|
||||||
|
|
||||||
68
docs/TAYLORDB_ATTACHMENTS.md
Normal file
68
docs/TAYLORDB_ATTACHMENTS.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# TaylorDB Attachments
|
||||||
|
|
||||||
|
Attachments are treated as **standard columns** and can be selected and written like other fields, using helper utilities for uploads.
|
||||||
|
|
||||||
|
This document covers:
|
||||||
|
|
||||||
|
- Selecting attachment fields
|
||||||
|
- Creating records with attachments
|
||||||
|
- Updating attachments
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Select Attachments
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// New Standard: Use regular .select() like any other field.
|
||||||
|
const expenses = await qb
|
||||||
|
.selectFrom("expenses")
|
||||||
|
.select(["id", "amount", "receipt"])
|
||||||
|
.execute();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Create with Attachments
|
||||||
|
|
||||||
|
Use `qb.uploadAttachments` to upload files before inserting.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await qb
|
||||||
|
.insertInto("customers")
|
||||||
|
.values({
|
||||||
|
firstName: "Jane",
|
||||||
|
lastName: "Doe",
|
||||||
|
avatar: await qb.uploadAttachments([
|
||||||
|
{ file: new Blob([""]), name: "test.png" },
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Update with Attachments
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await qb
|
||||||
|
.update("customers")
|
||||||
|
.set({
|
||||||
|
lastName: "Smith",
|
||||||
|
avatar: await qb.uploadAttachments([
|
||||||
|
{ file: new Blob([""]), name: "test.png" },
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
.where("id", "=", 1)
|
||||||
|
.execute();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For more topics, see:
|
||||||
|
|
||||||
|
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
|
||||||
|
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
|
||||||
|
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
|
||||||
|
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums
|
||||||
|
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices
|
||||||
|
|
||||||
187
docs/TAYLORDB_BASIC_QUERIES.md
Normal file
187
docs/TAYLORDB_BASIC_QUERIES.md
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
# TaylorDB Basic Queries & Filtering
|
||||||
|
|
||||||
|
This document covers **core read operations** using the TaylorDB query builder:
|
||||||
|
|
||||||
|
- **Basic select queries**
|
||||||
|
- **Ordering**
|
||||||
|
- **Filtering with where clauses**
|
||||||
|
- **Date filters**
|
||||||
|
- **Array and select field filters**
|
||||||
|
- **Simple text search**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Basic Queries
|
||||||
|
|
||||||
|
### Get All Records
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function getAllUsers() {
|
||||||
|
return await queryBuilder
|
||||||
|
.selectFrom("users")
|
||||||
|
.select(["id", "name", "email", "createdAt"])
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get All Records (All Fields)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function getAllUsers() {
|
||||||
|
return await queryBuilder.selectFrom("users").execute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Single Record by ID
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function getUserById(id: number) {
|
||||||
|
return await queryBuilder
|
||||||
|
.selectFrom("users")
|
||||||
|
.where("id", "=", id)
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: `.executeTakeFirst()` returns a single record or `undefined`.
|
||||||
|
|
||||||
|
### Get Records with Ordering
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Descending order (newest first)
|
||||||
|
export async function getRecentUsers() {
|
||||||
|
return await queryBuilder
|
||||||
|
.selectFrom("users")
|
||||||
|
.orderBy("createdAt", "desc")
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ascending order (oldest first)
|
||||||
|
export async function getOldestUsers() {
|
||||||
|
return await queryBuilder
|
||||||
|
.selectFrom("users")
|
||||||
|
.orderBy("createdAt", "asc")
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Filtering & Conditions
|
||||||
|
|
||||||
|
### Basic Where Clauses
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Exact match
|
||||||
|
.where("status", "=", "active")
|
||||||
|
|
||||||
|
// Not equal
|
||||||
|
.where("status", "!=", "deleted")
|
||||||
|
|
||||||
|
// Greater than / Less than
|
||||||
|
.where("age", ">", 18)
|
||||||
|
.where("age", ">=", 18)
|
||||||
|
.where("age", "<", 65)
|
||||||
|
.where("age", "<=", 65)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Conditions (AND logic)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function getActiveAdults() {
|
||||||
|
return await queryBuilder
|
||||||
|
.selectFrom("users")
|
||||||
|
.where("status", "=", "active")
|
||||||
|
.where("age", ">=", 18)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Date Filtering
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Exact date
|
||||||
|
export async function getUsersForDate(date: string) {
|
||||||
|
return await queryBuilder
|
||||||
|
.selectFrom("users")
|
||||||
|
.where("createdAt", "=", ["exactDay", date])
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date range
|
||||||
|
export async function getUsersInRange(startDate: string, endDate: string) {
|
||||||
|
return await queryBuilder
|
||||||
|
.selectFrom("users")
|
||||||
|
.where("createdAt", ">=", ["exactDay", startDate])
|
||||||
|
.where("createdAt", "<=", ["exactDay", endDate])
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before/After a date
|
||||||
|
.where("dueDate", "<", ["exactDay", "2024-01-01"])
|
||||||
|
.where("startDate", ">", ["exactDay", "2024-12-31"])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Array/Multi-Select Filtering
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Check if array contains any of the values
|
||||||
|
export async function getUsersByTags(tags: string[]) {
|
||||||
|
return await queryBuilder
|
||||||
|
.selectFrom("users")
|
||||||
|
.where("tags", "hasAnyOf", tags)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Get users tagged with "admin" OR "moderator"
|
||||||
|
const adminUsers = await getUsersByTags(["admin", "moderator"]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Select Field Filtering
|
||||||
|
|
||||||
|
#### Single Select
|
||||||
|
|
||||||
|
For single-select fields, the query builder now returns a single string value.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function getUsersByRole(role: string) {
|
||||||
|
return await queryBuilder
|
||||||
|
.selectFrom("users")
|
||||||
|
.where("role", "=", role)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Multi Select
|
||||||
|
|
||||||
|
For multi-select fields, the query builder returns and accepts multiple values.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function getUsersByInterests(interests: string[]) {
|
||||||
|
return await queryBuilder
|
||||||
|
.selectFrom("users")
|
||||||
|
.where("interests", "hasAnyOf", interests)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Text Search (Contains)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function searchUsersByName(query: string) {
|
||||||
|
return await queryBuilder
|
||||||
|
.selectFrom("users")
|
||||||
|
.where("name", "contains", query)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For more topics, see:
|
||||||
|
|
||||||
|
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
|
||||||
|
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
|
||||||
|
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums
|
||||||
|
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices
|
||||||
|
|
||||||
84
docs/TAYLORDB_FIELD_TYPES.md
Normal file
84
docs/TAYLORDB_FIELD_TYPES.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# TaylorDB Field Types & Enums
|
||||||
|
|
||||||
|
This document explains how TaylorDB field types map to TypeScript and how to handle them correctly:
|
||||||
|
|
||||||
|
- Field type reference
|
||||||
|
- Nullable fields
|
||||||
|
- Enums and options from generated types
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Field Type Reference
|
||||||
|
|
||||||
|
| TaylorDB Field Type | TypeScript Type | Insert Value | Query Value |
|
||||||
|
| ------------------- | ----------------------- | --------------------- | ---------------------------- |
|
||||||
|
| **Text** | `string` | `"Hello"` | `"Hello"` |
|
||||||
|
| **Number** | `number` | `42` | `42` |
|
||||||
|
| **Date** | `string` (ISO) | `"2024-01-15"` | `["exactDay", "2024-01-15"]` |
|
||||||
|
| **Checkbox** | `boolean` | `true` | `true` |
|
||||||
|
| **Single Select** | `string` | `"option"` | `"option"` |
|
||||||
|
| **Multi Select** | `string[]` | `["opt1", "opt2"]` | `["opt1", "opt2"]` |
|
||||||
|
| **Attachment** | `string[]` (File Paths) | `uploadAttachments()` | `"file-path"` |
|
||||||
|
| **Email** | `string` | `"user@example.com"` | `"user@example.com"` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Handling Nullable Fields
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function createUserSafe(data: {
|
||||||
|
name: string;
|
||||||
|
email?: string | null;
|
||||||
|
age?: number | null;
|
||||||
|
}) {
|
||||||
|
return await queryBuilder
|
||||||
|
.insertInto("users")
|
||||||
|
.values({
|
||||||
|
name: data.name,
|
||||||
|
email: data.email ?? "", // Default to empty string
|
||||||
|
age: data.age ?? 0, // Default to 0
|
||||||
|
})
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Working with Enums
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Import from generated types
|
||||||
|
import type { TaskStatusOptions } from "./types";
|
||||||
|
|
||||||
|
export async function createTask(data: {
|
||||||
|
title: string;
|
||||||
|
status: (typeof TaskStatusOptions)[number]; // e.g., "todo" | "in-progress" | "done"
|
||||||
|
}) {
|
||||||
|
return await queryBuilder
|
||||||
|
.insertInto("tasks")
|
||||||
|
.values({
|
||||||
|
title: data.title,
|
||||||
|
status: data.status,
|
||||||
|
})
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTasksByStatus(
|
||||||
|
status: (typeof TaskStatusOptions)[number],
|
||||||
|
) {
|
||||||
|
return await queryBuilder
|
||||||
|
.selectFrom("tasks")
|
||||||
|
.where("status", "=", status)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For more topics, see:
|
||||||
|
|
||||||
|
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
|
||||||
|
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
|
||||||
|
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
|
||||||
|
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices
|
||||||
|
|
||||||
98
docs/TAYLORDB_PITFALLS_BEST_PRACTICES.md
Normal file
98
docs/TAYLORDB_PITFALLS_BEST_PRACTICES.md
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
# TaylorDB Pitfalls & Best Practices
|
||||||
|
|
||||||
|
This document captures **common mistakes** and **recommended patterns** when using the TaylorDB query builder.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### ❌ Pitfall: Not Using exactDay for Dates
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ WRONG
|
||||||
|
.where("date", "=", "2024-01-15")
|
||||||
|
|
||||||
|
// ✅ CORRECT
|
||||||
|
.where("date", "=", ["exactDay", "2024-01-15"])
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Pitfall: Ignoring Nullable Fields
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ WRONG (assumes field is always present)
|
||||||
|
const user = await queryBuilder
|
||||||
|
.selectFrom("users")
|
||||||
|
.where("id", "=", 1)
|
||||||
|
.executeTakeFirst();
|
||||||
|
console.log(user.email); // Could be undefined!
|
||||||
|
|
||||||
|
// ✅ CORRECT
|
||||||
|
const user = await queryBuilder
|
||||||
|
.selectFrom("users")
|
||||||
|
.where("id", "=", 1)
|
||||||
|
.executeTakeFirst();
|
||||||
|
if (user && user.email) {
|
||||||
|
console.log(user.email);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Pitfall: Using execute() for Single Record
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ WRONG (returns array)
|
||||||
|
const user = await queryBuilder
|
||||||
|
.selectFrom("users")
|
||||||
|
.where("id", "=", 1)
|
||||||
|
.execute();
|
||||||
|
console.log(user.name); // Error: user is an array!
|
||||||
|
|
||||||
|
// ✅ CORRECT
|
||||||
|
const user = await queryBuilder
|
||||||
|
.selectFrom("users")
|
||||||
|
.where("id", "=", 1)
|
||||||
|
.executeTakeFirst();
|
||||||
|
if (user) {
|
||||||
|
console.log(user.name);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Pitfall: Not Handling Empty Arrays
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ WRONG (fails if users is empty)
|
||||||
|
const ages = users.map((u) => u.age);
|
||||||
|
const avg = ages.reduce((a, b) => a + b) / ages.length; // Division by zero!
|
||||||
|
|
||||||
|
// ✅ CORRECT
|
||||||
|
if (users.length === 0) {
|
||||||
|
return { average: null };
|
||||||
|
}
|
||||||
|
const ages = users
|
||||||
|
.map((u) => u.age)
|
||||||
|
.filter((a): a is number => a !== undefined);
|
||||||
|
const avg = ages.reduce((a, b) => a + b, 0) / ages.length;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Always handle `undefined` and `null`** when working with query results.
|
||||||
|
2. **Use TypeScript types** from `taylordb/types.ts` for type safety.
|
||||||
|
3. **Use `executeTakeFirst()`** when you expect a single record.
|
||||||
|
4. **Filter nullish values** before aggregations.
|
||||||
|
5. **Provide defaults** for optional fields.
|
||||||
|
6. **Use `["exactDay", date]`** format for date comparisons.
|
||||||
|
7. **Group related queries** in the same function file.
|
||||||
|
8. **Export functions**, not raw queries.
|
||||||
|
9. **Document complex queries** with JSDoc comments.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For more topics, see:
|
||||||
|
|
||||||
|
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
|
||||||
|
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
|
||||||
|
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
|
||||||
|
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums
|
||||||
|
|
||||||
|
|
@ -1,700 +1,78 @@
|
||||||
# TaylorDB Query Builder Reference
|
# TaylorDB Query Builder Reference
|
||||||
|
|
||||||
This document provides comprehensive examples of how to use the TaylorDB query builder for all common database operations.
|
This is the **entry point** for all TaylorDB query builder docs in this template.
|
||||||
|
The content has been split into smaller, focused files to make it easier for agents (and humans) to scan and reuse.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📚 Table of Contents
|
## 📚 Topics
|
||||||
|
|
||||||
1. [Setup & Configuration](#setup--configuration)
|
- **Basic Reads & Filtering**
|
||||||
2. [Basic Queries](#basic-queries)
|
See `TAYLORDB_BASIC_QUERIES.md` for:
|
||||||
3. [Filtering & Conditions](#filtering--conditions)
|
- Basic `selectFrom` usage
|
||||||
4. [Inserting Data](#inserting-data)
|
- Ordering
|
||||||
5. [Updating Data](#updating-data)
|
- `where` clauses
|
||||||
6. [Deleting Data](#deleting-data)
|
- Date filters
|
||||||
7. [Advanced Patterns](#advanced-patterns)
|
- Array/select field filters
|
||||||
8. [Field Type Handling](#field-type-handling)
|
- Text search (`contains`)
|
||||||
9. [Common Pitfalls](#common-pitfalls)
|
|
||||||
|
- **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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Setup & Configuration
|
## How Agents Should Use These Docs
|
||||||
|
|
||||||
### Initialize Query Builder
|
1. **Start from your use case**
|
||||||
|
- Need a simple read? Open `TAYLORDB_BASIC_QUERIES.md`.
|
||||||
|
- Doing writes? Use `TAYLORDB_WRITE_OPERATIONS.md`.
|
||||||
|
- Need aggregations or pagination? Use `TAYLORDB_ADVANCED_PATTERNS.md`.
|
||||||
|
|
||||||
```typescript
|
2. **Combine with generated types**
|
||||||
import { createQueryBuilder } from "@taylordb/query-builder";
|
Always cross-reference:
|
||||||
import type { TaylorDatabase } from "./types.js";
|
- `apps/server/taylordb/types.ts` (schema-derived types)
|
||||||
|
- `apps/server/taylordb/query-builder.ts` (project-specific query functions)
|
||||||
|
|
||||||
export const queryBuilder = createQueryBuilder<TaylorDatabase>({
|
3. **Check pitfalls before finalizing**
|
||||||
baseUrl: process.env.TAYLORDB_BASE_URL!,
|
Before shipping queries, skim `TAYLORDB_PITFALLS_BEST_PRACTICES.md` to avoid common errors.
|
||||||
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
|
## Additional Resources
|
||||||
|
|
||||||
- **Generated Types**: Check `apps/server/taylordb/types.ts` for your schema
|
- **Generated Types**: `apps/server/taylordb/types.ts`
|
||||||
- **Example Queries**: See `apps/server/taylordb/query-builder.ts`
|
- **Example Queries in This Template**: `apps/server/taylordb/query-builder.ts`
|
||||||
- **tRPC Integration**: See `apps/server/router.ts`
|
- **tRPC Integration**: `apps/server/router.ts`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Note**: This reference is based on the TaylorDB query builder patterns used in this template. Always refer to the official TaylorDB documentation for the most up-to-date API details.
|
**Note**: These docs mirror the TaylorDB query builder patterns used in this template.
|
||||||
|
For the most up-to-date API details, always refer to the official TaylorDB documentation.
|
||||||
|
|
||||||
|
|
|
||||||
265
docs/TAYLORDB_WRITE_OPERATIONS.md
Normal file
265
docs/TAYLORDB_WRITE_OPERATIONS.md
Normal file
|
|
@ -0,0 +1,265 @@
|
||||||
|
# TaylorDB Write Operations (Insert, Update, Delete)
|
||||||
|
|
||||||
|
This document covers **write operations** using the TaylorDB query builder:
|
||||||
|
|
||||||
|
- **Inserting data**
|
||||||
|
- **Updating single/multiple records**
|
||||||
|
- **Deleting records**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Inserting Data
|
||||||
|
|
||||||
|
### Insert Single Record
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function createUser(data: {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
age: number;
|
||||||
|
}) {
|
||||||
|
return await queryBuilder
|
||||||
|
.insertInto("users")
|
||||||
|
.values({
|
||||||
|
name: data.name,
|
||||||
|
email: data.email,
|
||||||
|
age: data.age,
|
||||||
|
status: "active", // Default value
|
||||||
|
})
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns**: The created record with its generated `id`.
|
||||||
|
|
||||||
|
### Insert with Single-Select Field
|
||||||
|
|
||||||
|
Single-select fields now accept a single string value directly.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function createTask(data: {
|
||||||
|
title: string;
|
||||||
|
priority: "low" | "medium" | "high";
|
||||||
|
}) {
|
||||||
|
return await queryBuilder
|
||||||
|
.insertInto("tasks")
|
||||||
|
.values({
|
||||||
|
title: data.title,
|
||||||
|
priority: data.priority,
|
||||||
|
})
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Insert with Multi-Select Field
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function createProject(data: { name: string; tags: string[] }) {
|
||||||
|
return await queryBuilder
|
||||||
|
.insertInto("projects")
|
||||||
|
.values({
|
||||||
|
name: data.name,
|
||||||
|
tags: data.tags, // Already an array
|
||||||
|
})
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Insert with Computed Fields
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function createCardioSession(data: {
|
||||||
|
distance: number;
|
||||||
|
duration: number; // in minutes
|
||||||
|
}) {
|
||||||
|
const speed = data.distance / (data.duration / 60); // km/h
|
||||||
|
|
||||||
|
return await queryBuilder
|
||||||
|
.insertInto("cardio")
|
||||||
|
.values({
|
||||||
|
distance: data.distance,
|
||||||
|
duration: data.duration,
|
||||||
|
speed: speed, // Computed field
|
||||||
|
})
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Insert with Optional Fields
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function createPost(data: {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
tags?: string[];
|
||||||
|
}) {
|
||||||
|
return await queryBuilder
|
||||||
|
.insertInto("posts")
|
||||||
|
.values({
|
||||||
|
title: data.title,
|
||||||
|
content: data.content,
|
||||||
|
tags: data.tags || [], // Default to empty array
|
||||||
|
})
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Updating Data
|
||||||
|
|
||||||
|
### Update Single Field
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function updateUserName(id: number, name: string) {
|
||||||
|
return await queryBuilder
|
||||||
|
.update("users")
|
||||||
|
.set({ name })
|
||||||
|
.where("id", "=", id)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Multiple Fields
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function updateUser(
|
||||||
|
id: number,
|
||||||
|
data: {
|
||||||
|
name?: string;
|
||||||
|
email?: string;
|
||||||
|
age?: number;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
return await queryBuilder
|
||||||
|
.update("users")
|
||||||
|
.set(data)
|
||||||
|
.where("id", "=", id)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Only provided fields will be updated.
|
||||||
|
|
||||||
|
### Update with Single-Select Field
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function updateTaskPriority(
|
||||||
|
id: number,
|
||||||
|
priority: "low" | "medium" | "high",
|
||||||
|
) {
|
||||||
|
return await queryBuilder
|
||||||
|
.update("tasks")
|
||||||
|
.set({ priority })
|
||||||
|
.where("id", "=", id)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update with Conditional Logic
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function updateCardioSession(
|
||||||
|
id: number,
|
||||||
|
data: {
|
||||||
|
distance?: number;
|
||||||
|
duration?: number;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
// Fetch current record to compute speed
|
||||||
|
const currentRecord = await queryBuilder
|
||||||
|
.selectFrom("cardio")
|
||||||
|
.select(["distance", "duration"])
|
||||||
|
.where("id", "=", id)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
if (!currentRecord) {
|
||||||
|
throw new Error("Record not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDistance = data.distance ?? currentRecord.distance ?? 0;
|
||||||
|
const newDuration = data.duration ?? currentRecord.duration ?? 0;
|
||||||
|
const speed = newDistance / (newDuration / 60);
|
||||||
|
|
||||||
|
return await queryBuilder
|
||||||
|
.update("cardio")
|
||||||
|
.set({
|
||||||
|
...data,
|
||||||
|
speed,
|
||||||
|
})
|
||||||
|
.where("id", "=", id)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Multiple Records
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function activateAllUsers() {
|
||||||
|
return await queryBuilder.update("users").set({ status: "active" }).execute(); // No where clause = update all
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update with condition
|
||||||
|
export async function activateInactiveUsers() {
|
||||||
|
return await queryBuilder
|
||||||
|
.update("users")
|
||||||
|
.set({ status: "active" })
|
||||||
|
.where("status", "=", "inactive")
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deleting Data
|
||||||
|
|
||||||
|
### Delete Single Record
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function deleteUser(id: number) {
|
||||||
|
return await queryBuilder.deleteFrom("users").where("id", "=", id).execute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Multiple Records by IDs
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function deleteUsers(ids: number[]) {
|
||||||
|
return await queryBuilder
|
||||||
|
.deleteFrom("users")
|
||||||
|
.where("id", "hasAnyOf", ids)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete with Condition
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function deleteInactiveUsers() {
|
||||||
|
return await queryBuilder
|
||||||
|
.deleteFrom("users")
|
||||||
|
.where("status", "=", "inactive")
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Old Records
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function deleteOldLogs(beforeDate: string) {
|
||||||
|
return await queryBuilder
|
||||||
|
.deleteFrom("logs")
|
||||||
|
.where("createdAt", "<", ["exactDay", beforeDate])
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For more topics, see:
|
||||||
|
|
||||||
|
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
|
||||||
|
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
|
||||||
|
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums
|
||||||
|
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices
|
||||||
|
|
||||||
199
pnpm-lock.yaml
199
pnpm-lock.yaml
|
|
@ -139,27 +139,39 @@ importers:
|
||||||
apps/server:
|
apps/server:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@taylordb/query-builder':
|
'@taylordb/query-builder':
|
||||||
specifier: ^0.10.3
|
specifier: ^0.11.4
|
||||||
version: 0.10.3
|
version: 0.11.4
|
||||||
'@trpc/server':
|
'@trpc/server':
|
||||||
specifier: ^11.8.1
|
specifier: ^11.8.1
|
||||||
version: 11.8.1(typescript@5.9.3)
|
version: 11.8.1(typescript@5.9.3)
|
||||||
|
cookie-parser:
|
||||||
|
specifier: ^1.4.7
|
||||||
|
version: 1.4.7
|
||||||
cors:
|
cors:
|
||||||
specifier: ^2.8.5
|
specifier: ^2.8.5
|
||||||
version: 2.8.5
|
version: 2.8.5
|
||||||
express:
|
express:
|
||||||
specifier: ^5.2.1
|
specifier: ^5.2.1
|
||||||
version: 5.2.1
|
version: 5.2.1
|
||||||
|
multer:
|
||||||
|
specifier: ^2.0.2
|
||||||
|
version: 2.0.2
|
||||||
zod:
|
zod:
|
||||||
specifier: ^4.3.5
|
specifier: ^4.3.5
|
||||||
version: 4.3.5
|
version: 4.3.5
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/cookie-parser':
|
||||||
|
specifier: ^1.4.10
|
||||||
|
version: 1.4.10(@types/express@5.0.6)
|
||||||
'@types/cors':
|
'@types/cors':
|
||||||
specifier: ^2.8.19
|
specifier: ^2.8.19
|
||||||
version: 2.8.19
|
version: 2.8.19
|
||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: ^5.0.6
|
specifier: ^5.0.6
|
||||||
version: 5.0.6
|
version: 5.0.6
|
||||||
|
'@types/multer':
|
||||||
|
specifier: ^2.0.0
|
||||||
|
version: 2.0.0
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^24.10.1
|
specifier: ^24.10.1
|
||||||
version: 24.10.1
|
version: 24.10.1
|
||||||
|
|
@ -1273,9 +1285,15 @@ packages:
|
||||||
'@taylordb/query-builder@0.10.3':
|
'@taylordb/query-builder@0.10.3':
|
||||||
resolution: {integrity: sha512-GwsrSdCQelTvghPY4uzoSWHI2vsWFOFSOb4IKRS0qQT8Z6jG8VWMOSRwocvcMoQfOf2RqCDt9Wew1dK6FZNBHA==}
|
resolution: {integrity: sha512-GwsrSdCQelTvghPY4uzoSWHI2vsWFOFSOb4IKRS0qQT8Z6jG8VWMOSRwocvcMoQfOf2RqCDt9Wew1dK6FZNBHA==}
|
||||||
|
|
||||||
|
'@taylordb/query-builder@0.11.4':
|
||||||
|
resolution: {integrity: sha512-5hGFtxTKtRK76BUq08c95lqzYBcCTNRm1W04z1P8ufAVvSrr9Iuq1sPf8D7ayBJrAZ8KHThwvAR9w4n0vqiwYQ==}
|
||||||
|
|
||||||
'@taylordb/shared@0.4.4':
|
'@taylordb/shared@0.4.4':
|
||||||
resolution: {integrity: sha512-Xykr4I26JapNLePkapBGjz15t9Ep1iLs30VbfCcc2z30x8Qy/2tm+sSa5LcacCP2EaxNVDp2UuYKhZ7kOWBLBQ==}
|
resolution: {integrity: sha512-Xykr4I26JapNLePkapBGjz15t9Ep1iLs30VbfCcc2z30x8Qy/2tm+sSa5LcacCP2EaxNVDp2UuYKhZ7kOWBLBQ==}
|
||||||
|
|
||||||
|
'@taylordb/shared@0.5.4':
|
||||||
|
resolution: {integrity: sha512-85tqzVQXsbx3rUAhpcdeA/BKHaHEFgpMxjmRAb12bD4U4ghGrcFw1eyaq+/YtoBfiWChPCruCK+d03e+rEMoiA==}
|
||||||
|
|
||||||
'@trpc/client@11.8.1':
|
'@trpc/client@11.8.1':
|
||||||
resolution: {integrity: sha512-L/SJFGanr9xGABmuDoeXR4xAdHJmsXsiF9OuH+apecJ+8sUITzVT1EPeqp0ebqA6lBhEl5pPfg3rngVhi/h60Q==}
|
resolution: {integrity: sha512-L/SJFGanr9xGABmuDoeXR4xAdHJmsXsiF9OuH+apecJ+8sUITzVT1EPeqp0ebqA6lBhEl5pPfg3rngVhi/h60Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -1315,6 +1333,11 @@ packages:
|
||||||
'@types/connect@3.4.38':
|
'@types/connect@3.4.38':
|
||||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||||
|
|
||||||
|
'@types/cookie-parser@1.4.10':
|
||||||
|
resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/express': '*'
|
||||||
|
|
||||||
'@types/cors@2.8.19':
|
'@types/cors@2.8.19':
|
||||||
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
|
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
|
||||||
|
|
||||||
|
|
@ -1360,6 +1383,9 @@ packages:
|
||||||
'@types/json-schema@7.0.15':
|
'@types/json-schema@7.0.15':
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
|
'@types/multer@2.0.0':
|
||||||
|
resolution: {integrity: sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==}
|
||||||
|
|
||||||
'@types/node@24.10.1':
|
'@types/node@24.10.1':
|
||||||
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
|
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
|
||||||
|
|
||||||
|
|
@ -1476,6 +1502,9 @@ packages:
|
||||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
append-field@1.0.0:
|
||||||
|
resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==}
|
||||||
|
|
||||||
argparse@2.0.1:
|
argparse@2.0.1:
|
||||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
|
|
||||||
|
|
@ -1509,6 +1538,13 @@ packages:
|
||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
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:
|
bytes@3.1.2:
|
||||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
@ -1553,6 +1589,10 @@ packages:
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
|
|
||||||
|
concat-stream@2.0.0:
|
||||||
|
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
|
||||||
|
engines: {'0': node >= 6.0}
|
||||||
|
|
||||||
concurrently@9.2.1:
|
concurrently@9.2.1:
|
||||||
resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==}
|
resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
@ -1569,6 +1609,13 @@ packages:
|
||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
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:
|
cookie-signature@1.2.2:
|
||||||
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
||||||
engines: {node: '>=6.6.0'}
|
engines: {node: '>=6.6.0'}
|
||||||
|
|
@ -2124,6 +2171,10 @@ packages:
|
||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
engines: {node: '>= 0.4'}
|
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:
|
media-typer@1.1.0:
|
||||||
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
|
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
@ -2140,10 +2191,18 @@ packages:
|
||||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
|
|
||||||
|
mime-db@1.52.0:
|
||||||
|
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
mime-db@1.54.0:
|
mime-db@1.54.0:
|
||||||
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
|
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
|
||||||
engines: {node: '>= 0.6'}
|
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:
|
mime-types@3.0.2:
|
||||||
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
|
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
@ -2155,9 +2214,20 @@ packages:
|
||||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
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:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
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:
|
nanoid@3.3.11:
|
||||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
|
|
@ -2332,6 +2402,10 @@ packages:
|
||||||
resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
|
resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
readable-stream@3.6.2:
|
||||||
|
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
recharts@3.5.0:
|
recharts@3.5.0:
|
||||||
resolution: {integrity: sha512-jWqBtu8L3VICXWa3g/y+bKjL8DDHSRme7DHD/70LQ/Tk0di1h11Y0kKC0nPh6YJ2oaa0k6anIFNhg6SfzHWdEA==}
|
resolution: {integrity: sha512-jWqBtu8L3VICXWa3g/y+bKjL8DDHSRme7DHD/70LQ/Tk0di1h11Y0kKC0nPh6YJ2oaa0k6anIFNhg6SfzHWdEA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
@ -2381,6 +2455,9 @@ packages:
|
||||||
rxjs@7.8.2:
|
rxjs@7.8.2:
|
||||||
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
||||||
|
|
||||||
|
safe-buffer@5.2.1:
|
||||||
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
|
||||||
safer-buffer@2.1.2:
|
safer-buffer@2.1.2:
|
||||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
|
|
||||||
|
|
@ -2451,10 +2528,17 @@ packages:
|
||||||
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
streamsearch@1.1.0:
|
||||||
|
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
string-width@4.2.3:
|
string-width@4.2.3:
|
||||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
string_decoder@1.3.0:
|
||||||
|
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||||
|
|
||||||
strip-ansi@6.0.1:
|
strip-ansi@6.0.1:
|
||||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
@ -2521,10 +2605,17 @@ packages:
|
||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
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:
|
type-is@2.0.1:
|
||||||
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
typedarray@0.0.6:
|
||||||
|
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
||||||
|
|
||||||
typescript-eslint@8.47.0:
|
typescript-eslint@8.47.0:
|
||||||
resolution: {integrity: sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==}
|
resolution: {integrity: sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
@ -2578,6 +2669,9 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
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:
|
vary@1.1.2:
|
||||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
@ -2657,6 +2751,10 @@ packages:
|
||||||
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
|
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
|
xtend@4.0.2:
|
||||||
|
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||||
|
engines: {node: '>=0.4'}
|
||||||
|
|
||||||
y18n@5.0.8:
|
y18n@5.0.8:
|
||||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
@ -3586,10 +3684,28 @@ snapshots:
|
||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- 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':
|
'@taylordb/shared@0.4.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash: 4.17.21
|
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)':
|
'@trpc/client@11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@trpc/server': 11.8.1(typescript@5.9.3)
|
'@trpc/server': 11.8.1(typescript@5.9.3)
|
||||||
|
|
@ -3638,6 +3754,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.10.1
|
'@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':
|
'@types/cors@2.8.19':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.10.1
|
'@types/node': 24.10.1
|
||||||
|
|
@ -3685,6 +3805,10 @@ snapshots:
|
||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
|
'@types/multer@2.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@types/express': 5.0.6
|
||||||
|
|
||||||
'@types/node@24.10.1':
|
'@types/node@24.10.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.16.0
|
undici-types: 7.16.0
|
||||||
|
|
@ -3841,6 +3965,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-convert: 2.0.1
|
color-convert: 2.0.1
|
||||||
|
|
||||||
|
append-field@1.0.0: {}
|
||||||
|
|
||||||
argparse@2.0.1: {}
|
argparse@2.0.1: {}
|
||||||
|
|
||||||
aria-hidden@1.2.6:
|
aria-hidden@1.2.6:
|
||||||
|
|
@ -3886,6 +4012,12 @@ snapshots:
|
||||||
node-releases: 2.0.27
|
node-releases: 2.0.27
|
||||||
update-browserslist-db: 1.1.4(browserslist@4.28.0)
|
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: {}
|
bytes@3.1.2: {}
|
||||||
|
|
||||||
call-bind-apply-helpers@1.0.2:
|
call-bind-apply-helpers@1.0.2:
|
||||||
|
|
@ -3927,6 +4059,13 @@ snapshots:
|
||||||
|
|
||||||
concat-map@0.0.1: {}
|
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:
|
concurrently@9.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
|
|
@ -3942,6 +4081,13 @@ snapshots:
|
||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
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-signature@1.2.2: {}
|
||||||
|
|
||||||
cookie@0.7.2: {}
|
cookie@0.7.2: {}
|
||||||
|
|
@ -4513,6 +4659,8 @@ snapshots:
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
|
media-typer@0.3.0: {}
|
||||||
|
|
||||||
media-typer@1.1.0: {}
|
media-typer@1.1.0: {}
|
||||||
|
|
||||||
merge-descriptors@2.0.0: {}
|
merge-descriptors@2.0.0: {}
|
||||||
|
|
@ -4524,8 +4672,14 @@ snapshots:
|
||||||
braces: 3.0.3
|
braces: 3.0.3
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
|
|
||||||
|
mime-db@1.52.0: {}
|
||||||
|
|
||||||
mime-db@1.54.0: {}
|
mime-db@1.54.0: {}
|
||||||
|
|
||||||
|
mime-types@2.1.35:
|
||||||
|
dependencies:
|
||||||
|
mime-db: 1.52.0
|
||||||
|
|
||||||
mime-types@3.0.2:
|
mime-types@3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-db: 1.54.0
|
mime-db: 1.54.0
|
||||||
|
|
@ -4538,8 +4692,24 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 2.0.2
|
brace-expansion: 2.0.2
|
||||||
|
|
||||||
|
minimist@1.2.8: {}
|
||||||
|
|
||||||
|
mkdirp@0.5.6:
|
||||||
|
dependencies:
|
||||||
|
minimist: 1.2.8
|
||||||
|
|
||||||
ms@2.1.3: {}
|
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: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
@ -4684,6 +4854,12 @@ snapshots:
|
||||||
|
|
||||||
react@19.2.0: {}
|
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):
|
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:
|
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)
|
'@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)
|
||||||
|
|
@ -4768,6 +4944,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
safe-buffer@5.2.1: {}
|
||||||
|
|
||||||
safer-buffer@2.1.2: {}
|
safer-buffer@2.1.2: {}
|
||||||
|
|
||||||
scheduler@0.27.0: {}
|
scheduler@0.27.0: {}
|
||||||
|
|
@ -4861,12 +5039,18 @@ snapshots:
|
||||||
|
|
||||||
statuses@2.0.2: {}
|
statuses@2.0.2: {}
|
||||||
|
|
||||||
|
streamsearch@1.1.0: {}
|
||||||
|
|
||||||
string-width@4.2.3:
|
string-width@4.2.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
emoji-regex: 8.0.0
|
emoji-regex: 8.0.0
|
||||||
is-fullwidth-code-point: 3.0.0
|
is-fullwidth-code-point: 3.0.0
|
||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
string_decoder@1.3.0:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
strip-ansi@6.0.1:
|
strip-ansi@6.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex: 5.0.1
|
ansi-regex: 5.0.1
|
||||||
|
|
@ -4921,12 +5105,19 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
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:
|
type-is@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
media-typer: 1.1.0
|
media-typer: 1.1.0
|
||||||
mime-types: 3.0.2
|
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):
|
typescript-eslint@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
|
||||||
dependencies:
|
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)
|
'@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)
|
||||||
|
|
@ -4973,6 +5164,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
|
|
||||||
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
victory-vendor@37.3.6:
|
victory-vendor@37.3.6:
|
||||||
|
|
@ -5025,6 +5218,8 @@ snapshots:
|
||||||
|
|
||||||
xmlhttprequest-ssl@2.1.2: {}
|
xmlhttprequest-ssl@2.1.2: {}
|
||||||
|
|
||||||
|
xtend@4.0.2: {}
|
||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ services:
|
||||||
env:
|
env:
|
||||||
vars:
|
vars:
|
||||||
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
|
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
|
||||||
TAYLORDB_API_TOKEN: secrets.TAYLORDB_API_TOKEN
|
|
||||||
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
|
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
|
||||||
FRONTEND_URL: routing.client.url
|
FRONTEND_URL: routing.client.url
|
||||||
|
|
||||||
|
|
@ -31,7 +30,6 @@ services:
|
||||||
env:
|
env:
|
||||||
vars:
|
vars:
|
||||||
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
|
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
|
||||||
TAYLORDB_API_TOKEN: secrets.TAYLORDB_API_TOKEN
|
|
||||||
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
|
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
|
||||||
FRONTEND_URL: routing.client.url
|
FRONTEND_URL: routing.client.url
|
||||||
|
|
||||||
|
|
@ -41,7 +39,6 @@ services:
|
||||||
env:
|
env:
|
||||||
vars:
|
vars:
|
||||||
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
|
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
|
||||||
TAYLORDB_API_TOKEN: secrets.TAYLORDB_API_TOKEN
|
|
||||||
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
|
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
|
||||||
FRONTEND_URL: routing.client.url
|
FRONTEND_URL: routing.client.url
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user