Compare commits

..

No commits in common. "26b00bcf61c720bb136c4079d2dfd3ef315bff6e" and "d51a2f372fdb3cd30e27dba3c8795ff7c022eb16" have entirely different histories.

27 changed files with 1709 additions and 2336 deletions

111
AGENTS.md
View File

@ -15,29 +15,6 @@ Build production-ready, modern web applications (primarily dashboards and CRUD i
---
## 📚 Docs for Agents
Before you start implementing, skim these docs in the `docs/` folder:
- **TaylorDB Query Builder**
- `docs/TAYLORDB_QUERY_REFERENCE.md` index for all query examples
- `docs/TAYLORDB_BASIC_QUERIES.md` basic reads, filtering, dates
- `docs/TAYLORDB_WRITE_OPERATIONS.md` inserts, updates, deletes
- `docs/TAYLORDB_ADVANCED_PATTERNS.md` aggregations, pagination, conditional queries
- `docs/TAYLORDB_FIELD_TYPES.md` field type mapping & enums
- `docs/TAYLORDB_ATTACHMENTS.md` working with attachment fields
- `docs/TAYLORDB_PITFALLS_BEST_PRACTICES.md` common mistakes & best practices
- **shadcn/ui Components & Dashboard Patterns**
- `docs/SHADCN_COMPONENTS_GUIDE.md` index for all shadcn/ui docs
- `docs/SHADCN_INSTALLATION.md` how to install shadcn/ui components
- `docs/SHADCN_DASHBOARD_PATTERNS.md` ready-made dashboard/layout patterns
- `docs/SHADCN_DESIGN_AND_LAYOUT.md` design tokens, layout, responsive & performance tips
Use `AGENTS.md` for **workflow and rules** and the `docs/` files for **detailed code examples**.
---
## 📋 Development Workflow
### **Phase 1: Understand Requirements & Design**
@ -77,15 +54,19 @@ Document your design decisions briefly before implementing.
#### Step 1: Set Up Server-Side Data Layer
Use querybuilder which is in **File: `apps/server/taylordb/query-builder.ts`**
**File: `apps/server/taylordb/query-builder.ts`**
You can access the query builder from
This file contains all database operations. Create type-safe CRUD functions for each table:
```typescript
publicProcedure.input({}).query(({ input, ctx }) => {
const queryBuilder = ctx.queryBuilder;
import { createQueryBuilder } from "@taylordb/query-builder";
import type { TaylorDatabase } from "./types.js";
export const queryBuilder = createQueryBuilder<TaylorDatabase>({
baseUrl: process.env.TAYLORDB_BASE_URL!,
baseId: process.env.TAYLORDB_SERVER_ID!,
apiKey: process.env.TAYLORDB_API_TOKEN!,
});
```
// ============================================================================
// READ Operations
@ -258,16 +239,21 @@ Update the design tokens based on your chosen color scheme:
#### Step 2: Create shadcn/ui Components
**Always use shadcn/ui components**. Install components as needed with:
**Always use shadcn/ui components**. Install components as needed:
```bash
pnpm dlx shadcn@latest add <component-name>
```
For concrete install commands and patterns:
**Available by default:**
- See `docs/SHADCN_INSTALLATION.md` for component install snippets
- See `docs/SHADCN_DASHBOARD_PATTERNS.md` for tables, dialogs, forms, toasts, sheets, command palette, etc.
- `button`, `card`, `input`, `label`, `textarea`, `select`, `tabs`, `alert`
**Common additions for dashboards:**
- `table`, `dialog`, `dropdown-menu`, `toast`, `sheet`, `form`, `badge`, `avatar`, `skeleton`
Install with: `pnpm dlx shadcn@latest add table dialog dropdown-menu toast sheet form badge avatar skeleton`
#### Step 3: Build Page Components
@ -594,17 +580,60 @@ apps/client/src/
## 🔧 TaylorDB Query Builder Reference
Instead of duplicating examples here, use the dedicated docs:
### Common Query Patterns
- `docs/TAYLORDB_QUERY_REFERENCE.md` index for all query builder docs
- `docs/TAYLORDB_BASIC_QUERIES.md` `selectFrom`, filtering, ordering, date filters
- `docs/TAYLORDB_WRITE_OPERATIONS.md` `insertInto`, `update`, `deleteFrom`
- `docs/TAYLORDB_ADVANCED_PATTERNS.md` aggregations, totals, conditional queries, pagination
- `docs/TAYLORDB_FIELD_TYPES.md` field type mapping, nullable handling, enums
- `docs/TAYLORDB_ATTACHMENTS.md` selecting and writing attachment fields
- `docs/TAYLORDB_PITFALLS_BEST_PRACTICES.md` pitfalls and best practices
```typescript
// SELECT with specific fields
queryBuilder
.selectFrom("tableName")
.select(["id", "name", "status"])
.execute();
When writing queries in `apps/server/taylordb/query-builder.ts`, mirror the patterns from these docs and keep everything **strongly typed** using `taylordb/types.ts`.
// WHERE conditions
.where("status", "=", "active")
.where("createdAt", ">=", ["exactDay", "2024-01-01"])
.where("tags", "hasAnyOf", ["tag1", "tag2"])
// ORDER BY
.orderBy("createdAt", "desc")
.orderBy("name", "asc")
// INSERT
queryBuilder
.insertInto("tableName")
.values({ name: "John", status: "active" })
.executeTakeFirst();
// UPDATE
queryBuilder
.update("tableName")
.set({ status: "inactive" })
.where("id", "=", 123)
.execute();
// DELETE
queryBuilder
.deleteFrom("tableName")
.where("id", "=", 123)
.execute();
// DELETE multiple
queryBuilder
.deleteFrom("tableName")
.where("id", "hasAnyOf", [1, 2, 3])
.execute();
```
### Field Type Handling
| TaylorDB Field Type | TypeScript Type | Query Value Format |
| ------------------- | --------------- | ---------------------------- |
| Text | `string` | `"value"` |
| Number | `number` | `42` |
| Date | `string` | `["exactDay", "2024-01-01"]` |
| Single Select | `string[]` | `["option"]` |
| Multi Select | `string[]` | `["opt1", "opt2"]` |
| Checkbox | `boolean` | `true` / `false` |
---

View File

@ -174,6 +174,7 @@ This template is designed to deploy to TaylorDB's platform using the included `t
**Environment Variables Required:**
- `TAYLORDB_BASE_URL`
- `TAYLORDB_API_TOKEN`
- `TAYLORDB_SERVER_ID`
---

View File

@ -1,269 +0,0 @@
import { useState, useRef, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Upload, X, FileIcon, CheckCircle2, Send } from "lucide-react";
import { DemoCard, InlineSpinner } from "@/components/demo";
import { trpc } from "@/lib/trpc";
interface FileMetadata {
originalName: string;
mimeType: string;
size: number;
sizeFormatted: string;
}
const BASE_URL =
import.meta.env.VITE_TRPC_URL || "http://localhost:3001/api";
export function FileUploadExample() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [files, setFiles] = useState<File[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isDragging, setIsDragging] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const utils = trpc.useUtils();
const { data: submissions } = trpc.submitUserData.getAll.useQuery();
const submitMutation = trpc.submitUserData.submit.useMutation({
onSuccess: () => {
utils.submitUserData.getAll.invalidate();
setName("");
setEmail("");
setFiles([]);
},
});
const addFiles = useCallback((newFiles: FileList | File[]) => {
setFiles((prev) => [...prev, ...Array.from(newFiles)]);
}, []);
const removeFile = (index: number) => {
setFiles((prev) => prev.filter((_, i) => i !== index));
};
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
if (e.dataTransfer.files.length) {
addFiles(e.dataTransfer.files);
}
},
[addFiles]
);
const handleSubmit = async () => {
setIsLoading(true);
setError(null);
try {
// Step 1: Upload files via /api/upload
let uploadedFiles: FileMetadata[] = [];
if (files.length > 0) {
const formData = new FormData();
files.forEach((file) => formData.append("files", file));
const uploadRes = await fetch(`${BASE_URL}/upload`, {
method: "POST",
body: formData,
credentials: "include",
});
const uploadJson = await uploadRes.json();
if (!uploadRes.ok || !uploadJson.success) {
throw new Error(uploadJson.error || "File upload failed");
}
uploadedFiles = uploadJson.data.files;
}
// Step 2: Submit user data with file references via tRPC
await submitMutation.mutateAsync({
name,
email,
files: uploadedFiles.map((f) => ({
originalName: f.originalName,
mimeType: f.mimeType,
size: f.size,
})),
});
} catch (err) {
setError(err instanceof Error ? err.message : "Something went wrong");
} finally {
setIsLoading(false);
}
};
const formatSize = (bytes: number) => {
if (bytes === 0) return "0 B";
const units = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
};
const canSubmit = name.trim() && email.trim() && !isLoading;
return (
<DemoCard
title="Submit User Data"
description="Upload files via /api/upload, then submit data via tRPC"
icon={Send}
iconColorClass="bg-amber-500/10 text-amber-500"
glowClass="glow-primary"
>
{/* Form Fields */}
<div className="flex gap-3">
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
className="flex-1"
/>
<Input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
type="email"
className="flex-1"
/>
</div>
{/* Drop Zone */}
<div
className={`relative rounded-lg border-2 border-dashed p-6 text-center transition-colors cursor-pointer ${
isDragging
? "border-primary bg-primary/5"
: "border-border/60 hover:border-primary/40 hover:bg-muted/30"
}`}
onDragOver={(e) => {
e.preventDefault();
setIsDragging(true);
}}
onDragLeave={() => setIsDragging(false)}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
>
<input
ref={fileInputRef}
type="file"
multiple
className="hidden"
onChange={(e) => {
if (e.target.files) addFiles(e.target.files);
e.target.value = "";
}}
/>
<Upload className="mx-auto h-8 w-8 text-muted-foreground/60 mb-2" />
<p className="text-sm text-muted-foreground">
{isDragging ? (
<span className="text-primary font-medium">Drop files here</span>
) : (
<>
<span className="font-medium text-foreground">Click to browse</span>{" "}
or drag & drop files
</>
)}
</p>
<p className="text-xs text-muted-foreground/60 mt-1">
Any file type Max 10 MB per file Up to 10 files
</p>
</div>
{/* Selected Files */}
{files.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
{files.length} file{files.length > 1 ? "s" : ""} selected
</p>
{files.map((file, i) => (
<div
key={`${file.name}-${i}`}
className="flex items-center gap-3 p-3 rounded-lg bg-muted/30 border border-border/50"
>
<FileIcon className="h-4 w-4 text-muted-foreground shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{file.name}</p>
<p className="text-xs text-muted-foreground">
{file.type || "unknown"} {formatSize(file.size)}
</p>
</div>
<Button
size="icon"
variant="ghost"
className="h-7 w-7 text-muted-foreground hover:text-destructive shrink-0"
onClick={(e) => {
e.stopPropagation();
removeFile(i);
}}
>
<X className="h-3.5 w-3.5" />
</Button>
</div>
))}
</div>
)}
{/* Submit Button */}
<Button
onClick={handleSubmit}
disabled={!canSubmit}
className="w-full"
>
{isLoading ? (
<>
<InlineSpinner /> Submitting...
</>
) : (
<>
<Send className="h-4 w-4 mr-2" />
Submit User Data
</>
)}
</Button>
{/* Error */}
{error && (
<div className="p-3 rounded-lg bg-destructive/10 border border-destructive/30 text-sm text-destructive">
{error}
</div>
)}
{/* Submissions List */}
{submissions && submissions.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Recent submissions
</p>
{submissions.map((sub) => (
<div
key={sub.id}
className="p-3 rounded-lg bg-muted/30 border border-border/50 space-y-1"
>
<div className="flex items-center gap-2">
<CheckCircle2 className="h-3.5 w-3.5 text-green-500 shrink-0" />
<span className="text-sm font-medium">{sub.name}</span>
<span className="text-xs text-muted-foreground">{sub.email}</span>
</div>
{sub.files.length > 0 && (
<div className="flex flex-wrap gap-1 ml-5.5">
{sub.files.map((f, i) => (
<span
key={i}
className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary"
>
<FileIcon className="h-3 w-3" />
{f.originalName}
</span>
))}
</div>
)}
</div>
))}
</div>
)}
</DemoCard>
);
}

