Compare commits

...

12 Commits

Author SHA1 Message Date
26b00bcf61 improvements: dos & fileUpload example 2026-02-13 18:37:04 +03:00
Umar Adilov
62bd7b0a6c WIP 2026-02-13 18:40:09 +05:00
Umar Adilov
71df6a350d Added details on query builder access 2026-02-13 18:38:07 +05:00
Umar Adilov
f6c614016d Updated docs adopted single&multiple select & attachments 2026-02-13 18:03:54 +05:00
Umar Adilov
0050023af0 Merge remote-tracking branch 'origin/main' 2026-02-13 17:47:34 +05:00
Umar Adilov
90fc22e55c Fixed trpc type isses 2026-02-13 17:45:30 +05:00
Umar Adilov
1c33189fca
Merge pull request #1 from webbeetechnologies/develop
Develop
2026-02-13 15:34:21 +03:00
Umar Adilov
73a4cf2ca0 Updated query builder version 2026-02-13 16:45:53 +05:00
Umar Adilov
f9d49df4d6 Added app access token to cookies 2026-02-12 14:12:12 +05:00
Umar Adilov
474e99c253 Merge remote-tracking branch 'origin/main' 2026-01-20 23:34:39 +05:00
81a2e1f87d Merge branch 'main' of github.com:webbeetechnologies/taylordb-fullstack-template 2026-01-20 16:24:38 +03:00
091e06fead fix: build issue 2026-01-20 16:24:26 +03:00
27 changed files with 2336 additions and 1709 deletions

111
AGENTS.md
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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