View File

@ -1,4 +1,3 @@
export { HelloExample } from "./HelloExample";
export { UsersExample } from "./UsersExample";
export { PostsExample } from "./PostsExample";
export { FileUploadExample } from "./FileUploadExample";

View File

@ -3,7 +3,6 @@ import {
HelloExample,
UsersExample,
PostsExample,
FileUploadExample,
} from "@/components/demo/examples";
export default function TRPCDemoPage() {
@ -30,7 +29,6 @@ export default function TRPCDemoPage() {
<HelloExample />
<UsersExample />
<PostsExample />
<FileUploadExample />
</main>
{/* Footer */}

View File

@ -1,17 +1,12 @@
import express from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
import * as trpcExpress from "@trpc/server/adapters/express";
import { appRouter } from "./router.js";
import { createContext } from "./trpc.js";
import { fileUploadRouter } from "./routers/fileUpload.js";
const app = express();
const PORT = process.env.PORT || 3001;
// Parse cookies
app.use(cookieParser());
// Enable CORS for frontend (adjust origin in production)
app.use(
cors({
@ -28,9 +23,6 @@ app.get("/api/health", (_req, res) => {
res.json({ status: "ok", timestamp: new Date().toISOString() });
});
// File upload endpoint (multipart/form-data — not supported by tRPC Express adapter)
app.use("/api/upload", fileUploadRouter);
// tRPC middleware
app.use(
"/api/trpc",

View File

@ -14,19 +14,15 @@
"start": "node dist/index.js"
},
"dependencies": {
"@taylordb/query-builder": "^0.11.4",
"@taylordb/query-builder": "^0.10.3",
"@trpc/server": "^11.8.1",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"express": "^5.2.1",
"multer": "^2.0.2",
"zod": "^4.3.5"
},
"devDependencies": {
"@types/cookie-parser": "^1.4.10",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/multer": "^2.0.0",
"@types/node": "^24.10.1",
"concurrently": "^9.2.1",
"esbuild": "^0.27.2",

View File

@ -1,6 +1,6 @@
import { z } from "zod";
import { router, publicProcedure } from "./trpc";
import { usersRouter, postsRouter, submitUserDataRouter } from "./routers";
import { usersRouter, postsRouter } from "./routers";
/**
* Main tRPC Router
@ -20,7 +20,6 @@ export const appRouter = router({
// ============================================================================
users: usersRouter,
posts: postsRouter,
submitUserData: submitUserDataRouter,
// ============================================================================
// Global / Utility Procedures

View File

@ -1,97 +0,0 @@
import { Router } from "express";
import multer from "multer";
/**
* File Upload Router
*
* Express router for handling multipart FormData file uploads.
* tRPC v11's FormData support is designed for fetch-based adapters (Next.js, etc.),
* not the Express adapter. This dedicated Express route is the recommended pattern
* for handling file uploads in Express + tRPC stacks.
*
* Uses TaylorDB's uploadAttachments pattern to store files.
* See docs/TAYLORDB_ATTACHMENTS.md for details.
*/
// Configure multer with memory storage (files available as Buffer)
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 10 * 1024 * 1024, // 10 MB per file
files: 10, // max 10 files
},
});
const fileUploadRouter = Router();
/**
* POST /api/upload
*
* Accepts multipart/form-data with:
* - files (File[], multiple files of any type)
*
* Returns metadata about each uploaded file.
*/
fileUploadRouter.post("/", upload.array("files", 10), (req, res) => {
const files = (req.files as Express.Multer.File[]) || [];
if (files.length === 0) {
res.status(400).json({
success: false,
error: "At least one file is required.",
});
return;
}
const fileMetadata = files.map((file) => ({
originalName: file.originalname,
mimeType: file.mimetype,
size: file.size,
sizeFormatted: formatFileSize(file.size),
}));
// ────────────────────────────────────────────────────────────────────────
// TaylorDB Attachment Example (see docs/TAYLORDB_ATTACHMENTS.md)
//
// To store uploaded files in a TaylorDB record, convert multer buffers
// to Blobs and use qb.uploadAttachments():
//
// const attachments = await qb.uploadAttachments(
// files.map((file) => ({
// file: new Blob([file.buffer], { type: file.mimetype }),
// name: file.originalname,
// }))
// );
//
// // Then use the attachments when inserting/updating a record:
// await qb.insertInto("tableName").values({
// name: "Some record",
// documents: attachments, // attachment column
// }).execute();
//
// // Or when updating an existing record:
// await qb.update("tableName").set({
// documents: attachments,
// }).where("id", "=", recordId).execute();
// ────────────────────────────────────────────────────────────────────────
res.json({
success: true,
data: {
filesCount: files.length,
files: fileMetadata,
totalSize: formatFileSize(files.reduce((sum, f) => sum + f.size, 0)),
uploadedAt: new Date().toISOString(),
},
});
});
/** Format bytes to human-readable string */
function formatFileSize(bytes: number): string {
if (bytes === 0) return "0 B";
const units = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
}
export { fileUploadRouter };

View File

@ -7,4 +7,3 @@
export { usersRouter } from "./users";
export { postsRouter } from "./posts";
export { submitUserDataRouter } from "./submitUserData";

View File

@ -6,39 +6,6 @@ import { router, publicProcedure } from "../trpc";
*
* Another example sub-router showing a different domain.
* Demonstrates relationships (author references users).
*
* Example using TaylorDB queryBuilder (available via ctx.queryBuilder):
*
* // Query all posts
* const posts = await ctx.queryBuilder.from("posts").select("*");
*
* // Query with filters
* const publishedPosts = await ctx.queryBuilder
* .from("posts")
* .where({ published: true })
* .select("*");
*
* // Create a new post
* const newPost = await ctx.queryBuilder
* .from("posts")
* .insert({
* title: "My Title",
* content: "My Content",
* authorId: 1,
* published: false
* });
*
* // Update a post
* const updatedPost = await ctx.queryBuilder
* .from("posts")
* .where({ id: 1 })
* .update({ published: true });
*
* // Delete a post
* await ctx.queryBuilder
* .from("posts")
* .where({ id: 1 })
* .delete();
*/
// In-memory store for demonstration
@ -69,7 +36,7 @@ export const postsRouter = router({
published: z.boolean().optional(),
authorId: z.number().optional(),
})
.optional(),
.optional()
)
.query(({ input }) => {
let result = posts;
@ -121,7 +88,7 @@ export const postsRouter = router({
title: z.string().min(1).max(200).optional(),
content: z.string().min(1).optional(),
published: z.boolean().optional(),
}),
})
)
.mutation(({ input }) => {
const post = posts.find((p) => p.id === input.id);

View File

@ -1,66 +0,0 @@
import { z } from "zod";
import { router, publicProcedure } from "../trpc";
/**
* Submit User Data Router
*
* tRPC router for submitting user data (name, email) with file attachment references.
* Files should first be uploaded via /api/upload, then referenced here.
*
* In a real app, you'd use qb.uploadAttachments() to store files in TaylorDB
* and insert the record with the attachment column. This demo uses in-memory storage.
*
* TaylorDB attachment pattern (see docs/TAYLORDB_ATTACHMENTS.md):
* await qb.insertInto("users").values({
* name: "Jane",
* avatar: await qb.uploadAttachments([
* { file: new Blob([buffer]), name: "photo.png" },
* ]),
* }).execute();
*/
// In-memory store for demonstration
interface Submission {
id: number;
name: string;
email: string;
files: { originalName: string; mimeType: string; size: number }[];
submittedAt: string;
}
const submissions: Submission[] = [];
let nextId = 1;
export const submitUserDataRouter = router({
/** Submit user data with file metadata */
submit: publicProcedure
.input(
z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Valid email is required"),
files: z.array(
z.object({
originalName: z.string(),
mimeType: z.string(),
size: z.number(),
})
),
})
)
.mutation(({ input }) => {
const submission: Submission = {
id: nextId++,
name: input.name,
email: input.email,
files: input.files,
submittedAt: new Date().toISOString(),
};
submissions.push(submission);
return submission;
}),
/** Get all submissions */
getAll: publicProcedure.query(() => {
return submissions;
}),
});

View File

@ -0,0 +1,319 @@
import { createQueryBuilder } from "@taylordb/query-builder";
import type { TaylorDatabase } from "./types.js";
/**
* TaylorDB Query Builder Instance
*
* This is the main query builder instance configured with your TaylorDB credentials.
* Use this to perform all database operations in a type-safe manner.
*/
export const queryBuilder = createQueryBuilder<TaylorDatabase>({
baseUrl: process.env.TAYLORDB_BASE_URL!,
baseId: process.env.TAYLORDB_SERVER_ID!,
apiKey: process.env.TAYLORDB_API_TOKEN!,
});
/**
* ============================================================================
* Example Query Functions
* ============================================================================
*
* Below are example patterns for common database operations.
* Replace these with your own functions based on your actual schema.
*
* For comprehensive examples, see: /docs/TAYLORDB_QUERY_REFERENCE.md
*/
// ============================================================================
// READ Operations (Queries)
// ============================================================================
/**
* Example: Get all records from a table
*
* @example
* export async function getAllUsers() {
* return await queryBuilder
* .selectFrom("users")
* .select(["id", "name", "email", "createdAt"])
* .orderBy("createdAt", "desc")
* .execute();
* }
*/
/**
* Example: Get a single record by ID
*
* @example
* export async function getUserById(id: number) {
* return await queryBuilder
* .selectFrom("users")
* .where("id", "=", id)
* .executeTakeFirst();
* }
*/
/**
* Example: Get records with filtering
*
* @example
* export async function getActiveUsers() {
* return await queryBuilder
* .selectFrom("users")
* .where("status", "=", "active")
* .orderBy("name", "asc")
* .execute();
* }
*/
/**
* Example: Get records with date range filtering
*
* @example
* export async function getRecordsInDateRange(startDate: string, endDate: string) {
* return await queryBuilder
* .selectFrom("records")
* .where("date", ">=", ["exactDay", startDate])
* .where("date", "<=", ["exactDay", endDate])
* .orderBy("date", "asc")
* .execute();
* }
*/
// ============================================================================
// CREATE Operations (Insert)
// ============================================================================
/**
* Example: Insert a new record
*
* @example
* export async function createUser(data: { name: string; email: string }) {
* return await queryBuilder
* .insertInto("users")
* .values({
* name: data.name,
* email: data.email,
* status: "active",
* })
* .executeTakeFirst();
* }
*/
/**
* Example: Insert with single-select field
*
* Note: Single-select fields must be wrapped in an array
*
* @example
* export async function createTask(data: { title: string; priority: "low" | "medium" | "high" }) {
* return await queryBuilder
* .insertInto("tasks")
* .values({
* title: data.title,
* priority: [data.priority], // Wrap in array for single-select
* })
* .executeTakeFirst();
* }
*/
/**
* Example: Insert with computed fields
*
* @example
* export async function createOrder(data: { quantity: number; pricePerUnit: number }) {
* const totalPrice = data.quantity * data.pricePerUnit;
*
* return await queryBuilder
* .insertInto("orders")
* .values({
* quantity: data.quantity,
* pricePerUnit: data.pricePerUnit,
* totalPrice: totalPrice,
* })
* .executeTakeFirst();
* }
*/
// ============================================================================
// UPDATE Operations
// ============================================================================
/**
* Example: Update a record
*
* @example
* export async function updateUser(id: number, data: { name?: string; email?: string }) {
* return await queryBuilder
* .update("users")
* .set(data)
* .where("id", "=", id)
* .execute();
* }
*/
/**
* Example: Update with conditional recalculation
*
* @example
* export async function updateOrder(id: number, data: { quantity?: number; pricePerUnit?: number }) {
* // Fetch current record to compute total
* const currentOrder = await queryBuilder
* .selectFrom("orders")
* .select(["quantity", "pricePerUnit"])
* .where("id", "=", id)
* .executeTakeFirst();
*
* if (!currentOrder) {
* throw new Error("Order not found");
* }
*
* const newQuantity = data.quantity ?? currentOrder.quantity ?? 0;
* const newPrice = data.pricePerUnit ?? currentOrder.pricePerUnit ?? 0;
* const totalPrice = newQuantity * newPrice;
*
* return await queryBuilder
* .update("orders")
* .set({
* ...data,
* totalPrice,
* })
* .where("id", "=", id)
* .execute();
* }
*/
// ============================================================================
// DELETE Operations
// ============================================================================
/**
* Example: Delete a single record
*
* @example
* export async function deleteUser(id: number) {
* return await queryBuilder
* .deleteFrom("users")
* .where("id", "=", id)
* .execute();
* }
*/
/**
* Example: Delete multiple records by IDs
*
* @example
* export async function deleteUsers(ids: number[]) {
* return await queryBuilder
* .deleteFrom("users")
* .where("id", "hasAnyOf", ids)
* .execute();
* }
*/
/**
* Example: Delete with condition
*
* @example
* export async function deleteInactiveUsers() {
* return await queryBuilder
* .deleteFrom("users")
* .where("status", "=", "inactive")
* .execute();
* }
*/
// ============================================================================
// AGGREGATION Operations (Manual)
// ============================================================================
/**
* Example: Calculate statistics
*
* @example
* export async function getUserStats() {
* const users = await queryBuilder
* .selectFrom("users")
* .select(["age"])
* .execute();
*
* if (users.length === 0) {
* return { count: 0, average: null, min: null, max: null };
* }
*
* const ages = users.map(u => u.age).filter((a): a is number => a !== undefined);
*
* return {
* count: ages.length,
* average: ages.reduce((a, b) => a + b, 0) / ages.length,
* min: Math.min(...ages),
* max: Math.max(...ages),
* };
* }
*/
/**
* Example: Sum totals for a date
*
* @example
* export async function getTotalSalesForDate(date: string) {
* const sales = await queryBuilder
* .selectFrom("sales")
* .select(["amount", "quantity"])
* .where("date", "=", ["exactDay", date])
* .execute();
*
* return {
* totalAmount: sales.reduce((sum, s) => sum + (s.amount ?? 0), 0),
* totalQuantity: sales.reduce((sum, s) => sum + (s.quantity ?? 0), 0),
* };
* }
*/
/**
* ============================================================================
* Query Builder Quick Reference
* ============================================================================
*
* SELECT:
* - .selectFrom("tableName")
* - .select(["field1", "field2"])
* - .execute() // Returns array
* - .executeTakeFirst() // Returns single record or undefined
*
* WHERE:
* - .where("field", "=", value)
* - .where("field", ">", value)
* - .where("field", "hasAnyOf", [value1, value2])
* - .where("date", ">=", ["exactDay", "2024-01-01"])
*
* ORDER BY:
* - .orderBy("field", "asc")
* - .orderBy("field", "desc")
*
* INSERT:
* - .insertInto("tableName")
* - .values({ field1: value1, field2: value2 })
* - .executeTakeFirst()
*
* UPDATE:
* - .update("tableName")
* - .set({ field1: value1 })
* - .where("id", "=", id)
* - .execute()
*
* DELETE:
* - .deleteFrom("tableName")
* - .where("id", "=", id)
* - .execute()
*
* Field Types:
* - Text: string
* - Number: number
* - Date: ["exactDay", "YYYY-MM-DD"]
* - Single Select: ["option"]
* - Multi Select: ["opt1", "opt2"]
* - Boolean: true/false
*
* For comprehensive examples, see: /docs/TAYLORDB_QUERY_REFERENCE.md
*/

View File

@ -1,30 +1,15 @@
import { createQueryBuilder } from "@taylordb/query-builder";
import { initTRPC } from "@trpc/server";
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
import type { TaylorDatabase } from "./taylordb/types";
/**
* Create context for each tRPC request
* This is where you can add user session, database clients, etc.
*/
export const createContext = ({ req, res }: CreateExpressContextOptions) => {
// Extract app_access_token from cookies
const appAccessToken = req.cookies?.app_access_token;
if (!appAccessToken) {
throw new Error("Unauthorized: app_access_token cookie is required");
}
const queryBuilder = createQueryBuilder<TaylorDatabase>({
baseUrl: process.env.TAYLORDB_BASE_URL!,
baseId: process.env.TAYLORDB_SERVER_ID!,
apiKey: appAccessToken,
});
return {
req,
res,
queryBuilder,
// Add any shared context here (e.g., database client, user session)
};
};

View File

@ -1,55 +1,651 @@
# shadcn/ui Dashboard Components Guide
This is the **entry point** for shadcn/ui documentation in this template. The guide is split into smaller files so agents (and humans) can jump to the topic they need.
This guide provides examples of using shadcn/ui components to build modern dashboard interfaces for your TaylorDB applications.
---
## 📚 Topics
## 📦 Installation
- **Installation**
See `SHADCN_INSTALLATION.md` for:
- Installing individual components (core, dashboard, data display, advanced)
- Command examples for `pnpm dlx shadcn@latest add`
### Install Individual Components
- **Dashboard Patterns**
See `SHADCN_DASHBOARD_PATTERNS.md` for:
- Stats cards layout
- Data table with dropdown actions
- Create/edit form in a dialog
- Loading states with Skeleton
- Tabs for different views
- Status badges
- Toast notifications (with tRPC mutations)
- Form with validation (react-hook-form + zod)
- Side sheet for details
- Command palette (search, Cmd/Ctrl+K)
```bash
# Core components (already included)
pnpm dlx shadcn@latest add button card input label textarea select tabs alert
- **Design & Layout**
See `SHADCN_DESIGN_AND_LAYOUT.md` for:
- Color schemes (semantic tokens)
- Spacing conventions
- Icons (lucide-react)
- Responsive grids and hide-on-mobile
- Performance tips (lazy dialogs, virtualization, skeletons, optimistic updates, debounce)
# Dashboard-specific components
pnpm dlx shadcn@latest add table dialog dropdown-menu toast sheet form badge avatar skeleton
# Data display components
pnpm dlx shadcn@latest add separator progress scroll-area tooltip
# Advanced components
pnpm dlx shadcn@latest add command popover calendar date-picker
```
---
## How Agents Should Use These Docs
## 🎨 Common Dashboard Patterns
1. **Need to add a component?**
Use `SHADCN_INSTALLATION.md` for the exact `pnpm dlx shadcn@latest add` commands.
### 1. Dashboard Layout with Stats Cards
2. **Building a dashboard or CRUD UI?**
Use `SHADCN_DASHBOARD_PATTERNS.md` for copy-paste patterns (tables, dialogs, forms, toasts, sheets, etc.).
```typescript
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
3. **Styling and layout?**
Use `SHADCN_DESIGN_AND_LAYOUT.md` for tokens, spacing, responsive patterns, and performance.
export default function DashboardPage() {
return (
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold mb-6">Dashboard</h1>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-muted-foreground">
Total Users
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">1,234</div>
<p className="text-xs text-muted-foreground mt-1">
+20% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-muted-foreground">
Revenue
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">$45,231</div>
<p className="text-xs text-muted-foreground mt-1">
+12% from last month
</p>
</CardContent>
</Card>
{/* More stats cards... */}
</div>
</div>
);
}
```
### 2. Data Table with Actions
```typescript
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { MoreHorizontal, Pencil, Trash } from "lucide-react";
export default function UsersTable({ users }: { users: User[] }) {
return (
<Card>
<CardHeader>
<CardTitle>Users</CardTitle>
<CardDescription>Manage your user accounts</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<Badge
variant={user.status === "active" ? "default" : "secondary"}
>
{user.status}
</Badge>
</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>
<Pencil className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
<DropdownMenuItem className="text-destructive">
<Trash className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
);
}
```
### 3. Create/Edit Form in Dialog
```typescript
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useState } from "react";
import { PlusIcon } from "lucide-react";
export default function CreateUserDialog() {
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const createMutation = trpc.users.create.useMutation({
onSuccess: () => {
setOpen(false);
setName("");
setEmail("");
},
});
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<PlusIcon className="mr-2 h-4 w-4" />
Add User
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New User</DialogTitle>
<DialogDescription>Add a new user to your database</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="John Doe"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="john@example.com"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
onClick={() => createMutation.mutate({ name, email })}
disabled={createMutation.isPending}
>
Create User
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
```
### 4. Loading States with Skeleton
```typescript
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
export default function DashboardSkeleton() {
return (
<div className="container mx-auto p-8">
<Skeleton className="h-10 w-48 mb-6" />
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
{[...Array(4)].map((_, i) => (
<Card key={i}>
<CardHeader>
<Skeleton className="h-4 w-24" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-20 mb-2" />
<Skeleton className="h-3 w-32" />
</CardContent>
</Card>
))}
</div>
</div>
);
}
// Usage in main component
export default function DashboardPage() {
const { data: stats, isLoading } = trpc.dashboard.getStats.useQuery();
if (isLoading) {
return <DashboardSkeleton />;
}
return <div>{/* Actual dashboard */}</div>;
}
```
### 5. Tabs for Different Views
```typescript
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
export default function DataExplorer() {
return (
<Card>
<CardHeader>
<CardTitle>Data Explorer</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="overview">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
<TabsTrigger value="reports">Reports</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
{/* Overview content */}
</TabsContent>
<TabsContent value="analytics" className="space-y-4">
{/* Analytics content */}
</TabsContent>
<TabsContent value="reports" className="space-y-4">
{/* Reports content */}
</TabsContent>
</Tabs>
</CardContent>
</Card>
);
}
```
### 6. Status Badges
```typescript
import { Badge } from "@/components/ui/badge";
function StatusBadge({ status }: { status: string }) {
const variants = {
active: "default",
pending: "secondary",
inactive: "outline",
error: "destructive",
} as const;
return (
<Badge variant={variants[status as keyof typeof variants] || "outline"}>
{status}
</Badge>
);
}
// Usage
<StatusBadge status="active" />;
```
### 7. Toast Notifications
```typescript
import { useToast } from "@/hooks/use-toast";
import { Button } from "@/components/ui/button";
export default function ActionsPage() {
const { toast } = useToast();
const deleteMutation = trpc.items.delete.useMutation({
onSuccess: () => {
toast({
title: "Success!",
description: "Item deleted successfully",
});
},
onError: (error) => {
toast({
title: "Error",
description: error.message,
variant: "destructive",
});
},
});
return (
<Button onClick={() => deleteMutation.mutate({ id: 1 })}>
Delete Item
</Button>
);
}
// Don't forget to add <Toaster /> in your App.tsx or layout
```
### 8. Form with Validation
```typescript
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
const formSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().email("Invalid email address"),
age: z.number().min(0).max(120),
});
export default function UserForm() {
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
email: "",
age: 0,
},
});
const createMutation = trpc.users.create.useMutation({
onSuccess: () => {
form.reset();
},
});
const onSubmit = (data: z.infer<typeof formSchema>) => {
createMutation.mutate(data);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
</FormControl>
<FormDescription>
Your full name as it appears on documents.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={createMutation.isPending}>
{createMutation.isPending ? "Submitting..." : "Submit"}
</Button>
</form>
</Form>
);
}
```
### 9. Side Sheet for Details
```typescript
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
export default function UserDetailsSheet({ userId }: { userId: number }) {
const { data: user } = trpc.users.getById.useQuery({ id: userId });
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">View Details</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>User Details</SheetTitle>
<SheetDescription>Information about this user</SheetDescription>
</SheetHeader>
{user && (
<div className="mt-6 space-y-4">
<div>
<h4 className="text-sm font-medium text-muted-foreground">
Name
</h4>
<p className="text-base">{user.name}</p>
</div>
<div>
<h4 className="text-sm font-medium text-muted-foreground">
Email
</h4>
<p className="text-base">{user.email}</p>
</div>
{/* More fields... */}
</div>
)}
</SheetContent>
</Sheet>
);
}
```
### 10. Command Palette (Search)
```typescript
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { useState, useEffect } from "react";
export default function GlobalSearch() {
const [open, setOpen] = useState(false);
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
}
};
document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, []);
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Users">
<CommandItem>John Doe</CommandItem>
<CommandItem>Jane Smith</CommandItem>
</CommandGroup>
<CommandGroup heading="Projects">
<CommandItem>Project Alpha</CommandItem>
<CommandItem>Project Beta</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
);
}
```
---
## Resources
## 🎨 Design Tips
### Color Schemes
Use semantic color tokens:
- `bg-background` / `text-foreground` - Main background and text
- `bg-card` / `text-card-foreground` - Card surfaces
- `bg-primary` / `text-primary-foreground` - Primary actions
- `bg-destructive` - Destructive actions (delete, etc.)
- `bg-muted` / `text-muted-foreground` - Subtle UI elements
### Spacing
Use consistent spacing:
- `space-y-4` / `gap-4` - Between related items
- `space-y-6` / `gap-6` - Between sections
- `p-4` / `p-6` - Card padding
- `p-8` - Page padding
### Icons
Use `lucide-react` for consistent icons:
```typescript
import { Home, Users, Settings, Plus, Edit, Trash, Search } from "lucide-react";
<Button>
<Plus className="mr-2 h-4 w-4" />
Add Item
</Button>;
```
---
## 📱 Responsive Design
### Grid Layouts
```typescript
// 1 column on mobile, 2 on tablet, 4 on desktop
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{/* cards */}
</div>
// 1 column on mobile, 3 on desktop
<div className="grid gap-6 lg:grid-cols-3">
{/* cards */}
</div>
```
### Hide on Mobile
```typescript
// Hide text on mobile, show on desktop
<span className="hidden sm:inline">Dashboard</span>
// Different layout on mobile
<div className="flex flex-col md:flex-row gap-4">
{/* content */}
</div>
```
---
## 🚀 Performance Tips
1. **Lazy load dialogs**: Only render dialog content when open
2. **Virtualize long lists**: Use libraries like `react-window`
3. **Skeleton loaders**: Always show loading states
4. **Optimistic updates**: Update UI before server confirms
5. **Debounce search**: Don't query on every keystroke
---
## 📚 Resources
- **shadcn/ui**: https://ui.shadcn.com/
- **Tailwind CSS**: https://tailwindcss.com/
- **Lucide Icons**: https://lucide.dev/
- **React Hook Form**: https://react-hook-form.com/
---
**Remember**: Always test your components in both light and dark mode, and on different screen sizes!

View File

@ -1,566 +0,0 @@
# shadcn/ui — Dashboard Patterns
Examples of using shadcn/ui components to build dashboard interfaces for TaylorDB applications.
---
## 1. Dashboard Layout with Stats Cards
```typescript
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
export default function DashboardPage() {
return (
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold mb-6">Dashboard</h1>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-muted-foreground">
Total Users
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">1,234</div>
<p className="text-xs text-muted-foreground mt-1">
+20% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium text-muted-foreground">
Revenue
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">$45,231</div>
<p className="text-xs text-muted-foreground mt-1">
+12% from last month
</p>
</CardContent>
</Card>
{/* More stats cards... */}
</div>
</div>
);
}
```
---
## 2. Data Table with Actions
```typescript
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { MoreHorizontal, Pencil, Trash } from "lucide-react";
export default function UsersTable({ users }: { users: User[] }) {
return (
<Card>
<CardHeader>
<CardTitle>Users</CardTitle>
<CardDescription>Manage your user accounts</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<Badge
variant={user.status === "active" ? "default" : "secondary"}
>
{user.status}
</Badge>
</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>
<Pencil className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
<DropdownMenuItem className="text-destructive">
<Trash className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
);
}
```
---
## 3. Create/Edit Form in Dialog
```typescript
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useState } from "react";
import { PlusIcon } from "lucide-react";
export default function CreateUserDialog() {
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const createMutation = trpc.users.create.useMutation({
onSuccess: () => {
setOpen(false);
setName("");
setEmail("");
},
});
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<PlusIcon className="mr-2 h-4 w-4" />
Add User
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New User</DialogTitle>
<DialogDescription>Add a new user to your database</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="John Doe"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="john@example.com"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
onClick={() => createMutation.mutate({ name, email })}
disabled={createMutation.isPending}
>
Create User
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
```
---
## 4. Loading States with Skeleton
```typescript
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
export default function DashboardSkeleton() {
return (
<div className="container mx-auto p-8">
<Skeleton className="h-10 w-48 mb-6" />
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
{[...Array(4)].map((_, i) => (
<Card key={i}>
<CardHeader>
<Skeleton className="h-4 w-24" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-20 mb-2" />
<Skeleton className="h-3 w-32" />
</CardContent>
</Card>
))}
</div>
</div>
);
}
// Usage in main component
export default function DashboardPage() {
const { data: stats, isLoading } = trpc.dashboard.getStats.useQuery();
if (isLoading) {
return <DashboardSkeleton />;
}
return <div>{/* Actual dashboard */}</div>;
}
```
---
## 5. Tabs for Different Views
```typescript
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
export default function DataExplorer() {
return (
<Card>
<CardHeader>
<CardTitle>Data Explorer</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="overview">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
<TabsTrigger value="reports">Reports</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
{/* Overview content */}
</TabsContent>
<TabsContent value="analytics" className="space-y-4">
{/* Analytics content */}
</TabsContent>
<TabsContent value="reports" className="space-y-4">
{/* Reports content */}
</TabsContent>
</Tabs>
</CardContent>
</Card>
);
}
```
---
## 6. Status Badges
```typescript
import { Badge } from "@/components/ui/badge";
function StatusBadge({ status }: { status: string }) {
const variants = {
active: "default",
pending: "secondary",
inactive: "outline",
error: "destructive",
} as const;
return (
<Badge variant={variants[status as keyof typeof variants] || "outline"}>
{status}
</Badge>
);
}
// Usage
<StatusBadge status="active" />;
```
---
## 7. Toast Notifications
```typescript
import { useToast } from "@/hooks/use-toast";
import { Button } from "@/components/ui/button";
export default function ActionsPage() {
const { toast } = useToast();
const deleteMutation = trpc.items.delete.useMutation({
onSuccess: () => {
toast({
title: "Success!",
description: "Item deleted successfully",
});
},
onError: (error) => {
toast({
title: "Error",
description: error.message,
variant: "destructive",
});
},
});
return (
<Button onClick={() => deleteMutation.mutate({ id: 1 })}>
Delete Item
</Button>
);
}
// Don't forget to add <Toaster /> in your App.tsx or layout
```
---
## 8. Form with Validation
```typescript
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
const formSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().email("Invalid email address"),
age: z.number().min(0).max(120),
});
export default function UserForm() {
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
email: "",
age: 0,
},
});
const createMutation = trpc.users.create.useMutation({
onSuccess: () => {
form.reset();
},
});
const onSubmit = (data: z.infer<typeof formSchema>) => {
createMutation.mutate(data);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
</FormControl>
<FormDescription>
Your full name as it appears on documents.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={createMutation.isPending}>
{createMutation.isPending ? "Submitting..." : "Submit"}
</Button>
</form>
</Form>
);
}
```
---
## 9. Side Sheet for Details
```typescript
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
export default function UserDetailsSheet({ userId }: { userId: number }) {
const { data: user } = trpc.users.getById.useQuery({ id: userId });
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">View Details</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>User Details</SheetTitle>
<SheetDescription>Information about this user</SheetDescription>
</SheetHeader>
{user && (
<div className="mt-6 space-y-4">
<div>
<h4 className="text-sm font-medium text-muted-foreground">
Name
</h4>
<p className="text-base">{user.name}</p>
</div>
<div>
<h4 className="text-sm font-medium text-muted-foreground">
Email
</h4>
<p className="text-base">{user.email}</p>
</div>
{/* More fields... */}
</div>
)}
</SheetContent>
</Sheet>
);
}
```
---
## 10. Command Palette (Search)
```typescript
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { useState, useEffect } from "react";
export default function GlobalSearch() {
const [open, setOpen] = useState(false);
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
}
};
document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, []);
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Users">
<CommandItem>John Doe</CommandItem>
<CommandItem>Jane Smith</CommandItem>
</CommandGroup>
<CommandGroup heading="Projects">
<CommandItem>Project Alpha</CommandItem>
<CommandItem>Project Beta</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
);
}
```
---
For installation and design/layout, see:
- **Installation**: `SHADCN_INSTALLATION.md`
- **Design & layout**: `SHADCN_DESIGN_AND_LAYOUT.md`
- **Main guide index**: `SHADCN_COMPONENTS_GUIDE.md`

View File

@ -1,91 +0,0 @@
# shadcn/ui — Design & Layout
Design tokens, spacing, icons, responsive layouts, and performance tips for shadcn/ui dashboards.
---
## Design Tips
### Color Schemes
Use semantic color tokens:
- `bg-background` / `text-foreground` — Main background and text
- `bg-card` / `text-card-foreground` — Card surfaces
- `bg-primary` / `text-primary-foreground` — Primary actions
- `bg-destructive` — Destructive actions (delete, etc.)
- `bg-muted` / `text-muted-foreground` — Subtle UI elements
### Spacing
Use consistent spacing:
- `space-y-4` / `gap-4` — Between related items
- `space-y-6` / `gap-6` — Between sections
- `p-4` / `p-6` — Card padding
- `p-8` — Page padding
### Icons
Use `lucide-react` for consistent icons:
```typescript
import { Home, Users, Settings, Plus, Edit, Trash, Search } from "lucide-react";
<Button>
<Plus className="mr-2 h-4 w-4" />
Add Item
</Button>;
```
---
## Responsive Design
### Grid Layouts
```typescript
// 1 column on mobile, 2 on tablet, 4 on desktop
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{/* cards */}
</div>
// 1 column on mobile, 3 on desktop
<div className="grid gap-6 lg:grid-cols-3">
{/* cards */}
</div>
```
### Hide on Mobile
```typescript
// Hide text on mobile, show on desktop
<span className="hidden sm:inline">Dashboard</span>
// Different layout on mobile
<div className="flex flex-col md:flex-row gap-4">
{/* content */}
</div>
```
---
## Performance Tips
1. **Lazy load dialogs** — Only render dialog content when open
2. **Virtualize long lists** — Use libraries like `react-window`
3. **Skeleton loaders** — Always show loading states
4. **Optimistic updates** — Update UI before server confirms
5. **Debounce search** — Don't query on every keystroke
---
**Remember**: Always test your components in both light and dark mode, and on different screen sizes.
---
For more, see:
- **Installation**: `SHADCN_INSTALLATION.md`
- **Dashboard patterns**: `SHADCN_DASHBOARD_PATTERNS.md`
- **Main guide index**: `SHADCN_COMPONENTS_GUIDE.md`

View File

@ -1,29 +0,0 @@
# shadcn/ui — Installation
How to add shadcn/ui components to your TaylorDB app.
---
## Install Individual Components
```bash
# Core components (often already included)
pnpm dlx shadcn@latest add button card input label textarea select tabs alert
# Dashboard-specific components
pnpm dlx shadcn@latest add table dialog dropdown-menu toast sheet form badge avatar skeleton
# Data display components
pnpm dlx shadcn@latest add separator progress scroll-area tooltip
# Advanced components
pnpm dlx shadcn@latest add command popover calendar date-picker
```
---
For patterns and examples, see:
- **Dashboard patterns**: `SHADCN_DASHBOARD_PATTERNS.md`
- **Design & layout**: `SHADCN_DESIGN_AND_LAYOUT.md`
- **Main guide index**: `SHADCN_COMPONENTS_GUIDE.md`

View File

@ -1,117 +0,0 @@
# TaylorDB Advanced Patterns
This document covers **advanced query patterns** using the TaylorDB query builder:
- Manual aggregations
- Summation helpers
- Conditional queries
- Pagination
---
## Aggregations (Manual)
Since TaylorDB query builder might not have built-in aggregations, compute manually:
```typescript
export async function getUserStats() {
const users = await queryBuilder
.selectFrom("users")
.select(["age"])
.execute();
if (users.length === 0) {
return { count: 0, average: null, min: null, max: null };
}
const ages = users
.map((u) => u.age)
.filter((a): a is number => a !== undefined);
return {
count: ages.length,
average: ages.reduce((a, b) => a + b, 0) / ages.length,
min: Math.min(...ages),
max: Math.max(...ages),
};
}
```
---
## Sum Totals
```typescript
export async function getTotalCaloriesForDate(date: string) {
const entries = await queryBuilder
.selectFrom("meals")
.select(["calories", "protein", "carbs", "fats"])
.where("date", "=", ["exactDay", date])
.execute();
return {
totalCalories: entries.reduce((sum, e) => sum + (e.calories ?? 0), 0),
totalProtein: entries.reduce((sum, e) => sum + (e.protein ?? 0), 0),
totalCarbs: entries.reduce((sum, e) => sum + (e.carbs ?? 0), 0),
totalFats: entries.reduce((sum, e) => sum + (e.fats ?? 0), 0),
};
}
```
---
## Conditional Queries
```typescript
export async function searchTasks(filters: {
projectId?: number;
status?: string;
dueAfter?: string;
}) {
let query = queryBuilder
.selectFrom("tasks")
.select(["id", "title", "status", "dueDate"]);
if (filters.projectId) {
query = query.where("projectId", "=", filters.projectId);
}
if (filters.status) {
query = query.where("status", "=", filters.status);
}
if (filters.dueAfter) {
query = query.where("dueDate", ">=", ["exactDay", filters.dueAfter]);
}
return await query.execute();
}
```
---
## Pagination
```typescript
export async function getPaginatedUsers(page: number, pageSize: number) {
const offset = (page - 1) * pageSize;
return await queryBuilder
.selectFrom("users")
.select(["id", "name", "email"])
.orderBy("createdAt", "desc")
.limit(pageSize)
.offset(offset)
.execute();
}
```
---
For more topics, see:
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices

View File

@ -1,68 +0,0 @@
# TaylorDB Attachments
Attachments are treated as **standard columns** and can be selected and written like other fields, using helper utilities for uploads.
This document covers:
- Selecting attachment fields
- Creating records with attachments
- Updating attachments
---
## Select Attachments
```typescript
// New Standard: Use regular .select() like any other field.
const expenses = await qb
.selectFrom("expenses")
.select(["id", "amount", "receipt"])
.execute();
```
---
## Create with Attachments
Use `qb.uploadAttachments` to upload files before inserting.
```typescript
await qb
.insertInto("customers")
.values({
firstName: "Jane",
lastName: "Doe",
avatar: await qb.uploadAttachments([
{ file: new Blob([""]), name: "test.png" },
]),
})
.execute();
```
---
## Update with Attachments
```typescript
await qb
.update("customers")
.set({
lastName: "Smith",
avatar: await qb.uploadAttachments([
{ file: new Blob([""]), name: "test.png" },
]),
})
.where("id", "=", 1)
.execute();
```
---
For more topics, see:
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices

View File

@ -1,187 +0,0 @@
# TaylorDB Basic Queries & Filtering
This document covers **core read operations** using the TaylorDB query builder:
- **Basic select queries**
- **Ordering**
- **Filtering with where clauses**
- **Date filters**
- **Array and select field filters**
- **Simple text search**
---
## Basic Queries
### Get All Records
```typescript
export async function getAllUsers() {
return await queryBuilder
.selectFrom("users")
.select(["id", "name", "email", "createdAt"])
.execute();
}
```
### Get All Records (All Fields)
```typescript
export async function getAllUsers() {
return await queryBuilder.selectFrom("users").execute();
}
```
### Get Single Record by ID
```typescript
export async function getUserById(id: number) {
return await queryBuilder
.selectFrom("users")
.where("id", "=", id)
.executeTakeFirst();
}
```
**Note**: `.executeTakeFirst()` returns a single record or `undefined`.
### Get Records with Ordering
```typescript
// Descending order (newest first)
export async function getRecentUsers() {
return await queryBuilder
.selectFrom("users")
.orderBy("createdAt", "desc")
.execute();
}
// Ascending order (oldest first)
export async function getOldestUsers() {
return await queryBuilder
.selectFrom("users")
.orderBy("createdAt", "asc")
.execute();
}
```
---
## Filtering & Conditions
### Basic Where Clauses
```typescript
// Exact match
.where("status", "=", "active")
// Not equal
.where("status", "!=", "deleted")
// Greater than / Less than
.where("age", ">", 18)
.where("age", ">=", 18)
.where("age", "<", 65)
.where("age", "<=", 65)
```
### Multiple Conditions (AND logic)
```typescript
export async function getActiveAdults() {
return await queryBuilder
.selectFrom("users")
.where("status", "=", "active")
.where("age", ">=", 18)
.execute();
}
```
### Date Filtering
```typescript
// Exact date
export async function getUsersForDate(date: string) {
return await queryBuilder
.selectFrom("users")
.where("createdAt", "=", ["exactDay", date])
.execute();
}
// Date range
export async function getUsersInRange(startDate: string, endDate: string) {
return await queryBuilder
.selectFrom("users")
.where("createdAt", ">=", ["exactDay", startDate])
.where("createdAt", "<=", ["exactDay", endDate])
.execute();
}
// Before/After a date
.where("dueDate", "<", ["exactDay", "2024-01-01"])
.where("startDate", ">", ["exactDay", "2024-12-31"])
```
### Array/Multi-Select Filtering
```typescript
// Check if array contains any of the values
export async function getUsersByTags(tags: string[]) {
return await queryBuilder
.selectFrom("users")
.where("tags", "hasAnyOf", tags)
.execute();
}
// Example: Get users tagged with "admin" OR "moderator"
const adminUsers = await getUsersByTags(["admin", "moderator"]);
```
### Select Field Filtering
#### Single Select
For single-select fields, the query builder now returns a single string value.
```typescript
export async function getUsersByRole(role: string) {
return await queryBuilder
.selectFrom("users")
.where("role", "=", role)
.execute();
}
```
#### Multi Select
For multi-select fields, the query builder returns and accepts multiple values.
```typescript
export async function getUsersByInterests(interests: string[]) {
return await queryBuilder
.selectFrom("users")
.where("interests", "hasAnyOf", interests)
.execute();
}
```
### Text Search (Contains)
```typescript
export async function searchUsersByName(query: string) {
return await queryBuilder
.selectFrom("users")
.where("name", "contains", query)
.execute();
}
```
---
For more topics, see:
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices

View File

@ -1,84 +0,0 @@
# TaylorDB Field Types & Enums
This document explains how TaylorDB field types map to TypeScript and how to handle them correctly:
- Field type reference
- Nullable fields
- Enums and options from generated types
---
## Field Type Reference
| TaylorDB Field Type | TypeScript Type | Insert Value | Query Value |
| ------------------- | ----------------------- | --------------------- | ---------------------------- |
| **Text** | `string` | `"Hello"` | `"Hello"` |
| **Number** | `number` | `42` | `42` |
| **Date** | `string` (ISO) | `"2024-01-15"` | `["exactDay", "2024-01-15"]` |
| **Checkbox** | `boolean` | `true` | `true` |
| **Single Select** | `string` | `"option"` | `"option"` |
| **Multi Select** | `string[]` | `["opt1", "opt2"]` | `["opt1", "opt2"]` |
| **Attachment** | `string[]` (File Paths) | `uploadAttachments()` | `"file-path"` |
| **Email** | `string` | `"user@example.com"` | `"user@example.com"` |
---
## Handling Nullable Fields
```typescript
export async function createUserSafe(data: {
name: string;
email?: string | null;
age?: number | null;
}) {
return await queryBuilder
.insertInto("users")
.values({
name: data.name,
email: data.email ?? "", // Default to empty string
age: data.age ?? 0, // Default to 0
})
.executeTakeFirst();
}
```
---
## Working with Enums
```typescript
// Import from generated types
import type { TaskStatusOptions } from "./types";
export async function createTask(data: {
title: string;
status: (typeof TaskStatusOptions)[number]; // e.g., "todo" | "in-progress" | "done"
}) {
return await queryBuilder
.insertInto("tasks")
.values({
title: data.title,
status: data.status,
})
.executeTakeFirst();
}
export async function getTasksByStatus(
status: (typeof TaskStatusOptions)[number],
) {
return await queryBuilder
.selectFrom("tasks")
.where("status", "=", status)
.execute();
}
```
---
For more topics, see:
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices

View File

@ -1,98 +0,0 @@
# TaylorDB Pitfalls & Best Practices
This document captures **common mistakes** and **recommended patterns** when using the TaylorDB query builder.
---
## Common Pitfalls
### ❌ Pitfall: Not Using exactDay for Dates
```typescript
// ❌ WRONG
.where("date", "=", "2024-01-15")
// ✅ CORRECT
.where("date", "=", ["exactDay", "2024-01-15"])
```
### ❌ Pitfall: Ignoring Nullable Fields
```typescript
// ❌ WRONG (assumes field is always present)
const user = await queryBuilder
.selectFrom("users")
.where("id", "=", 1)
.executeTakeFirst();
console.log(user.email); // Could be undefined!
// ✅ CORRECT
const user = await queryBuilder
.selectFrom("users")
.where("id", "=", 1)
.executeTakeFirst();
if (user && user.email) {
console.log(user.email);
}
```
### ❌ Pitfall: Using execute() for Single Record
```typescript
// ❌ WRONG (returns array)
const user = await queryBuilder
.selectFrom("users")
.where("id", "=", 1)
.execute();
console.log(user.name); // Error: user is an array!
// ✅ CORRECT
const user = await queryBuilder
.selectFrom("users")
.where("id", "=", 1)
.executeTakeFirst();
if (user) {
console.log(user.name);
}
```
### ❌ Pitfall: Not Handling Empty Arrays
```typescript
// ❌ WRONG (fails if users is empty)
const ages = users.map((u) => u.age);
const avg = ages.reduce((a, b) => a + b) / ages.length; // Division by zero!
// ✅ CORRECT
if (users.length === 0) {
return { average: null };
}
const ages = users
.map((u) => u.age)
.filter((a): a is number => a !== undefined);
const avg = ages.reduce((a, b) => a + b, 0) / ages.length;
```
---
## Best Practices
1. **Always handle `undefined` and `null`** when working with query results.
2. **Use TypeScript types** from `taylordb/types.ts` for type safety.
3. **Use `executeTakeFirst()`** when you expect a single record.
4. **Filter nullish values** before aggregations.
5. **Provide defaults** for optional fields.
6. **Use `["exactDay", date]`** format for date comparisons.
7. **Group related queries** in the same function file.
8. **Export functions**, not raw queries.
9. **Document complex queries** with JSDoc comments.
---
For more topics, see:
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums

View File

@ -1,78 +1,700 @@
# TaylorDB Query Builder Reference
This is the **entry point** for all TaylorDB query builder docs in this template.
The content has been split into smaller, focused files to make it easier for agents (and humans) to scan and reuse.
This document provides comprehensive examples of how to use the TaylorDB query builder for all common database operations.
---
## 📚 Topics
## 📚 Table of Contents
- **Basic Reads & Filtering**
See `TAYLORDB_BASIC_QUERIES.md` for:
- Basic `selectFrom` usage
- Ordering
- `where` clauses
- Date filters
- Array/select field filters
- Text search (`contains`)
- **Write Operations (Insert, Update, Delete)**
See `TAYLORDB_WRITE_OPERATIONS.md` for:
- Inserting records (including single-/multi-select fields)
- Updates (single/multiple fields, conditional updates)
- Bulk updates
- Deleting single/multiple records and conditional deletes
- **Advanced Patterns**
See `TAYLORDB_ADVANCED_PATTERNS.md` for:
- Manual aggregations
- Summation helpers
- Conditional query builders
- Pagination patterns
- **Field Types & Enums**
See `TAYLORDB_FIELD_TYPES.md` for:
- TaylorDB field type → TypeScript mappings
- Nullable field handling
- Using generated enum options (`...Options`) types
- **Attachments**
See `TAYLORDB_ATTACHMENTS.md` for:
- Selecting attachment fields
- Creating/updating records with attachments via `uploadAttachments`
- **Pitfalls & Best Practices**
See `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for:
- Common mistakes (e.g., forgetting `["exactDay", date]`, misusing `execute`)
- Recommended patterns for safe, type-accurate queries
1. [Setup & Configuration](#setup--configuration)
2. [Basic Queries](#basic-queries)
3. [Filtering & Conditions](#filtering--conditions)
4. [Inserting Data](#inserting-data)
5. [Updating Data](#updating-data)
6. [Deleting Data](#deleting-data)
7. [Advanced Patterns](#advanced-patterns)
8. [Field Type Handling](#field-type-handling)
9. [Common Pitfalls](#common-pitfalls)
---
## How Agents Should Use These Docs
## Setup & Configuration
1. **Start from your use case**
- Need a simple read? Open `TAYLORDB_BASIC_QUERIES.md`.
- Doing writes? Use `TAYLORDB_WRITE_OPERATIONS.md`.
- Need aggregations or pagination? Use `TAYLORDB_ADVANCED_PATTERNS.md`.
### Initialize Query Builder
2. **Combine with generated types**
Always cross-reference:
- `apps/server/taylordb/types.ts` (schema-derived types)
- `apps/server/taylordb/query-builder.ts` (project-specific query functions)
```typescript
import { createQueryBuilder } from "@taylordb/query-builder";
import type { TaylorDatabase } from "./types.js";
3. **Check pitfalls before finalizing**
Before shipping queries, skim `TAYLORDB_PITFALLS_BEST_PRACTICES.md` to avoid common errors.
export const queryBuilder = createQueryBuilder<TaylorDatabase>({
baseUrl: process.env.TAYLORDB_BASE_URL!,
baseId: process.env.TAYLORDB_SERVER_ID!,
apiKey: process.env.TAYLORDB_API_TOKEN!,
});
```
The type parameter `<TaylorDatabase>` provides full type safety based on your generated schema.
---
## Basic Queries
### Get All Records
```typescript
export async function getAllUsers() {
return await queryBuilder
.selectFrom("users")
.select(["id", "name", "email", "createdAt"])
.execute();
}
```
### Get All Records (All Fields)
```typescript
export async function getAllUsers() {
return await queryBuilder.selectFrom("users").execute();
}
```
### Get Single Record by ID
```typescript
export async function getUserById(id: number) {
return await queryBuilder
.selectFrom("users")
.where("id", "=", id)
.executeTakeFirst();
}
```
**Note**: `.executeTakeFirst()` returns a single record or `undefined`.
### Get Records with Ordering
```typescript
// Descending order (newest first)
export async function getRecentUsers() {
return await queryBuilder
.selectFrom("users")
.orderBy("createdAt", "desc")
.execute();
}
// Ascending order (oldest first)
export async function getOldestUsers() {
return await queryBuilder
.selectFrom("users")
.orderBy("createdAt", "asc")
.execute();
}
```
---
## Filtering & Conditions
### Basic Where Clauses
```typescript
// Exact match
.where("status", "=", "active")
// Not equal
.where("status", "!=", "deleted")
// Greater than / Less than
.where("age", ">", 18)
.where("age", ">=", 18)
.where("age", "<", 65)
.where("age", "<=", 65)
```
### Multiple Conditions (AND logic)
```typescript
export async function getActiveAdults() {
return await queryBuilder
.selectFrom("users")
.where("status", "=", "active")
.where("age", ">=", 18)
.execute();
}
```
### Date Filtering
```typescript
// Exact date
export async function getUsersForDate(date: string) {
return await queryBuilder
.selectFrom("users")
.where("createdAt", "=", ["exactDay", date])
.execute();
}
// Date range
export async function getUsersInRange(startDate: string, endDate: string) {
return await queryBuilder
.selectFrom("users")
.where("createdAt", ">=", ["exactDay", startDate])
.where("createdAt", "<=", ["exactDay", endDate])
.execute();
}
// Before/After a date
.where("dueDate", "<", ["exactDay", "2024-01-01"])
.where("startDate", ">", ["exactDay", "2024-12-31"])
```
### Array/Multi-Select Filtering
```typescript
// Check if array contains any of the values
export async function getUsersByTags(tags: string[]) {
return await queryBuilder
.selectFrom("users")
.where("tags", "hasAnyOf", tags)
.execute();
}
// Example: Get users tagged with "admin" OR "moderator"
const adminUsers = await getUsersByTags(["admin", "moderator"]);
```
### Single Select Filtering
```typescript
// For single-select fields (stored as arrays in TaylorDB)
export async function getUsersByRole(role: string) {
return await queryBuilder
.selectFrom("users")
.where("role", "=", role)
.execute();
}
```
### Text Search (Contains)
```typescript
export async function searchUsersByName(query: string) {
return await queryBuilder
.selectFrom("users")
.where("name", "contains", query)
.execute();
}
```
---
## Inserting Data
### Insert Single Record
```typescript
export async function createUser(data: {
name: string;
email: string;
age: number;
}) {
return await queryBuilder
.insertInto("users")
.values({
name: data.name,
email: data.email,
age: data.age,
status: "active", // Default value
})
.executeTakeFirst();
}
```
**Returns**: The created record with its generated `id`.
### Insert with Single-Select Field
```typescript
export async function createTask(data: {
title: string;
priority: "low" | "medium" | "high";
}) {
return await queryBuilder
.insertInto("tasks")
.values({
title: data.title,
priority: [data.priority], // Wrap in array for single-select
})
.executeTakeFirst();
}
```
### Insert with Multi-Select Field
```typescript
export async function createProject(data: { name: string; tags: string[] }) {
return await queryBuilder
.insertInto("projects")
.values({
name: data.name,
tags: data.tags, // Already an array
})
.executeTakeFirst();
}
```
### Insert with Computed Fields
```typescript
export async function createCardioSession(data: {
distance: number;
duration: number; // in minutes
}) {
const speed = data.distance / (data.duration / 60); // km/h
return await queryBuilder
.insertInto("cardio")
.values({
distance: data.distance,
duration: data.duration,
speed: speed, // Computed field
})
.executeTakeFirst();
}
```
### Insert with Optional Fields
```typescript
export async function createPost(data: {
title: string;
content: string;
tags?: string[];
}) {
return await queryBuilder
.insertInto("posts")
.values({
title: data.title,
content: data.content,
tags: data.tags || [], // Default to empty array
})
.executeTakeFirst();
}
```
---
## Updating Data
### Update Single Field
```typescript
export async function updateUserName(id: number, name: string) {
return await queryBuilder
.update("users")
.set({ name })
.where("id", "=", id)
.execute();
}
```
### Update Multiple Fields
```typescript
export async function updateUser(
id: number,
data: {
name?: string;
email?: string;
age?: number;
},
) {
return await queryBuilder
.update("users")
.set(data)
.where("id", "=", id)
.execute();
}
```
**Note**: Only provided fields will be updated.
### Update with Single-Select Field
```typescript
export async function updateTaskPriority(
id: number,
priority: "low" | "medium" | "high",
) {
return await queryBuilder
.update("tasks")
.set({ priority: [priority] }) // Wrap in array
.where("id", "=", id)
.execute();
}
```
### Update with Conditional Logic
```typescript
export async function updateCardioSession(
id: number,
data: {
distance?: number;
duration?: number;
},
) {
// Fetch current record to compute speed
const currentRecord = await queryBuilder
.selectFrom("cardio")
.select(["distance", "duration"])
.where("id", "=", id)
.executeTakeFirst();
if (!currentRecord) {
throw new Error("Record not found");
}
const newDistance = data.distance ?? currentRecord.distance ?? 0;
const newDuration = data.duration ?? currentRecord.duration ?? 0;
const speed = newDistance / (newDuration / 60);
return await queryBuilder
.update("cardio")
.set({
...data,
speed,
})
.where("id", "=", id)
.execute();
}
```
### Update Multiple Records
```typescript
export async function activateAllUsers() {
return await queryBuilder.update("users").set({ status: "active" }).execute(); // No where clause = update all
}
// Update with condition
export async function activateInactiveUsers() {
return await queryBuilder
.update("users")
.set({ status: "active" })
.where("status", "=", "inactive")
.execute();
}
```
---
## Deleting Data
### Delete Single Record
```typescript
export async function deleteUser(id: number) {
return await queryBuilder.deleteFrom("users").where("id", "=", id).execute();
}
```
### Delete Multiple Records by IDs
```typescript
export async function deleteUsers(ids: number[]) {
return await queryBuilder
.deleteFrom("users")
.where("id", "hasAnyOf", ids)
.execute();
}
```
### Delete with Condition
```typescript
export async function deleteInactiveUsers() {
return await queryBuilder
.deleteFrom("users")
.where("status", "=", "inactive")
.execute();
}
```
### Delete Old Records
```typescript
export async function deleteOldLogs(beforeDate: string) {
return await queryBuilder
.deleteFrom("logs")
.where("createdAt", "<", ["exactDay", beforeDate])
.execute();
}
```
---
## Advanced Patterns
### Aggregations (Manual)
Since TaylorDB query builder might not have built-in aggregations, compute manually:
```typescript
export async function getUserStats() {
const users = await queryBuilder
.selectFrom("users")
.select(["age"])
.execute();
if (users.length === 0) {
return { count: 0, average: null, min: null, max: null };
}
const ages = users
.map((u) => u.age)
.filter((a): a is number => a !== undefined);
return {
count: ages.length,
average: ages.reduce((a, b) => a + b, 0) / ages.length,
min: Math.min(...ages),
max: Math.max(...ages),
};
}
```
### Sum Totals
```typescript
export async function getTotalCaloriesForDate(date: string) {
const entries = await queryBuilder
.selectFrom("meals")
.select(["calories", "protein", "carbs", "fats"])
.where("date", "=", ["exactDay", date])
.execute();
return {
totalCalories: entries.reduce((sum, e) => sum + (e.calories ?? 0), 0),
totalProtein: entries.reduce((sum, e) => sum + (e.protein ?? 0), 0),
totalCarbs: entries.reduce((sum, e) => sum + (e.carbs ?? 0), 0),
totalFats: entries.reduce((sum, e) => sum + (e.fats ?? 0), 0),
};
}
```
### Conditional Queries
```typescript
export async function searchTasks(filters: {
projectId?: number;
status?: string;
dueAfter?: string;
}) {
let query = queryBuilder
.selectFrom("tasks")
.select(["id", "title", "status", "dueDate"]);
if (filters.projectId) {
query = query.where("projectId", "=", filters.projectId);
}
if (filters.status) {
query = query.where("status", "=", filters.status);
}
if (filters.dueAfter) {
query = query.where("dueDate", ">=", ["exactDay", filters.dueAfter]);
}
return await query.execute();
}
```
### Pagination
```typescript
export async function getPaginatedUsers(page: number, pageSize: number) {
const offset = (page - 1) * pageSize;
return await queryBuilder
.selectFrom("users")
.select(["id", "name", "email"])
.orderBy("createdAt", "desc")
.limit(pageSize)
.offset(offset)
.execute();
}
```
---
## Field Type Handling
### Field Type Reference
| TaylorDB Field Type | TypeScript Type | Insert Value | Query Value |
| ------------------- | --------------- | -------------------- | ---------------------------- |
| **Text** | `string` | `"Hello"` | `"Hello"` |
| **Number** | `number` | `42` | `42` |
| **Date** | `string` (ISO) | `"2024-01-15"` | `["exactDay", "2024-01-15"]` |
| **Checkbox** | `boolean` | `true` | `true` |
| **Single Select** | `string[]` | `["option"]` | `"option"` |
| **Multi Select** | `string[]` | `["opt1", "opt2"]` | `tags: ["opt1", "opt2"]` |
| **Email** | `string` | `"user@example.com"` | `"user@example.com"` |
### Handling Nullable Fields
```typescript
export async function createUserSafe(data: {
name: string;
email?: string | null;
age?: number | null;
}) {
return await queryBuilder
.insertInto("users")
.values({
name: data.name,
email: data.email ?? "", // Default to empty string
age: data.age ?? 0, // Default to 0
})
.executeTakeFirst();
}
```
### Working with Enums
```typescript
// Import from generated types
import type { TaskStatusOptions } from "./types";
export async function createTask(data: {
title: string;
status: (typeof TaskStatusOptions)[number]; // e.g., "todo" | "in-progress" | "done"
}) {
return await queryBuilder
.insertInto("tasks")
.values({
title: data.title,
status: [data.status], // Single select as array
})
.executeTakeFirst();
}
export async function getTasksByStatus(
status: (typeof TaskStatusOptions)[number],
) {
return await queryBuilder
.selectFrom("tasks")
.where("status", "=", status)
.execute();
}
```
---
## Common Pitfalls
### ❌ Pitfall 1: Not Wrapping Single-Select in Array
```typescript
// ❌ WRONG
.values({ priority: "high" })
// ✅ CORRECT
.values({ priority: ["high"] })
```
### ❌ Pitfall 2: Not Using exactDay for Dates
```typescript
// ❌ WRONG
.where("date", "=", "2024-01-15")
// ✅ CORRECT
.where("date", "=", ["exactDay", "2024-01-15"])
```
### ❌ Pitfall 3: Ignoring Nullable Fields
```typescript
// ❌ WRONG (assumes field is always present)
const user = await queryBuilder
.selectFrom("users")
.where("id", "=", 1)
.executeTakeFirst();
console.log(user.email); // Could be undefined!
// ✅ CORRECT
const user = await queryBuilder
.selectFrom("users")
.where("id", "=", 1)
.executeTakeFirst();
if (user && user.email) {
console.log(user.email);
}
```
### ❌ Pitfall 4: Using execute() for Single Record
```typescript
// ❌ WRONG (returns array)
const user = await queryBuilder
.selectFrom("users")
.where("id", "=", 1)
.execute();
console.log(user.name); // Error: user is an array!
// ✅ CORRECT
const user = await queryBuilder
.selectFrom("users")
.where("id", "=", 1)
.executeTakeFirst();
if (user) {
console.log(user.name);
}
```
### ❌ Pitfall 5: Not Handling Empty Arrays
```typescript
// ❌ WRONG (fails if users is empty)
const ages = users.map((u) => u.age);
const avg = ages.reduce((a, b) => a + b) / ages.length; // Division by zero!
// ✅ CORRECT
if (users.length === 0) {
return { average: null };
}
const ages = users
.map((u) => u.age)
.filter((a): a is number => a !== undefined);
const avg = ages.reduce((a, b) => a + b, 0) / ages.length;
```
---
## Best Practices
1. **Always handle `undefined` and `null`** when working with query results
2. **Use TypeScript types** from `taylordb/types.ts` for type safety
3. **Wrap single-select values** in arrays when inserting/updating
4. **Use `executeTakeFirst()`** when you expect a single record
5. **Filter nullish values** before aggregations
6. **Provide defaults** for optional fields
7. **Use `exactDay`** format for date comparisons
8. **Group related queries** in the same function file
9. **Export functions**, not raw queries
10. **Document complex queries** with JSDoc comments
---
## Additional Resources
- **Generated Types**: `apps/server/taylordb/types.ts`
- **Example Queries in This Template**: `apps/server/taylordb/query-builder.ts`
- **tRPC Integration**: `apps/server/router.ts`
- **Generated Types**: Check `apps/server/taylordb/types.ts` for your schema
- **Example Queries**: See `apps/server/taylordb/query-builder.ts`
- **tRPC Integration**: See `apps/server/router.ts`
---
**Note**: These docs mirror the TaylorDB query builder patterns used in this template.
For the most up-to-date API details, always refer to the official TaylorDB documentation.
**Note**: This reference is based on the TaylorDB query builder patterns used in this template. Always refer to the official TaylorDB documentation for the most up-to-date API details.

View File

@ -1,265 +0,0 @@
# TaylorDB Write Operations (Insert, Update, Delete)
This document covers **write operations** using the TaylorDB query builder:
- **Inserting data**
- **Updating single/multiple records**
- **Deleting records**
---
## Inserting Data
### Insert Single Record
```typescript
export async function createUser(data: {
name: string;
email: string;
age: number;
}) {
return await queryBuilder
.insertInto("users")
.values({
name: data.name,
email: data.email,
age: data.age,
status: "active", // Default value
})
.executeTakeFirst();
}
```
**Returns**: The created record with its generated `id`.
### Insert with Single-Select Field
Single-select fields now accept a single string value directly.
```typescript
export async function createTask(data: {
title: string;
priority: "low" | "medium" | "high";
}) {
return await queryBuilder
.insertInto("tasks")
.values({
title: data.title,
priority: data.priority,
})
.executeTakeFirst();
}
```
### Insert with Multi-Select Field
```typescript
export async function createProject(data: { name: string; tags: string[] }) {
return await queryBuilder
.insertInto("projects")
.values({
name: data.name,
tags: data.tags, // Already an array
})
.executeTakeFirst();
}
```
### Insert with Computed Fields
```typescript
export async function createCardioSession(data: {
distance: number;
duration: number; // in minutes
}) {
const speed = data.distance / (data.duration / 60); // km/h
return await queryBuilder
.insertInto("cardio")
.values({
distance: data.distance,
duration: data.duration,
speed: speed, // Computed field
})
.executeTakeFirst();
}
```
### Insert with Optional Fields
```typescript
export async function createPost(data: {
title: string;
content: string;
tags?: string[];
}) {
return await queryBuilder
.insertInto("posts")
.values({
title: data.title,
content: data.content,
tags: data.tags || [], // Default to empty array
})
.executeTakeFirst();
}
```
---
## Updating Data
### Update Single Field
```typescript
export async function updateUserName(id: number, name: string) {
return await queryBuilder
.update("users")
.set({ name })
.where("id", "=", id)
.execute();
}
```
### Update Multiple Fields
```typescript
export async function updateUser(
id: number,
data: {
name?: string;
email?: string;
age?: number;
},
) {
return await queryBuilder
.update("users")
.set(data)
.where("id", "=", id)
.execute();
}
```
**Note**: Only provided fields will be updated.
### Update with Single-Select Field
```typescript
export async function updateTaskPriority(
id: number,
priority: "low" | "medium" | "high",
) {
return await queryBuilder
.update("tasks")
.set({ priority })
.where("id", "=", id)
.execute();
}
```
### Update with Conditional Logic
```typescript
export async function updateCardioSession(
id: number,
data: {
distance?: number;
duration?: number;
},
) {
// Fetch current record to compute speed
const currentRecord = await queryBuilder
.selectFrom("cardio")
.select(["distance", "duration"])
.where("id", "=", id)
.executeTakeFirst();
if (!currentRecord) {
throw new Error("Record not found");
}
const newDistance = data.distance ?? currentRecord.distance ?? 0;
const newDuration = data.duration ?? currentRecord.duration ?? 0;
const speed = newDistance / (newDuration / 60);
return await queryBuilder
.update("cardio")
.set({
...data,
speed,
})
.where("id", "=", id)
.execute();
}
```
### Update Multiple Records
```typescript
export async function activateAllUsers() {
return await queryBuilder.update("users").set({ status: "active" }).execute(); // No where clause = update all
}
// Update with condition
export async function activateInactiveUsers() {
return await queryBuilder
.update("users")
.set({ status: "active" })
.where("status", "=", "inactive")
.execute();
}
```
---
## Deleting Data
### Delete Single Record
```typescript
export async function deleteUser(id: number) {
return await queryBuilder.deleteFrom("users").where("id", "=", id).execute();
}
```
### Delete Multiple Records by IDs
```typescript
export async function deleteUsers(ids: number[]) {
return await queryBuilder
.deleteFrom("users")
.where("id", "hasAnyOf", ids)
.execute();
}
```
### Delete with Condition
```typescript
export async function deleteInactiveUsers() {
return await queryBuilder
.deleteFrom("users")
.where("status", "=", "inactive")
.execute();
}
```
### Delete Old Records
```typescript
export async function deleteOldLogs(beforeDate: string) {
return await queryBuilder
.deleteFrom("logs")
.where("createdAt", "<", ["exactDay", beforeDate])
.execute();
}
```
---
For more topics, see:
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices

View File

@ -139,39 +139,27 @@ importers:
apps/server:
dependencies:
'@taylordb/query-builder':
specifier: ^0.11.4
version: 0.11.4
specifier: ^0.10.3
version: 0.10.3
'@trpc/server':
specifier: ^11.8.1
version: 11.8.1(typescript@5.9.3)
cookie-parser:
specifier: ^1.4.7
version: 1.4.7
cors:
specifier: ^2.8.5
version: 2.8.5
express:
specifier: ^5.2.1
version: 5.2.1
multer:
specifier: ^2.0.2
version: 2.0.2
zod:
specifier: ^4.3.5
version: 4.3.5
devDependencies:
'@types/cookie-parser':
specifier: ^1.4.10
version: 1.4.10(@types/express@5.0.6)
'@types/cors':
specifier: ^2.8.19
version: 2.8.19
'@types/express':
specifier: ^5.0.6
version: 5.0.6
'@types/multer':
specifier: ^2.0.0
version: 2.0.0
'@types/node':
specifier: ^24.10.1
version: 24.10.1
@ -1285,15 +1273,9 @@ packages:
'@taylordb/query-builder@0.10.3':
resolution: {integrity: sha512-GwsrSdCQelTvghPY4uzoSWHI2vsWFOFSOb4IKRS0qQT8Z6jG8VWMOSRwocvcMoQfOf2RqCDt9Wew1dK6FZNBHA==}
'@taylordb/query-builder@0.11.4':
resolution: {integrity: sha512-5hGFtxTKtRK76BUq08c95lqzYBcCTNRm1W04z1P8ufAVvSrr9Iuq1sPf8D7ayBJrAZ8KHThwvAR9w4n0vqiwYQ==}
'@taylordb/shared@0.4.4':
resolution: {integrity: sha512-Xykr4I26JapNLePkapBGjz15t9Ep1iLs30VbfCcc2z30x8Qy/2tm+sSa5LcacCP2EaxNVDp2UuYKhZ7kOWBLBQ==}
'@taylordb/shared@0.5.4':
resolution: {integrity: sha512-85tqzVQXsbx3rUAhpcdeA/BKHaHEFgpMxjmRAb12bD4U4ghGrcFw1eyaq+/YtoBfiWChPCruCK+d03e+rEMoiA==}
'@trpc/client@11.8.1':
resolution: {integrity: sha512-L/SJFGanr9xGABmuDoeXR4xAdHJmsXsiF9OuH+apecJ+8sUITzVT1EPeqp0ebqA6lBhEl5pPfg3rngVhi/h60Q==}
peerDependencies:
@ -1333,11 +1315,6 @@ packages:
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
'@types/cookie-parser@1.4.10':
resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==}
peerDependencies:
'@types/express': '*'
'@types/cors@2.8.19':
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
@ -1383,9 +1360,6 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/multer@2.0.0':
resolution: {integrity: sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==}
'@types/node@24.10.1':
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
@ -1502,9 +1476,6 @@ packages:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
append-field@1.0.0:
resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@ -1538,13 +1509,6 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
@ -1589,10 +1553,6 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
concat-stream@2.0.0:
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
engines: {'0': node >= 6.0}
concurrently@9.2.1:
resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==}
engines: {node: '>=18'}
@ -1609,13 +1569,6 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
cookie-parser@1.4.7:
resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==}
engines: {node: '>= 0.8.0'}
cookie-signature@1.0.6:
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
cookie-signature@1.2.2:
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
engines: {node: '>=6.6.0'}
@ -2171,10 +2124,6 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
media-typer@1.1.0:
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
engines: {node: '>= 0.8'}
@ -2191,18 +2140,10 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-db@1.54.0:
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
mime-types@3.0.2:
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
engines: {node: '>=18'}
@ -2214,20 +2155,9 @@ packages:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
mkdirp@0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
multer@2.0.2:
resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==}
engines: {node: '>= 10.16.0'}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@ -2402,10 +2332,6 @@ packages:
resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
engines: {node: '>=0.10.0'}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
recharts@3.5.0:
resolution: {integrity: sha512-jWqBtu8L3VICXWa3g/y+bKjL8DDHSRme7DHD/70LQ/Tk0di1h11Y0kKC0nPh6YJ2oaa0k6anIFNhg6SfzHWdEA==}
engines: {node: '>=18'}
@ -2455,9 +2381,6 @@ packages:
rxjs@7.8.2:
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
@ -2528,17 +2451,10 @@ packages:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
@ -2605,17 +2521,10 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
type-is@2.0.1:
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
engines: {node: '>= 0.6'}
typedarray@0.0.6:
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
typescript-eslint@8.47.0:
resolution: {integrity: sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -2669,9 +2578,6 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
@ -2751,10 +2657,6 @@ packages:
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
engines: {node: '>=0.4.0'}
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@ -3684,28 +3586,10 @@ snapshots:
- supports-color
- utf-8-validate
'@taylordb/query-builder@0.11.4':
dependencies:
'@taylordb/shared': 0.5.4
eventemitter3: 5.0.1
fast-json-patch: 3.1.1
json-to-graphql-query: 2.3.0
lodash: 4.17.21
socket.io-client: 4.8.3
zod: 4.3.5
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
'@taylordb/shared@0.4.4':
dependencies:
lodash: 4.17.21
'@taylordb/shared@0.5.4':
dependencies:
lodash: 4.17.21
'@trpc/client@11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3)':
dependencies:
'@trpc/server': 11.8.1(typescript@5.9.3)
@ -3754,10 +3638,6 @@ snapshots:
dependencies:
'@types/node': 24.10.1
'@types/cookie-parser@1.4.10(@types/express@5.0.6)':
dependencies:
'@types/express': 5.0.6
'@types/cors@2.8.19':
dependencies:
'@types/node': 24.10.1
@ -3805,10 +3685,6 @@ snapshots:
'@types/json-schema@7.0.15': {}
'@types/multer@2.0.0':
dependencies:
'@types/express': 5.0.6
'@types/node@24.10.1':
dependencies:
undici-types: 7.16.0
@ -3965,8 +3841,6 @@ snapshots:
dependencies:
color-convert: 2.0.1
append-field@1.0.0: {}
argparse@2.0.1: {}
aria-hidden@1.2.6:
@ -4012,12 +3886,6 @@ snapshots:
node-releases: 2.0.27
update-browserslist-db: 1.1.4(browserslist@4.28.0)
buffer-from@1.1.2: {}
busboy@1.6.0:
dependencies:
streamsearch: 1.1.0
bytes@3.1.2: {}
call-bind-apply-helpers@1.0.2:
@ -4059,13 +3927,6 @@ snapshots:
concat-map@0.0.1: {}
concat-stream@2.0.0:
dependencies:
buffer-from: 1.1.2
inherits: 2.0.4
readable-stream: 3.6.2
typedarray: 0.0.6
concurrently@9.2.1:
dependencies:
chalk: 4.1.2
@ -4081,13 +3942,6 @@ snapshots:
convert-source-map@2.0.0: {}
cookie-parser@1.4.7:
dependencies:
cookie: 0.7.2
cookie-signature: 1.0.6
cookie-signature@1.0.6: {}
cookie-signature@1.2.2: {}
cookie@0.7.2: {}
@ -4659,8 +4513,6 @@ snapshots:
math-intrinsics@1.1.0: {}
media-typer@0.3.0: {}
media-typer@1.1.0: {}
merge-descriptors@2.0.0: {}
@ -4672,14 +4524,8 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.1
mime-db@1.52.0: {}
mime-db@1.54.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
mime-types@3.0.2:
dependencies:
mime-db: 1.54.0
@ -4692,24 +4538,8 @@ snapshots:
dependencies:
brace-expansion: 2.0.2
minimist@1.2.8: {}
mkdirp@0.5.6:
dependencies:
minimist: 1.2.8
ms@2.1.3: {}
multer@2.0.2:
dependencies:
append-field: 1.0.0
busboy: 1.6.0
concat-stream: 2.0.0
mkdirp: 0.5.6
object-assign: 4.1.1
type-is: 1.6.18
xtend: 4.0.2
nanoid@3.3.11: {}
natural-compare@1.4.0: {}
@ -4854,12 +4684,6 @@ snapshots:
react@19.2.0: {}
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
recharts@3.5.0(@types/react@19.2.6)(eslint@9.39.1(jiti@2.6.1))(react-dom@19.2.0(react@19.2.0))(react-is@19.2.0)(react@19.2.0)(redux@5.0.1):
dependencies:
'@reduxjs/toolkit': 2.11.0(react-redux@9.2.0(@types/react@19.2.6)(react@19.2.0)(redux@5.0.1))(react@19.2.0)
@ -4944,8 +4768,6 @@ snapshots:
dependencies:
tslib: 2.8.1
safe-buffer@5.2.1: {}
safer-buffer@2.1.2: {}
scheduler@0.27.0: {}
@ -5039,18 +4861,12 @@ snapshots:
statuses@2.0.2: {}
streamsearch@1.1.0: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
@ -5105,19 +4921,12 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
type-is@1.6.18:
dependencies:
media-typer: 0.3.0
mime-types: 2.1.35
type-is@2.0.1:
dependencies:
content-type: 1.0.5
media-typer: 1.1.0
mime-types: 3.0.2
typedarray@0.0.6: {}
typescript-eslint@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
@ -5164,8 +4973,6 @@ snapshots:
dependencies:
react: 19.2.0
util-deprecate@1.0.2: {}
vary@1.1.2: {}
victory-vendor@37.3.6:
@ -5218,8 +5025,6 @@ snapshots:
xmlhttprequest-ssl@2.1.2: {}
xtend@4.0.2: {}
y18n@5.0.8: {}
yallist@3.1.1: {}

View File

@ -21,6 +21,7 @@ services:
env:
vars:
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
TAYLORDB_API_TOKEN: secrets.TAYLORDB_API_TOKEN
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
FRONTEND_URL: routing.client.url
@ -30,6 +31,7 @@ services:
env:
vars:
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
TAYLORDB_API_TOKEN: secrets.TAYLORDB_API_TOKEN
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
FRONTEND_URL: routing.client.url
@ -39,6 +41,7 @@ services:
env:
vars:
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
TAYLORDB_API_TOKEN: secrets.TAYLORDB_API_TOKEN
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
FRONTEND_URL: routing.client.url