Adopted template for general useage

This commit is contained in:
Umar Adilov 2026-01-09 02:02:12 +05:00
parent 8f6508e6de
commit 2b1257634c
12 changed files with 2756 additions and 2359 deletions

779
AGENTS.md
View File

@ -1,66 +1,767 @@
# OpenCode Agent Instructions for TaylorDB Blank Template
# AI Agent Instructions for TaylorDB Full-Stack Template
This document provides instructions for AI agents on how to use this blank template to build custom UIs for TaylorDB.
This template is designed for AI agents to build **modern, type-safe, full-stack applications** using TaylorDB as the data source. This template provides a complete monorepo setup with a React frontend (Vite + shadcn/ui) and a Node.js backend (tRPC + TaylorDB query builder).
## Understanding the TaylorDB Blank Template
---
This is a blank template designed for building custom user interfaces on top of a TaylorDB database. TaylorDB is a no-code application that allows users to build applications using a table-based data visualization.
## 🎯 Your Mission
The purpose of this template is to overcome the limitations of table-only views by enabling the creation of custom components, forms, and other UI elements. The goal is to have an AI agent (like you) build and integrate a custom UI with a user's TaylorDB database automatically.
Build production-ready, modern web applications (primarily dashboards and CRUD interfaces) that:
## AI Development Workflow
- Are **fully type-safe** from database to UI
- Use TaylorDB as the single source of truth for data
- Have **stunning, modern UI/UX** that wows users
- Follow best practices in architecture and code organization
Your primary task is to build a custom UI that interacts with the user's TaylorDB data. Follow these steps carefully:
---
### 1. Understand the Database Schema
## 📋 Development Workflow
Upon initialization, this template includes a `/src/lib` directory containing two critical files:
### **Phase 1: Understand Requirements & Design**
- `taylordb.client.ts`: This file contains the pre-configured TaylorDB client query builder instance. You will use this client to interact with the database.
- `taylordb.types.ts`: This file contains the TypeScript types generated from the user's TaylorDB database schema.
#### Step 1: Gather Requirements
**Your first action must be to read both of these files.** This will give you a complete understanding of the database structure, tables, and data types you will be working with.
Ask the user clarifying questions about:
> Note: If `src/lib/taylordb.client.ts` or `src/lib/taylordb.types.ts` are missing in the repo, pause and ask the user to provide/regenerate them. Do not proceed with mock data.
- What data they want to work with (understand the domain)
- Key features and user workflows
- Target users and use cases
- Any specific UI/UX preferences
### 2. Integrate Directly with TaylorDB
#### Step 2: Understand the Database Schema
You must use the provided TaylorDB client for all data operations. **Do not use mock data under any circumstances.** The UI you build should be fully functional and connected to the live database from the start.
**CRITICAL**: Always start by reading these files to understand the data model:
### 3. Type Checking and Validation
- `apps/server/taylordb/types.ts` - TypeScript types generated from TaylorDB schema
- `apps/server/taylordb/query-builder.ts` - Query builder patterns (review for examples)
The TaylorDB query builder is strongly typed. While writing code, you may encounter TypeScript errors related to your queries. To ensure your queries and data manipulations are type-safe and correct, you must run the build command:
> ⚠️ **IMPORTANT**: If these files don't exist, STOP and ask the user to generate them first. Never proceed with mock data.
#### Step 3: Design the Color Scheme & Visual Identity
Based on the requirements, decide on:
- **Primary color scheme** (use HSL values for flexibility)
- **Design aesthetic** (modern glassmorphism, gradients, minimalism, etc.)
- **Typography** (Google Fonts like Inter, Outfit, or Manrope)
- **Animation style** (subtle micro-interactions vs. bold animations)
Document your design decisions briefly before implementing.
---
### **Phase 2: Build the Foundation**
#### Step 1: Set Up Server-Side Data Layer
**File: `apps/server/taylordb/query-builder.ts`**
This file contains all database operations. Create type-safe CRUD functions for each table:
```typescript
import pkg from "@taylordb/query-builder";
const { createQueryBuilder } = pkg;
import type { TaylorDatabase } from "./types.js";
export const queryBuilder = createQueryBuilder<TaylorDatabase>({
baseUrl: process.env.TAYLORDB_BASE_URL!,
baseId: process.env.TAYLORDB_SERVER_ID!,
apiKey: process.env.TAYLORDB_API_TOKEN!,
});
// ============================================================================
// READ Operations
// ============================================================================
/**
* Get all records from a table
*/
export async function getAllItems() {
return await queryBuilder
.selectFrom("items")
.select(["id", "name", "status", "createdAt"])
.orderBy("createdAt", "desc")
.execute();
}
/**
* Get a single record by ID
*/
export async function getItemById(id: number) {
return await queryBuilder
.selectFrom("items")
.where("id", "=", id)
.executeTakeFirst();
}
// ============================================================================
// CREATE Operations
// ============================================================================
export async function createItem(data: { name: string; status: string }) {
return await queryBuilder.insertInto("items").values(data).executeTakeFirst();
}
// ============================================================================
// UPDATE Operations
// ============================================================================
export async function updateItem(
id: number,
data: { name?: string; status?: string }
) {
return await queryBuilder
.update("items")
.set(data)
.where("id", "=", id)
.execute();
}
// ============================================================================
// DELETE Operations
// ============================================================================
export async function deleteItem(id: number) {
return await queryBuilder.deleteFrom("items").where("id", "=", id).execute();
}
```
**Query Builder Patterns:**
- **Filtering**: `.where("field", "=", value)`, `.where("date", ">=", ["exactDay", "2024-01-01"])`
- **Select specific fields**: `.select(["id", "name", "status"])`
- **Ordering**: `.orderBy("createdAt", "desc")`
- **Single record**: `.executeTakeFirst()`
- **Multiple records**: `.execute()`
- **Array fields**: Use `[value]` for single-select enums
- **Date filters**: Use `["exactDay", date]` format
Organize functions by domain (e.g., all user-related functions together).
#### Step 2: Create tRPC API Router
**File: `apps/server/router.ts`**
Expose your database functions as type-safe tRPC procedures:
```typescript
import { z } from "zod";
import { router, publicProcedure } from "./trpc";
import * as db from "./taylordb/query-builder";
export const appRouter = router({
// Group by domain/feature
items: {
getAll: publicProcedure.query(async () => {
return await db.getAllItems();
}),
getById: publicProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
return await db.getItemById(input.id);
}),
create: publicProcedure
.input(
z.object({
name: z.string().min(1),
status: z.string(),
})
)
.mutation(async ({ input }) => {
return await db.createItem(input);
}),
update: publicProcedure
.input(
z.object({
id: z.number(),
name: z.string().optional(),
status: z.string().optional(),
})
)
.mutation(async ({ input }) => {
const { id, ...data } = input;
return await db.updateItem(id, data);
}),
delete: publicProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
return await db.deleteItem(input.id);
}),
},
});
export type AppRouter = typeof appRouter;
```
**Organization:**
- Group related procedures (e.g., `items`, `users`, `projects`)
- Use Zod for input validation
- Queries for reads, mutations for writes
- Export `AppRouter` type for frontend
---
### **Phase 3: Build the Frontend**
#### Step 1: Update Design System
**File: `apps/client/src/index.css`**
Update the design tokens based on your chosen color scheme:
```css
@layer base {
:root {
/* Update these HSL values for your color scheme */
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 262 83% 58%; /* Example: Purple */
--primary-foreground: 210 40% 98%;
--accent: 262 90% 95%;
--accent-foreground: 262 83% 58%;
/* ... customize all tokens ... */
--radius: 0.75rem; /* Border radius */
}
.dark {
/* Dark mode colors */
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 263 70% 65%;
/* ... */
}
}
```
#### Step 2: Create shadcn/ui Components
**Always use shadcn/ui components**. Install components as needed:
```bash
pnpm dlx shadcn@latest add <component-name>
```
**Available by default:**
- `button`, `card`, `input`, `label`, `textarea`, `select`, `tabs`, `alert`
**Common additions for dashboards:**
- `table`, `dialog`, `dropdown-menu`, `toast`, `sheet`, `form`, `badge`, `avatar`, `skeleton`
Install with: `pnpm dlx shadcn@latest add table dialog dropdown-menu toast sheet form badge avatar skeleton`
#### Step 3: Build Page Components
**File: `apps/client/src/pages/DashboardPage.tsx`**
Create feature-rich, modern pages:
```typescript
import { useState } from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { trpc } from "@/lib/trpc";
import { PlusIcon, Loader2 } from "lucide-react";
export default function DashboardPage() {
const [name, setName] = useState("");
const { data: items, isLoading, refetch } = trpc.items.getAll.useQuery();
const createMutation = trpc.items.create.useMutation({
onSuccess: () => {
refetch();
setName("");
},
});
const deleteMutation = trpc.items.delete.useMutation({
onSuccess: () => refetch(),
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (name) {
createMutation.mutate({ name, status: "active" });
}
};
return (
<div className="container mx-auto p-8 max-w-6xl">
<div className="mb-8">
<h1 className="text-4xl font-bold mb-2">Dashboard</h1>
<p className="text-muted-foreground">Manage your items</p>
</div>
<div className="grid gap-6">
{/* Create Form */}
<Card>
<CardHeader>
<CardTitle>Add New Item</CardTitle>
<CardDescription>
Create a new item in your database
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="flex gap-4">
<div className="flex-1">
<Label htmlFor="name">Item Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter item name..."
required
/>
</div>
<Button
type="submit"
disabled={createMutation.isPending}
className="mt-auto"
>
{createMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Adding...
</>
) : (
<>
<PlusIcon className="mr-2 h-4 w-4" />
Add Item
</>
)}
</Button>
</form>
</CardContent>
</Card>
{/* Items List */}
<Card>
<CardHeader>
<CardTitle>Your Items</CardTitle>
</CardHeader>
<CardContent>
{isLoading && (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)}
{items && items.length === 0 && (
<p className="text-center text-muted-foreground py-8">
No items yet. Create your first item above.
</p>
)}
<div className="space-y-3">
{items?.map((item) => (
<div
key={item.id}
className="flex items-center justify-between p-4 border rounded-lg hover:bg-accent transition-colors"
>
<div>
<p className="font-medium">{item.name}</p>
<p className="text-sm text-muted-foreground">
{item.status}
</p>
</div>
<Button
variant="destructive"
size="sm"
onClick={() =>
item.id && deleteMutation.mutate({ id: item.id })
}
disabled={deleteMutation.isPending}
>
Delete
</Button>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</div>
);
}
```
#### Step 4: Update Routing
**File: `apps/client/src/main.tsx`**
Add your new pages to the router:
```typescript
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import App from "./App";
import HomePage from "./pages/HomePage";
import DashboardPage from "./pages/DashboardPage";
const router = createBrowserRouter([
{
path: "/",
element: <App />,
children: [
{ index: true, element: <HomePage /> },
{ path: "dashboard", element: <DashboardPage /> },
],
},
]);
```
**File: `apps/client/src/App.tsx`**
Update navigation:
```typescript
const navItems = [
{ to: "/", label: "Home" },
{ to: "/dashboard", label: "Dashboard" },
];
```
---
### **Phase 4: Polish & Validate**
#### Step 1: Run Type Checking
**ALWAYS run this before considering your work complete:**
```bash
pnpm build
```
This command will compile the TypeScript code and report any type errors. You should use this to validate your work.
Fix all TypeScript errors. Never use `any` unless absolutely necessary.
### 4. Development Server
#### Step 2: Run Linter
The development server is already running. You do not need to start it. Focus on building the UI components and integrating them with the database.
```bash
pnpm lint
```
Fix all linting errors.
#### Step 3: Test in Browser
The dev server should be running. Test:
- All CRUD operations work correctly
- Data updates in real-time
- Error states display properly
- Loading states show appropriate feedback
- UI is responsive and looks great
---
## Build, Lint, and Test Commands
- **Start development**: `pnpm dev` or `npm run dev`
- **Build for production**: `pnpm build`
- **Lint all files**: `pnpm lint`
- **Tests**: _No test framework/config found; add [Vitest](https://vitest.dev) or [Jest](https://jestjs.io) before writing tests._
- **Run a single test**: _Not configured until you add a test runner._
## 🎨 Design Guidelines
## Code Style Guidelines
- Strictly type everything in TypeScript. **Never use `any`.**
- Use [ESLint](https://eslint.org/) with recommended JS, TypeScript, and React Hooks rules. Fix all lint errors.
- Use ES modules for imports (`import ... from ...`). Group external first, then internal.
- Naming: camelCase for variables/functions, PascalCase for components/types, UPPER_CASE for constants.
- Components: use function components, arrow function style preferred.
- Handle errors explicitly. **Never ignore TypeScript or lint errors.**
- Formatting: 2-space indent, single quotes, semicolons required.
- Remove unused code/comments. Comments must be concise and relevant.
- Use TaylorDB query builder with generated types from `../src/lib/taylordb.types.ts`; **never modify generated schema files.**
- shadcn/ui is initialized with common components (button, card, input, label, textarea, select, tabs, alert). Add more via `pnpm dlx shadcn@latest add <component>`; files go in `../src/components/ui/`.
- Prefer shadcn/ui components and Tailwind tokens for UI. Only hand-roll a component if shadcn does not offer an equivalent; follow shadcn structure (component in `../src/components/ui/`, styling via Tailwind classes, `cn` helper, and theme tokens defined in `../src/index.css`).
### Visual Excellence Principles
_No Cursor or Copilot rules found. If added, include their guidelines here._
1. **No Generic Colors**: Never use plain red/blue/green. Use curated HSL palettes.
- ✅ `hsl(262 83% 58%)` (vibrant purple)
- ❌ `#0000ff` (plain blue)
2. **Premium Aesthetics**: Make it feel high-end
- Use subtle gradients, shadows, and glassmorphism
- Add smooth transitions (`transition-all duration-200`)
- Implement hover states on interactive elements
- Use proper spacing and visual hierarchy
3. **Modern Typography**:
- Import Google Fonts (e.g., Inter, Outfit, Manrope)
- Use varied font weights (400, 500, 600, 700)
- Proper text sizing hierarchy
4. **Micro-Animations**:
- Loading spinners with `lucide-react` icons + `animate-spin`
- Fade-ins on data load
- Smooth transitions on hover
- Button press feedback
5. **Dashboard-First Design**:
- Card-based layouts
- Clear visual grouping
- Stats/metrics prominently displayed
- Intuitive navigation
### Component Structure
**Always follow this pattern:**
```typescript
// 1. Imports (external, then internal)
import { useState } from "react";
import { Card } from "@/components/ui/card";
import { trpc } from "@/lib/trpc";
// 2. Type definitions (if needed)
interface ItemFormProps {
onSuccess: () => void;
}
// 3. Component (arrow function)
export default function ItemForm({ onSuccess }: ItemFormProps) {
// 4. State & hooks
const [name, setName] = useState("");
const createMutation = trpc.items.create.useMutation({ onSuccess });
// 5. Event handlers
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
createMutation.mutate({ name });
};
// 6. Render
return <form onSubmit={handleSubmit}>{/* ... */}</form>;
}
```
---
## 📁 File Organization Best Practices
### Backend Structure
```
apps/server/
├── taylordb/
│ ├── types.ts # Generated types (DO NOT EDIT)
│ └── query-builder.ts # All database operations
├── router.ts # tRPC API routes
├── trpc.ts # tRPC configuration
└── index.ts # Server entry point
```
### Frontend Structure
```
apps/client/src/
├── components/
│ ├── ui/ # shadcn/ui components (auto-generated)
│ └── [custom]/ # Your custom components
├── pages/
│ └── [PageName].tsx # Route pages
├── lib/
│ ├── trpc.ts # tRPC client setup
│ └── utils.ts # Utilities (cn helper, etc.)
├── App.tsx # Layout + navigation
├── main.tsx # Router + app initialization
└── index.css # Global styles + design tokens
```
### Where to Put What
| What | Where |
| ---------------------- | -------------------------------------------- |
| Database queries | `apps/server/taylordb/query-builder.ts` |
| API endpoints | `apps/server/router.ts` |
| Route pages | `apps/client/src/pages/` |
| Reusable UI components | `apps/client/src/components/` |
| shadcn/ui components | `apps/client/src/components/ui/` (auto) |
| Design tokens | `apps/client/src/index.css` |
| TypeScript types | Use generated types from `taylordb/types.ts` |
---
## 🔧 TaylorDB Query Builder Reference
### Common Query Patterns
```typescript
// SELECT with specific fields
queryBuilder
.selectFrom("tableName")
.select(["id", "name", "status"])
.execute();
// WHERE conditions
.where("status", "=", "active")
.where("createdAt", ">=", ["exactDay", "2024-01-01"])
.where("tags", "hasAnyOf", ["tag1", "tag2"])
// ORDER BY
.orderBy("createdAt", "desc")
.orderBy("name", "asc")
// INSERT
queryBuilder
.insertInto("tableName")
.values({ name: "John", status: "active" })
.executeTakeFirst();
// UPDATE
queryBuilder
.update("tableName")
.set({ status: "inactive" })
.where("id", "=", 123)
.execute();
// DELETE
queryBuilder
.deleteFrom("tableName")
.where("id", "=", 123)
.execute();
// DELETE multiple
queryBuilder
.deleteFrom("tableName")
.where("id", "hasAnyOf", [1, 2, 3])
.execute();
```
### Field Type Handling
| TaylorDB Field Type | TypeScript Type | Query Value Format |
| ------------------- | --------------- | ---------------------------- |
| Text | `string` | `"value"` |
| Number | `number` | `42` |
| Date | `string` | `["exactDay", "2024-01-01"]` |
| Single Select | `string[]` | `["option"]` |
| Multi Select | `string[]` | `["opt1", "opt2"]` |
| Checkbox | `boolean` | `true` / `false` |
---
## ✅ Code Style Guidelines
### TypeScript
- **Never use `any`**. Use proper types from `taylordb/types.ts`
- Strict null checks: handle `null` and `undefined` explicitly
- Use type inference where obvious, explicit types for function params/returns
### Naming Conventions
- **Variables/Functions**: `camelCase` (e.g., `getUserData`)
- **Components**: `PascalCase` (e.g., `DashboardPage`)
- **Constants**: `UPPER_CASE` (e.g., `MAX_ITEMS`)
- **Files**: Match component name (e.g., `DashboardPage.tsx`)
### Imports
- Group external imports first, then internal
- Use path aliases: `@/components/...` not `../../components/...`
### Components
- Use arrow functions: `const MyComponent = () => { ... }`
- Props typing: `interface MyComponentProps { ... }`
- Keep components focused (single responsibility)
### Error Handling
- Display error states in UI
- Use tRPC's built-in error handling
- Show user-friendly messages
### Comments
- Use JSDoc for functions: `/** Description */`
- Explain "why", not "what"
- Remove commented-out code
---
## 🎓 Example: Building a Task Manager Dashboard
**User Request**: "Build a task manager with projects and tasks"
### 1. Analyze Schema
Assume TaylorDB has:
- `projects` table: `id`, `name`, `description`, `status`
- `tasks` table: `id`, `title`, `projectId`, `status`, `dueDate`
### 2. Design Decision
- **Color**: Gradient purple/blue theme
- **Style**: Modern with glassmorphism cards
- **Layout**: Projects on left sidebar, tasks on right
### 3. Backend (`apps/server/taylordb/query-builder.ts`)
```typescript
export async function getAllProjects() {
return await queryBuilder
.selectFrom("projects")
.select(["id", "name", "description", "status"])
.execute();
}
export async function getTasksByProject(projectId: number) {
return await queryBuilder
.selectFrom("tasks")
.where("projectId", "=", projectId)
.orderBy("dueDate", "asc")
.execute();
}
```
### 4. API (`apps/server/router.ts`)
```typescript
export const appRouter = router({
projects: {
getAll: publicProcedure.query(() => db.getAllProjects()),
},
tasks: {
getByProject: publicProcedure
.input(z.object({ projectId: z.number() }))
.query(({ input }) => db.getTasksByProject(input.projectId)),
},
});
```
### 5. Frontend (`apps/client/src/pages/TasksPage.tsx`)
Build the UI with cards, proper loading states, and type-safe tRPC calls.
---
## ⚠️ Critical Rules
1. **NEVER use mock data.** Always connect to real TaylorDB.
2. **NEVER ignore TypeScript errors.** Fix them before moving on.
3. **ALWAYS use shadcn/ui components** instead of hand-rolling UI.
4. **NEVER modify generated types** in `taylordb/types.ts`.
5. **ALWAYS run `pnpm build`** to validate your work.
6. **Design must be modern and premium**, not basic MVP.
---
## 🎯 Success Criteria
Your implementation is successful when:
- ✅ All TypeScript errors are resolved (`pnpm build` passes)
- ✅ All lint errors are fixed (`pnpm lint` passes)
- ✅ UI looks modern and premium (not basic/generic)
- ✅ All CRUD operations work correctly with TaylorDB
- ✅ Loading and error states are handled gracefully
- ✅ Code is well-organized and follows best practices
- ✅ Type safety is maintained from database to UI
---
**Remember**: You're building production-quality applications that should impress users from the first glance. Focus on visual excellence, type safety, and solid architecture.

404
README.md
View File

@ -1,305 +1,219 @@
# TaylorDB + tRPC Full-Stack Monorepo
# TaylorDB Full-Stack Template
A production-ready pnpm monorepo combining React frontend and Express backend with tRPC for type-safe API communication. Built for AI-assisted development platforms.
A production-ready template for building modern, type-safe web applications with TaylorDB. Designed for AI-assisted development with comprehensive documentation and best practices built-in.
## 📦 Monorepo Structure
## 🎯 What This Template Provides
```
taylordb-clientserver-template/
├── pnpm-workspace.yaml # Workspace configuration
├── package.json # @repo/frontend (root)
├── server/
│ ├── package.json # @repo/server
│ ├── index.ts # Express + tRPC server
│ ├── router.ts # tRPC procedures
│ ├── trpc.ts # tRPC configuration
│ └── taylordb/
│ ├── types.ts # Auto-generated TaylorDB types
│ └── query-builder.ts # TaylorDB CRUD operations
├── src/ # React frontend
└── ...
```
- **Full-Stack Setup**: React frontend + Node.js backend in a monorepo
- **Type Safety**: End-to-end TypeScript from database to UI
- **Modern UI**: shadcn/ui components with Tailwind CSS
- **Type-Safe API**: tRPC for seamless client-server communication
- **TaylorDB Integration**: Query builder with generated types
- **AI-Ready**: Comprehensive documentation for AI-assisted development
---
## 🚀 Quick Start
### 1. Install Dependencies
```bash
# Install all workspace dependencies
pnpm install
# Run both frontend and backend
pnpm dev:full
# Or run separately
pnpm dev # Frontend only (port 5173)
pnpm dev:server # Backend only (port 3001)
```
Visit:
---
- **Frontend**: http://localhost:5173
- **Backend**: http://localhost:3001
- **Demo Page**: http://localhost:5173/trpc-demo
## 📁 Project Structure
## 📚 Workspaces
### @repo/frontend (Root)
The React + Vite frontend application
**Location**: `/` (root directory)
**Tech Stack**:
- React 19 with TypeScript
- Vite 7 for bundling
- TailwindCSS 4 for styling
- shadcn/ui components
- React Router v6
- tRPC React Query client
**Scripts**:
```bash
pnpm dev # Start dev server
pnpm build # Build for production
pnpm lint # Run ESLint
```
taylordb-fullstack-template/
├── apps/
│ ├── client/ # React frontend (Vite)
│ │ ├── src/
│ │ │ ├── components/ui/ # shadcn/ui components
│ │ │ ├── pages/ # Route pages
│ │ │ ├── lib/ # Utilities & tRPC client
│ │ │ └── index.css # Design tokens
│ │ └── package.json
│ │
│ └── server/ # Node.js backend
│ ├── taylordb/
│ │ ├── types.ts # Generated schema types
│ │ └── query-builder.ts # Database operations
│ ├── router.ts # tRPC API routes
│ └── package.json
├── docs/ # Comprehensive guides
│ ├── TAYLORDB_QUERY_REFERENCE.md
│ └── SHADCN_COMPONENTS_GUIDE.md
├── AGENTS.md # AI agent instructions
└── taylordb.yml # Deployment config
```
### @repo/server
---
The Express + tRPC backend API
## 📚 Documentation
**Location**: `/server`
This template includes comprehensive documentation for both human and AI developers:
**Tech Stack**:
### For AI Agents
- Express 5 web server
- tRPC 11 for type-safe APIs
- TaylorDB query builder
- Zod for validation
- TypeScript 5
- **[AGENTS.md](./AGENTS.md)**: Complete AI agent instructions
- Development workflow (Planning → Execution → Verification)
- Design guidelines for modern UIs
- Code organization best practices
- Type safety patterns
- Example implementations
**Scripts**:
### For Developers
```bash
pnpm --filter @repo/server dev # Start with hot reload
pnpm --filter @repo/server build # Build TypeScript
pnpm --filter @repo/server start # Run production build
```
- **[docs/TAYLORDB_QUERY_REFERENCE.md](./docs/TAYLORDB_QUERY_REFERENCE.md)**: Query builder reference
## 🛠️ Available Scripts
- All CRUD operations with examples
- Field type handling
- Advanced patterns (aggregations, pagination)
- Common pitfalls and solutions
### Root Commands (run from anywhere)
- **[docs/SHADCN_COMPONENTS_GUIDE.md](./docs/SHADCN_COMPONENTS_GUIDE.md)**: UI component guide
- Dashboard patterns
- Form examples
- Data tables
- Responsive design tips
```bash
# Development
pnpm dev:full # Run both workspaces concurrently
pnpm dev # Frontend only
pnpm dev:server # Backend only
---
# Building
pnpm build # Build frontend
pnpm build:server # Build backend
pnpm build:all # Build both
## 🎨 Tech Stack
# Other
pnpm lint # Lint all code
pnpm generate:schema # Generate TaylorDB types
```
### Frontend
### Workspace-Specific Commands
- **React 19** with TypeScript
- **Vite 7** for fast builds
- **TailwindCSS 4** for styling
- **shadcn/ui** for UI components
- **React Router v6** for routing
- **tRPC React Query** for API calls
```bash
# Run command in specific workspace
pnpm --filter @repo/frontend <command>
pnpm --filter @repo/server <command>
### Backend
# Examples
pnpm --filter @repo/server dev
pnpm --filter @repo/frontend build
```
- **Node.js** with TypeScript
- **Express 5** web server
- **tRPC 11** for type-safe APIs
- **Zod** for validation
- **TaylorDB Query Builder** for database
## 🔧 Configuration Files
---
### pnpm-workspace.yaml
## 🎯 Key Features
Defines workspace packages:
```yaml
packages:
- "server"
- "."
```
### Package Names
- Frontend: `@repo/frontend`
- Backend: `@repo/server`
All packages are marked as `private: true` (not published to npm)
## 📡 API Communication
### tRPC Router Export
The backend exports its router type from `server/router.ts`:
### ✅ Full Type Safety
```typescript
export type AppRouter = typeof appRouter;
// Backend defines the API
export const appRouter = router({
users: {
getById: publicProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => { ... }),
},
});
// Frontend gets full autocomplete
const { data: user } = trpc.users.getById.useQuery({ id: 1 });
// ^? User | undefined (fully typed!)
```
### Frontend tRPC Client
### ✅ Modern UI Components
The frontend imports the type for end-to-end type safety:
All components from shadcn/ui with:
- Full dark mode support
- Responsive design
- Accessible by default
- Customizable with Tailwind
### ✅ Database Integration
Type-safe queries with TaylorDB:
```typescript
import type { AppRouter } from "../../server/router";
export const trpc = createTRPCReact<AppRouter>();
```
**Note**: This works because both packages are in the same monorepo, allowing direct TypeScript imports without shared packages.
## 🗄️ TaylorDB Integration
### Setup
1. Copy `.env.example` to `.env`
2. Add your TaylorDB credentials:
```bash
TAYLORDB_BASE_URL=your_base_url
TAYLORDB_BASE_ID=your_base_id
TAYLORDB_API_KEY=your_api_key
```
3. Generate types: `pnpm generate:schema`
### CRUD Operations
50+ type-safe procedures available:
- **Weight Tracking**: Full CRUD + statistics
- **Goals Management**: Create, read, update, delete
- **Strength Workouts**: Exercise logging
- **Cardio Exercises**: Duration, distance, speed tracking
- **Calories/Nutrition**: Daily totals and aggregations
- **Settings**: Key-value configuration
See [TAYLORDB_INTEGRATION.md](file:///Users/thetaung/Desktop/taylordb-clientserver-template/TAYLORDB_INTEGRATION.md) for detailed examples.
## 🏗️ Monorepo Benefits
### For This Project
**Shared Types**: Frontend and backend share TypeScript types directly
**Single Repository**: One git repo, one pnpm-lock.yaml
**Coordinated Builds**: Build both projects together
**Simplified Setup**: One `pnpm install` for everything
**Workspace Commands**: Target specific packages easily
### Compared to Separate Repos
- ⚡ Faster development (no publishing to npm for type updates)
- 🔒 Type safety guaranteed at development time
- 📝 Simpler dependency management
- 🔄 Atomic commits across frontend and backend
## 📦 Dependency Management
### Installing Dependencies
**For frontend**:
```bash
# From root
pnpm add <package>
# Or explicitly
pnpm --filter @repo/frontend add <package>
```
**For backend**:
```bash
pnpm --filter @repo/server add <package>
```
**For both**:
```bash
pnpm add <package> -w # Add to root workspace
```
### Workspace Dependencies
Workspaces can depend on each other using `workspace:*`:
```json
{
"dependencies": {
"@repo/shared": "workspace:*"
}
// Auto-generated types from your schema
export async function getAllUsers() {
return await queryBuilder
.selectFrom("users")
.select(["id", "name", "email"])
.execute();
}
```
---
## 🤖 AI-Assisted Development
This template is optimized for AI-assisted development:
1. **Read AGENTS.md**: Comprehensive instructions for AI agents
2. **Follow the workflow**: Planning → Execution → Verification
3. **Use type safety**: All examples use strict TypeScript
4. **Reference docs**: Query patterns, component examples, best practices
The AI agent will:
- Understand your TaylorDB schema
- Design appropriate color schemes
- Build type-safe CRUD operations
- Create modern, responsive UIs
- Follow best practices automatically
---
## 🚢 Deployment
### Option 1: Deploy Separately
This template is designed to deploy to TaylorDB's platform using the included `taylordb.yml` configuration.
- Frontend → Vercel/Netlify (static site from `dist/`)
- Backend → Railway/Render/Fly.io (Node.js app)
Build commands:
```bash
pnpm build # Frontend
pnpm build:server # Backend
```
### Option 2: Deploy Together
Serve frontend from backend:
```bash
pnpm build:all # Build both
# Configure Express to serve frontend static files
```
### Environment Variables
Set these in your deployment platform:
**Environment Variables Required:**
- `TAYLORDB_BASE_URL`
- `TAYLORDB_BASE_ID`
- `TAYLORDB_API_KEY`
- `FRONTEND_URL` (for CORS)
- `VITE_TRPC_URL` (frontend)
- `TAYLORDB_API_TOKEN`
- `TAYLORDB_SERVER_ID`
## 🎯 Next Steps
---
1. **Add Shared Package** (optional):
## 📖 Usage Examples
### 1. Add a New Feature
1. Create database functions in `apps/server/taylordb/query-builder.ts`
2. Expose via tRPC in `apps/server/router.ts`
3. Build UI in `apps/client/src/pages/`
4. Add route in `apps/client/src/main.tsx`
### 2. Add shadcn/ui Component
```bash
mkdir packages/shared
# Create package.json for shared utilities/types
pnpm dlx shadcn@latest add <component-name>
```
2. **Add Tests**:
Example:
```bash
pnpm --filter @repo/server add -D vitest
pnpm --filter @repo/frontend add -D vitest
pnpm dlx shadcn@latest add table dialog toast
```
3. **CI/CD**:
- Use `pnpm install --frozen-lockfile` in CI
- Run `pnpm build:all` for deployment
- Cache `node_modules` and `.pnpm-store`
### 3. Customize Design
## 📚 Learn More
Edit `apps/client/src/index.css` to change colors, fonts, and spacing.
- [pnpm Workspaces](https://pnpm.io/workspaces)
- [tRPC Documentation](https://trpc.io)
- [TaylorDB](https://taylordb.io)
- [Monorepo Handbook](https://monorepo.tools)
---
## 🔗 Resources
- **shadcn/ui**: https://ui.shadcn.com/
- **tRPC**: https://trpc.io/
- **TaylorDB**: https://taylordb.ai/
- **Tailwind CSS**: https://tailwindcss.com/
---
## 📄 License
@ -307,4 +221,4 @@ MIT - Use freely for any project!
---
**Built with ❤️ for AI-assisted development platforms**
**Built for modern, type-safe full-stack development with AI assistance** ✨

View File

@ -1,234 +0,0 @@
# TaylorDB Integration Summary
## Created Files
### [server/taylordb/query-builder.ts](file:///Users/thetaung/Desktop/taylordb-clientserver-template/server/taylordb/query-builder.ts)
Comprehensive CRUD operations for all TaylorDB tables with **40+ functions**:
#### Weight Tracking
- ✅ `getAllWeightRecords()` - Get all weight entries
- ✅ `getWeightRecordsByDateRange()` - Filter by date range
- ✅ `getWeightRecordById()` - Get single record
- ✅ `createWeightRecord()` - Add new weight entry
- ✅ `updateWeightRecord()` - Update existing record
- ✅ `deleteWeightRecord()` - Delete single record
- ✅ `deleteWeightRecords()` - Batch delete
- ✅ `getWeightStats()` - Get aggregated statistics
#### Goals Management
- ✅ `getAllGoals()` - List all goals
- ✅ `createGoal()` - Create new goal
- ✅ `updateGoal()` - Update goal
- ✅ `deleteGoal()` - Delete goal
#### Strength Training
- ✅ `getStrengthWorkouts()` - Get workouts with optional filtering
- ✅ `createStrengthWorkout()` - Log workout
- ✅ `updateStrengthWorkout()` - Update workout
- ✅ `deleteStrengthWorkout()` - Delete workout
#### Cardio Exercise
- ✅ `getCardioExercises()` - Get all cardio sessions
- ✅ `createCardioExercise()` - Log cardio session (auto-calculates speed)
- ✅ `updateCardioExercise()` - Update session
- ✅ `deleteCardioExercise()` - Delete session
#### Calories/Nutrition
- ✅ `getCaloriesByTimeOfDay()` - Filter by meal time
- ✅ `createCalorieEntry()` - Log meal/calories
- ✅ `updateCalorieEntry()` - Update nutrition data
- ✅ `deleteCalorieEntry()` - Delete entry
- ✅ `getTotalCaloriesForDate()` - Daily nutrition aggregation
#### Settings
- ✅ `getSettingByName()` - Get setting by name
- ✅ `createSetting()` - Create/update setting
- ✅ `updateSetting()` - Modify setting
- ✅ `deleteSetting()` - Remove setting
---
### [server/router.ts](file:///Users/thetaung/Desktop/taylordb-clientserver-template/server/router.ts)
Updated tRPC router with **50+ type-safe procedures** organized by feature:
```typescript
appRouter = {
hello: { ... }, // Test procedure
weight: {
getAll: query,
getById: query,
getByDateRange: query,
create: mutation,
update: mutation,
delete: mutation,
deleteMultiple: mutation,
getStats: query,
},
goals: {
getAll: query,
create: mutation,
update: mutation,
delete: mutation,
},
strength: {
getAll: query,
create: mutation,
update: mutation,
delete: mutation,
},
cardio: {
getAll: query,
create: mutation,
update: mutation,
delete: mutation,
},
calories: {
getByTimeOfDay: query,
create: mutation,
update: mutation,
delete: mutation,
getTotalForDate: query,
},
settings: {
getByName: query,
create: mutation,
update: mutation,
delete: mutation,
},
}
```
**All procedures include**:
- ✅ Zod validation
- ✅ Type safety
- ✅ Error handling
- ✅ Proper TypeScript types
---
### [.env.example](file:///Users/thetaung/Desktop/taylordb-clientserver-template/.env.example)
Environment configuration template with all required variables.
---
## Usage Examples
### Frontend (React Component)
```typescript
import { trpc } from "@/lib/trpc";
function WeightTracker() {
// Query all weights
const { data: weights } = trpc.weight.getAll.useQuery();
// Create mutation
const createWeight = trpc.weight.create.useMutation({
onSuccess: () => {
// Refetch or invalidate queries
},
});
// Add new weight entry
const handleSubmit = () => {
createWeight.mutate({
date: "2026-01-08",
weight: 75.5,
name: "Morning weight",
});
};
return (
<div>
{weights?.map((w) => (
<div key={w.id}>
{w.date}: {w.weight}kg
</div>
))}
</div>
);
}
```
### Backend (Direct Query Builder Usage)
```typescript
import * as db from "./taylordb/query-builder";
// Get workouts for specific exercise
const pushups = await db.getStrengthWorkouts("Push-ups");
// Create cardio session with auto-calculated speed
await db.createCardioExercise({
exercise: "Running",
duration: 30, // minutes
distance: 5, // km
date: "2026-01-08",
// speed is automatically calculated: 5km / 0.5hr = 10km/h
});
// Get nutrition totals for today
const nutrition = await db.getTotalCaloriesForDate("2026-01-08");
console.log(nutrition.totalCalories.sum); // e.g., 2000
```
---
## Key Features
### Type Safety
- Full end-to-end type inference
- TypeScript autocomplete for all operations
- Zod runtime validation
### Query Builder Features
- **Filtering**: Date ranges, text search, numeric comparisons
- **Aggregations**: Sum, average, min/max, statistics
- **Batch Operations**: Delete multiple records
- **Auto-calculations**: Speed calculation for cardio
### Organization
- Grouped by domain (weight, goals, strength, etc.)
- Clear naming conventions
- Comprehensive documentation
---
## Next Steps
1. **Copy `.env.example` to `.env`** and add your TaylorDB credentials
2. **Generate types**: Run `pnpm generate:schema` to create TaylorDB types
3. **Test endpoints**: Use the tRPC demo page or create your own
4. **Add authentication**: Protect procedures as needed
5. **Customize**: Add more procedures for your specific use cases
---
## TypeScript Notes
The type errors you see are expected until you:
1. Set up TaylorDB credentials in `.env`
2. Run `pnpm generate:schema` to generate proper types
3. Restart the TypeScript server
The code is ready to use - the types will align once TaylorDB is properly configured!

View File

@ -8,7 +8,6 @@ import { cn } from "@/lib/utils";
const navItems = [
{ to: "/", label: "Home" },
{ to: "/about", label: "About" },
{ to: "/trpc-demo", label: "tRPC Demo" },
];
const getInitialTheme = (): "light" | "dark" => {

View File

@ -1,47 +1,86 @@
import { ExternalLink } from "lucide-react";
import { Link } from "react-router-dom";
import { Button } from "@/components/ui/button";
const AboutPage = () => {
return (
<section className="space-y-4">
<section className="space-y-6">
<div className="space-y-2">
<h1 className="text-2xl font-semibold">About this starter</h1>
<h1 className="text-2xl font-semibold">About This Template</h1>
<p className="text-muted-foreground">
This project now ships with React Router v6, Tailwind CSS, and a
shadcn/ui component baseline so you can focus on building TaylorDB
experiences instead of wiring up UI plumbing.
This is a full-stack template for building modern web applications
with TaylorDB. It includes React + Vite frontend, Node.js + tRPC
backend, and shadcn/ui components.
</p>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="rounded-lg border bg-card p-4 shadow-sm">
<h2 className="text-lg font-medium">Routing</h2>
<p className="text-sm text-muted-foreground">
Use nested routes with layouts via <code>createBrowserRouter</code>{" "}
and <code>Outlet</code>. Add pages in <code>src/pages</code> and
register them in the router.
<h2 className="text-lg font-medium mb-2">Type-Safe APIs</h2>
<p className="text-sm text-muted-foreground mb-3">
Full end-to-end type safety from database to UI using tRPC and
TypeScript. Auto-generated types from your TaylorDB schema.
</p>
<Button variant="outline" size="sm" asChild>
<Link to="/trpc-demo">View Demo</Link>
</Button>
</div>
<div className="rounded-lg border bg-card p-4 shadow-sm">
<h2 className="text-lg font-medium">UI primitives</h2>
<p className="text-sm text-muted-foreground">
The shared <code>Button</code> uses shadcn/ui patterns and can be
extended with more components via the CLI using{" "}
<code>components.json</code>.
<h2 className="text-lg font-medium mb-2">Modern UI</h2>
<p className="text-sm text-muted-foreground mb-3">
Built with shadcn/ui and Tailwind CSS. Responsive, accessible, and
customizable components with dark mode support.
</p>
<Button variant="outline" size="sm" asChild>
<a
href="https://ui.shadcn.com/docs"
target="_blank"
rel="noreferrer"
>
View Components <ExternalLink className="ml-1 h-3 w-3" />
</a>
</Button>
</div>
</div>
<Button variant="link" asChild>
<div className="space-y-3">
<h2 className="text-lg font-medium">Documentation</h2>
<ul className="space-y-2 text-sm text-muted-foreground">
<li>
📘 <code className="font-mono">AGENTS.md</code> - AI agent
instructions and development workflow
</li>
<li>
📗{" "}
<code className="font-mono">docs/TAYLORDB_QUERY_REFERENCE.md</code>{" "}
- Complete query builder examples
</li>
<li>
📙{" "}
<code className="font-mono">docs/SHADCN_COMPONENTS_GUIDE.md</code> -
UI component patterns for dashboards
</li>
</ul>
</div>
<div className="flex gap-3">
<Button variant="outline" asChild>
<a
href="https://www.npmjs.com/package/@taylordb/query-builder"
target="_blank"
rel="noreferrer"
>
Explore TaylorDB Query Builder <ExternalLink className="h-4 w-4" />
TaylorDB Query Builder <ExternalLink className="ml-1 h-4 w-4" />
</a>
</Button>
<Button variant="outline" asChild>
<a href="https://trpc.io" target="_blank" rel="noreferrer">
tRPC Docs <ExternalLink className="ml-1 h-4 w-4" />
</a>
</Button>
</div>
</section>
);
};

View File

@ -10,594 +10,119 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { trpc } from "@/lib/trpc";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { InfoIcon, Loader2 } from "lucide-react";
export default function TRPCDemoPage() {
return (
<div className="container mx-auto p-8 max-w-6xl">
<h1 className="text-4xl font-bold mb-2">TaylorDB + tRPC Demo</h1>
<p className="text-muted-foreground mb-8">
Full-stack CRUD operations with type safety
<div className="container mx-auto p-8 max-w-4xl">
<div className="mb-8">
<h1 className="text-4xl font-bold mb-2">tRPC + TaylorDB Demo</h1>
<p className="text-muted-foreground">
Example of type-safe API calls with tRPC
</p>
<Tabs defaultValue="weight" className="w-full">
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="weight">Weight</TabsTrigger>
<TabsTrigger value="goals">Goals</TabsTrigger>
<TabsTrigger value="strength">Strength</TabsTrigger>
<TabsTrigger value="cardio">Cardio</TabsTrigger>
<TabsTrigger value="calories">Calories</TabsTrigger>
</TabsList>
<TabsContent value="weight">
<WeightTracker />
</TabsContent>
<TabsContent value="goals">
<GoalsManager />
</TabsContent>
<TabsContent value="strength">
<StrengthWorkouts />
</TabsContent>
<TabsContent value="cardio">
<CardioExercises />
</TabsContent>
<TabsContent value="calories">
<CaloriesTracker />
</TabsContent>
</Tabs>
</div>
);
}
// ============================================================================
// Weight Tracker Component
// ============================================================================
function WeightTracker() {
const [weight, setWeight] = useState("");
const [date, setDate] = useState(new Date().toISOString().split("T")[0]);
const { data: weights, isLoading, refetch } = trpc.weight.getAll.useQuery();
const { data: stats } = trpc.weight.getStats.useQuery();
const createMutation = trpc.weight.create.useMutation({
onSuccess: () => {
refetch();
setWeight("");
},
});
const deleteMutation = trpc.weight.delete.useMutation({
onSuccess: () => refetch(),
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (weight) {
createMutation.mutate({
date,
weight: parseFloat(weight),
});
}
};
return (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>Weight Statistics</CardTitle>
<CardDescription>Your weight tracking overview</CardDescription>
</CardHeader>
<CardContent>
{stats && (
<div className="grid grid-cols-4 gap-4">
<div className="text-center">
<p className="text-2xl font-bold">{stats.count}</p>
<p className="text-sm text-muted-foreground">Total Entries</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold">
{stats.average?.toFixed(1) || "-"}
</p>
<p className="text-sm text-muted-foreground">Average (kg)</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold">
{stats.min?.toFixed(1) || "-"}
</p>
<p className="text-sm text-muted-foreground">Min (kg)</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold">
{stats.max?.toFixed(1) || "-"}
</p>
<p className="text-sm text-muted-foreground">Max (kg)</p>
</div>
</div>
)}
</CardContent>
</Card>
{/* Info Alert */}
<Alert>
<InfoIcon className="h-4 w-4" />
<AlertTitle>Template Example</AlertTitle>
<AlertDescription>
This page demonstrates a simple tRPC query. Replace this with your
own queries based on your TaylorDB schema. See{" "}
<code className="font-mono text-sm">apps/server/router.ts</code> to
add more procedures.
</AlertDescription>
</Alert>
<Card>
<CardHeader>
<CardTitle>Add Weight Entry</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="date">Date</Label>
<Input
id="date"
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
required
/>
{/* Example: Hello Query */}
<HelloExample />
</div>
<div>
<Label htmlFor="weight">Weight (kg)</Label>
<Input
id="weight"
type="number"
step="0.1"
value={weight}
onChange={(e) => setWeight(e.target.value)}
placeholder="75.5"
required
/>
</div>
</div>
<Button type="submit" disabled={createMutation.isPending}>
{createMutation.isPending ? "Adding..." : "Add Entry"}
</Button>
{createMutation.error && (
<p className="text-sm text-destructive">
{createMutation.error.message}
</p>
)}
</form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Weight History</CardTitle>
</CardHeader>
<CardContent>
{isLoading && <p>Loading...</p>}
{weights && weights.length === 0 && (
<p className="text-muted-foreground">No entries yet</p>
)}
<div className="space-y-2">
{weights?.map((w) => (
<div
key={w.id}
className="flex items-center justify-between p-3 border rounded-lg"
>
<div>
<p className="font-medium">{w.weight} kg</p>
<p className="text-sm text-muted-foreground">{w.date}</p>
</div>
<Button
variant="destructive"
size="sm"
onClick={() => w.id && deleteMutation.mutate({ id: w.id })}
disabled={deleteMutation.isPending}
>
Delete
</Button>
</div>
))}
</div>
</CardContent>
</Card>
</div>
);
}
// ============================================================================
// Goals Manager Component
// Example Component: Simple Query
// ============================================================================
function GoalsManager() {
function HelloExample() {
const [name, setName] = useState("");
const [value, setValue] = useState("");
const [description, setDescription] = useState("");
const { data, isLoading, refetch } = trpc.hello.useQuery(
{ name: name || undefined },
{ enabled: false } // Only run when user clicks button
);
const { data: goals, isLoading, refetch } = trpc.goals.getAll.useQuery();
const createMutation = trpc.goals.create.useMutation({
onSuccess: () => {
const handleQuery = () => {
refetch();
setName("");
setValue("");
setDescription("");
},
});
const deleteMutation = trpc.goals.delete.useMutation({
onSuccess: () => refetch(),
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
createMutation.mutate({ name, value, description });
};
return (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>Create Goal</CardTitle>
<CardTitle>Example: Hello Query</CardTitle>
<CardDescription>
A simple tRPC query to test the connection
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="goalName">Goal Name</Label>
<CardContent className="space-y-4">
<div className="grid gap-2">
<Label htmlFor="name">Name (optional)</Label>
<Input
id="goalName"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Lose 5kg"
required
placeholder="Enter your name..."
/>
</div>
<div>
<Label htmlFor="goalValue">Target Value</Label>
<Input
id="goalValue"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="70kg"
required
/>
</div>
<div>
<Label htmlFor="goalDescription">Description (optional)</Label>
<Input
id="goalDescription"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="By end of Q1"
/>
</div>
<Button type="submit" disabled={createMutation.isPending}>
{createMutation.isPending ? "Creating..." : "Create Goal"}
</Button>
</form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>My Goals</CardTitle>
</CardHeader>
<CardContent>
{isLoading && <p>Loading...</p>}
{goals && goals.length === 0 && (
<p className="text-muted-foreground">No goals yet</p>
<Button onClick={handleQuery} disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Loading...
</>
) : (
"Send Query"
)}
<div className="space-y-3">
{goals?.map((goal) => (
<div key={goal.id} className="border rounded-lg p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="font-semibold">{goal.name}</h3>
<p className="text-lg text-primary">{goal.value}</p>
{goal.description && (
<p className="text-sm text-muted-foreground mt-1">
{goal.description}
</p>
)}
</div>
<Button
variant="ghost"
size="sm"
onClick={() =>
goal.id && deleteMutation.mutate({ id: goal.id })
}
>
Delete
</Button>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
);
}
// ============================================================================
// Strength Workouts Component
// ============================================================================
function StrengthWorkouts() {
const [exercise, setExercise] = useState("Push-ups");
const [reps, setReps] = useState("");
const [weight, setWeight] = useState("");
const [date, setDate] = useState(new Date().toISOString().split("T")[0]);
const {
data: workouts,
isLoading,
refetch,
} = trpc.strength.getAll.useQuery();
const createMutation = trpc.strength.create.useMutation({
onSuccess: () => {
refetch();
setReps("");
setWeight("");
},
});
const deleteMutation = trpc.strength.delete.useMutation({
onSuccess: () => refetch(),
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
createMutation.mutate({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
exercise: exercise as any,
reps: parseInt(reps),
weight: parseFloat(weight),
date,
});
};
return (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>Log Strength Workout</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="exercise">Exercise</Label>
<select
id="exercise"
className="w-full p-2 border rounded-md"
value={exercise}
onChange={(e) => setExercise(e.target.value)}
>
<option>Push-ups</option>
<option>Pull-ups</option>
<option>Squats</option>
<option>Bench Press</option>
<option>Deadlifts</option>
</select>
</div>
<div>
<Label htmlFor="workoutDate">Date</Label>
<Input
id="workoutDate"
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="reps">Reps</Label>
<Input
id="reps"
type="number"
value={reps}
onChange={(e) => setReps(e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="workoutWeight">Weight (kg)</Label>
<Input
id="workoutWeight"
type="number"
step="0.5"
value={weight}
onChange={(e) => setWeight(e.target.value)}
required
/>
</div>
</div>
<Button type="submit" disabled={createMutation.isPending}>
Log Workout
</Button>
</form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Workout History</CardTitle>
</CardHeader>
<CardContent>
{isLoading && <p>Loading...</p>}
<div className="space-y-2">
{workouts?.map((w) => (
<div
key={w.id}
className="flex items-center justify-between p-3 border rounded-lg"
>
<div>
<p className="font-medium">{w.exercise?.[0] || "N/A"}</p>
<p className="text-sm text-muted-foreground">
{w.reps} reps × {w.weight}kg {w.date}
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => w.id && deleteMutation.mutate({ id: w.id })}
>
Delete
</Button>
</div>
))}
</div>
</CardContent>
</Card>
</div>
);
}
// ============================================================================
// Cardio Exercises Component
// ============================================================================
function CardioExercises() {
const [exercise, setExercise] = useState("Running");
const [duration, setDuration] = useState("");
const [distance, setDistance] = useState("");
const [date, setDate] = useState(new Date().toISOString().split("T")[0]);
const { data: exercises, isLoading, refetch } = trpc.cardio.getAll.useQuery();
const createMutation = trpc.cardio.create.useMutation({
onSuccess: () => {
refetch();
setDuration("");
setDistance("");
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
createMutation.mutate({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
exercise: exercise as any,
duration: parseFloat(duration),
distance: parseFloat(distance),
date,
});
};
return (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>Log Cardio</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Exercise</Label>
<select
className="w-full p-2 border rounded-md"
value={exercise}
onChange={(e) => setExercise(e.target.value)}
>
<option>Running</option>
<option>Cycling</option>
<option>Swimming</option>
</select>
</div>
<div>
<Label>Date</Label>
<Input
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Duration (min)</Label>
<Input
type="number"
value={duration}
onChange={(e) => setDuration(e.target.value)}
required
/>
</div>
<div>
<Label>Distance (km)</Label>
<Input
type="number"
step="0.1"
value={distance}
onChange={(e) => setDistance(e.target.value)}
required
/>
</div>
</div>
<Button type="submit">Log Exercise</Button>
</form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Cardio History</CardTitle>
</CardHeader>
<CardContent>
{isLoading && <p>Loading...</p>}
<div className="space-y-2">
{exercises?.map((e) => (
<div key={e.id} className="p-3 border rounded-lg">
<p className="font-medium">{e.exercise?.[0] || "N/A"}</p>
<p className="text-sm text-muted-foreground">
{e.distance}km in {e.duration}min {e.speed?.toFixed(1)} km/h
{e.date}
</p>
</div>
))}
</div>
</CardContent>
</Card>
</div>
);
}
// ============================================================================
// Calories Tracker Component
// ============================================================================
function CaloriesTracker() {
const [date] = useState(new Date().toISOString().split("T")[0]);
const { data: totals } = trpc.calories.getTotalForDate.useQuery({ date });
return (
<Card>
<CardHeader>
<CardTitle>Daily Nutrition</CardTitle>
<CardDescription>{date}</CardDescription>
</CardHeader>
<CardContent>
{totals && (
<div className="grid grid-cols-4 gap-4">
<div className="text-center p-4 border rounded-lg">
<p className="text-3xl font-bold">{totals.totalCalories}</p>
<p className="text-sm text-muted-foreground">Calories</p>
</div>
<div className="text-center p-4 border rounded-lg">
<p className="text-3xl font-bold">{totals.totalProtein}g</p>
<p className="text-sm text-muted-foreground">Protein</p>
</div>
<div className="text-center p-4 border rounded-lg">
<p className="text-3xl font-bold">{totals.totalCarbs}g</p>
<p className="text-sm text-muted-foreground">Carbs</p>
</div>
<div className="text-center p-4 border rounded-lg">
<p className="text-3xl font-bold">{totals.totalFats}g</p>
<p className="text-sm text-muted-foreground">Fats</p>
</div>
{data && (
<div className="mt-4 p-4 border rounded-lg bg-muted/50">
<p className="font-medium text-sm mb-2">Response:</p>
<pre className="text-sm">{JSON.stringify(data, null, 2)}</pre>
</div>
)}
</CardContent>
</Card>
);
}
/**
* ============================================================================
* Add Your Own Components Here
* ============================================================================
*
* Follow this pattern to create your own tRPC queries and mutations:
*
* 1. Create procedures in apps/server/router.ts
* 2. Use trpc.<procedure>.useQuery() for queries (reading data)
* 3. Use trpc.<procedure>.useMutation() for mutations (writing data)
* 4. Handle loading and error states
* 5. Refetch queries after mutations to update the UI
*
* Example Query:
* const { data, isLoading, error } = trpc.items.getAll.useQuery();
*
* Example Mutation:
* const createMutation = trpc.items.create.useMutation({
* onSuccess: () => {
* // Refetch or invalidate queries
* },
* });
*
* For comprehensive examples, see:
* - docs/SHADCN_COMPONENTS_GUIDE.md - UI component patterns
* - AGENTS.md - Complete development workflow
*/

View File

@ -1,354 +1,126 @@
import { z } from "zod";
import { router, publicProcedure } from "./trpc";
import * as db from "./taylordb/query-builder";
import {
StrengthExerciseOptions,
CaloriesTimeOfDayOptions,
CardioExerciseOptions,
CaloriesUnitOptions,
} from "./taylordb/types";
// import * as db from "./taylordb/query-builder";
/**
* Main tRPC router with TaylorDB integration
* All procedures are type-safe and use the TaylorDB query builder
* Main tRPC Router
*
* This is your main API router. Define all your procedures here.
* Group related procedures together for better organization.
*
* Example structure:
*
* export const appRouter = router({
* users: {
* getAll: publicProcedure.query(async () => { ... }),
* getById: publicProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => { ... }),
* create: publicProcedure.input(z.object({ ... })).mutation(async ({ input }) => { ... }),
* update: publicProcedure.input(z.object({ ... })).mutation(async ({ input }) => { ... }),
* delete: publicProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => { ... }),
* },
* posts: {
* // ...
* },
* });
*/
export const appRouter = router({
// ============================================================================
// Example / Test Procedures
// ============================================================================
hello: publicProcedure
.input(z.object({ name: z.string().optional() }).optional())
.input(
z
.object({
name: z.string().optional(),
})
.optional()
)
.query(({ input }) => {
return {
message: `Hello ${input?.name || "from tRPC"}!`,
timestamp: new Date().toISOString(),
};
}),
health: publicProcedure.query(() => {
return {
status: "ok",
message: `Hello ${input?.name ?? "World"}!`,
timestamp: new Date().toISOString(),
uptime: process.uptime(),
};
}),
// ============================================================================
// Weight Tracking
// Your API Procedures
// ============================================================================
weight: {
getAll: publicProcedure.query(async () => {
return await db.getAllWeightRecords();
}),
getById: publicProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
return await db.getWeightRecordById(input.id);
}),
getByDateRange: publicProcedure
.input(
z.object({
startDate: z.string(),
endDate: z.string(),
})
)
.query(async ({ input }) => {
return await db.getWeightRecordsByDateRange(
input.startDate,
input.endDate
);
}),
create: publicProcedure
.input(
z.object({
date: z.string(),
weight: z.number().positive(),
name: z.string().optional(),
})
)
.mutation(async ({ input }) => {
return await db.createWeightRecord(input);
}),
update: publicProcedure
.input(
z.object({
id: z.number(),
weight: z.number().positive().optional(),
date: z.string().optional(),
name: z.string().optional(),
})
)
.mutation(async ({ input }) => {
const { id, ...data } = input;
return await db.updateWeightRecord(id, data);
}),
delete: publicProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
return await db.deleteWeightRecord(input.id);
}),
deleteMultiple: publicProcedure
.input(z.object({ ids: z.array(z.number()) }))
.mutation(async ({ input }) => {
return await db.deleteWeightRecords(input.ids);
}),
getStats: publicProcedure.query(async () => {
return await db.getWeightStats();
}),
},
// ============================================================================
// Goals Management
// ============================================================================
goals: {
getAll: publicProcedure.query(async () => {
return await db.getAllGoals();
}),
create: publicProcedure
.input(
z.object({
name: z.string().min(1),
value: z.string().min(1),
description: z.string().optional(),
})
)
.mutation(async ({ input }) => {
return await db.createGoal(input);
}),
update: publicProcedure
.input(
z.object({
id: z.number(),
name: z.string().optional(),
value: z.string().optional(),
description: z.string().optional(),
})
)
.mutation(async ({ input }) => {
const { id, ...data } = input;
return await db.updateGoal(id, data);
}),
delete: publicProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
return await db.deleteGoal(input.id);
}),
},
// ============================================================================
// Strength Training
// ============================================================================
strength: {
getAll: publicProcedure
.input(
z
.object({ exercise: z.enum(StrengthExerciseOptions).optional() })
.optional()
)
.query(async ({ input }) => {
return await db.getStrengthWorkouts(input?.exercise);
}),
create: publicProcedure
.input(
z.object({
exercise: z.enum(StrengthExerciseOptions),
reps: z.number().positive(),
weight: z.number().positive(),
date: z.string(),
name: z.string().optional(),
})
)
.mutation(async ({ input }) => {
return await db.createStrengthWorkout(input);
}),
update: publicProcedure
.input(
z.object({
id: z.number(),
exercise: z.enum(StrengthExerciseOptions).optional(),
reps: z.number().positive().optional(),
weight: z.number().positive().optional(),
date: z.string().optional(),
name: z.string().optional(),
})
)
.mutation(async ({ input }) => {
const { id, ...data } = input;
return await db.updateStrengthWorkout(id, data);
}),
delete: publicProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
return await db.deleteStrengthWorkout(input.id);
}),
},
// ============================================================================
// Cardio Exercise
// ============================================================================
cardio: {
getAll: publicProcedure.query(async () => {
return await db.getCardioExercises();
}),
create: publicProcedure
.input(
z.object({
exercise: z.enum(CardioExerciseOptions),
duration: z.number().positive(),
distance: z.number().positive(),
date: z.string(),
name: z.string().optional(),
})
)
.mutation(async ({ input }) => {
return await db.createCardioExercise(input);
}),
update: publicProcedure
.input(
z.object({
id: z.number(),
exercise: z.enum(CardioExerciseOptions).optional(),
duration: z.number().positive().optional(),
distance: z.number().positive().optional(),
date: z.string().optional(),
name: z.string().optional(),
})
)
.mutation(async ({ input }) => {
const { id, ...data } = input;
return await db.updateCardioExercise(id, data);
}),
delete: publicProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
return await db.deleteCardioExercise(input.id);
}),
},
// ============================================================================
// Calories/Nutrition Tracking
// ============================================================================
calories: {
getByTimeOfDay: publicProcedure
.input(z.object({ timeOfDay: z.enum(CaloriesTimeOfDayOptions) }))
.query(async ({ input }) => {
return await db.getCaloriesByTimeOfDay(input.timeOfDay);
}),
create: publicProcedure
.input(
z.object({
date: z.string(),
timeOfDay: z.enum(CaloriesTimeOfDayOptions),
mealName: z.string(),
mealIngredient: z.string(),
quantity: z.number().positive(),
unit: z.enum(CaloriesUnitOptions),
totalCalories: z.number(),
totalProtein: z.number(),
totalCarbs: z.number(),
totalFats: z.number(),
})
)
.mutation(async ({ input }) => {
return await db.createCalorieEntry(input);
}),
update: publicProcedure
.input(
z.object({
id: z.number(),
totalCalories: z.number().optional(),
totalProtein: z.number().optional(),
totalCarbs: z.number().optional(),
totalFats: z.number().optional(),
quantity: z.number().positive().optional(),
mealName: z.string().optional(),
})
)
.mutation(async ({ input }) => {
const { id, ...data } = input;
return await db.updateCalorieEntry(id, data);
}),
delete: publicProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
return await db.deleteCalorieEntry(input.id);
}),
getTotalForDate: publicProcedure
.input(z.object({ date: z.string() }))
.query(async ({ input }) => {
return await db.getTotalCaloriesForDate(input.date);
}),
},
// ============================================================================
// Settings
// ============================================================================
settings: {
getByName: publicProcedure
.input(z.object({ name: z.string() }))
.query(async ({ input }) => {
return await db.getSettingByName(input.name);
}),
create: publicProcedure
.input(
z.object({
name: z.string().min(1),
value: z.string(),
description: z.string().optional(),
})
)
.mutation(async ({ input }) => {
return await db.createSetting(input);
}),
update: publicProcedure
.input(
z.object({
id: z.number(),
value: z.string().optional(),
description: z.string().optional(),
})
)
.mutation(async ({ input }) => {
const { id, ...data } = input;
return await db.updateSetting(id, data);
}),
delete: publicProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
return await db.deleteSetting(input.id);
}),
},
//
// Add your procedures here following this pattern:
//
// tableName: {
// getAll: publicProcedure.query(async () => {
// return await db.getAllRecords();
// }),
//
// getById: publicProcedure
// .input(z.object({ id: z.number() }))
// .query(async ({ input }) => {
// return await db.getRecordById(input.id);
// }),
//
// create: publicProcedure
// .input(z.object({
// name: z.string().min(1),
// status: z.string()
// }))
// .mutation(async ({ input }) => {
// return await db.createRecord(input);
// }),
//
// update: publicProcedure
// .input(z.object({
// id: z.number(),
// name: z.string().optional(),
// status: z.string().optional()
// }))
// .mutation(async ({ input }) => {
// const { id, ...data } = input;
// return await db.updateRecord(id, data);
// }),
//
// delete: publicProcedure
// .input(z.object({ id: z.number() }))
// .mutation(async ({ input }) => {
// return await db.deleteRecord(input.id);
// }),
// },
});
// Export type definition of API
export type AppRouter = typeof appRouter;
/**
* ============================================================================
* tRPC Quick Reference
* ============================================================================
*
* QUERIES (for reading data):
* - Use .query() for operations that don't modify data
* - Example: getAll, getById, search, etc.
*
* MUTATIONS (for writing data):
* - Use .mutation() for operations that create, update, or delete data
* - Example: create, update, delete
*
* INPUT VALIDATION:
* - Use .input() with Zod schemas to validate input
* - Example: .input(z.object({ id: z.number(), name: z.string() }))
*
* ACCESSING INPUT:
* - Access validated input via { input } destructuring
* - Example: .query(async ({ input }) => { ... })
*
* ORGANIZATION:
* - Group related procedures under a namespace
* - Example: users.getAll, users.create, posts.getAll, etc.
*
* ERROR HANDLING:
* - Throw errors from procedures, tRPC will handle them
* - Example: throw new Error("User not found");
*
* For comprehensive examples, see the example implementation or docs.
*/

View File

@ -1,13 +1,13 @@
import pkg from "@taylordb/query-builder";
const { createQueryBuilder } = pkg;
import type {
StrengthExerciseOptions,
CaloriesTimeOfDayOptions,
CardioExerciseOptions,
CaloriesUnitOptions,
} from "./types.js";
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!,
@ -15,8 +15,14 @@ export const queryBuilder = createQueryBuilder<TaylorDatabase>({
});
/**
* Sample CRUD Operations for TaylorDB
* These demonstrate how to interact with the database using the query builder
* ============================================================================
* 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
*/
// ============================================================================
@ -24,511 +30,291 @@ export const queryBuilder = createQueryBuilder<TaylorDatabase>({
// ============================================================================
/**
* Get all weight records
* 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();
* }
*/
export async function getAllWeightRecords() {
return await queryBuilder
.selectFrom("weight")
.select(["id", "date", "weight", "name", "createdAt", "updatedAt"])
.orderBy("date", "desc")
.execute();
}
/**
* Get weight records for a specific date range
* Example: Get a single record by ID
*
* @example
* export async function getUserById(id: number) {
* return await queryBuilder
* .selectFrom("users")
* .where("id", "=", id)
* .executeTakeFirst();
* }
*/
export async function getWeightRecordsByDateRange(
startDate: string,
endDate: string
) {
return await queryBuilder
.selectFrom("weight")
.where("date", ">=", ["exactDay", startDate])
.where("date", "<=", ["exactDay", endDate])
.orderBy("date", "asc")
.execute();
}
/**
* Get a single weight record by ID
* Example: Get records with filtering
*
* @example
* export async function getActiveUsers() {
* return await queryBuilder
* .selectFrom("users")
* .where("status", "=", "active")
* .orderBy("name", "asc")
* .execute();
* }
*/
export async function getWeightRecordById(id: number) {
return await queryBuilder
.selectFrom("weight")
.where("id", "=", id)
.executeTakeFirst();
}
/**
* Get all goals
* 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();
* }
*/
export async function getAllGoals() {
return await queryBuilder
.selectFrom("goals")
.select(["id", "name", "value", "description", "createdAt", "updatedAt"])
.execute();
}
/**
* Get strength workout records with optional filtering by exercise
*/
export async function getStrengthWorkouts(
exercise?: (typeof StrengthExerciseOptions)[number]
) {
let query = queryBuilder
.selectFrom("strength")
.select(["id", "date", "exercise", "reps", "weight", "name"])
.orderBy("date", "desc");
if (exercise) {
query = query.where("exercise", "=", exercise);
}
return await query.execute();
}
/**
* Get cardio exercises
*/
export async function getCardioExercises() {
return await queryBuilder
.selectFrom("cardio")
.select(["id", "date", "exercise", "duration", "distance", "speed", "name"])
.orderBy("date", "desc")
.execute();
}
/**
* Get calories by time of day
*/
export async function getCaloriesByTimeOfDay(
timeOfDay: (typeof CaloriesTimeOfDayOptions)[number]
) {
return await queryBuilder
.selectFrom("calories")
.select([
"id",
"date",
"timeOfDay",
"mealName",
"totalCalories",
"totalProtein",
"totalCarbs",
"totalFats",
])
.where("timeOfDay", "=", timeOfDay)
.execute();
}
/**
* Get settings by name
*/
export async function getSettingByName(name: string) {
return await queryBuilder
.selectFrom("settings")
.where("name", "=", name)
.execute();
}
// ============================================================================
// CREATE Operations (Insert)
// ============================================================================
/**
* Add a new weight record
* 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();
* }
*/
export async function createWeightRecord(data: {
date: string;
weight: number;
name?: string;
}) {
return await queryBuilder
.insertInto("weight")
.values({
date: data.date,
weight: data.weight,
name: data.name || "",
})
.executeTakeFirst();
}
/**
* Create a new goal
* 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();
* }
*/
export async function createGoal(data: {
name: string;
value: string;
description?: string;
}) {
return await queryBuilder
.insertInto("goals")
.values({
name: data.name,
value: data.value,
description: data.description || "",
})
.executeTakeFirst();
}
/**
* Log a strength workout
* 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();
* }
*/
export async function createStrengthWorkout(data: {
exercise: (typeof StrengthExerciseOptions)[number];
reps: number;
weight: number;
date: string;
name?: string;
}) {
return await queryBuilder
.insertInto("strength")
.values({
exercise: [data.exercise],
reps: data.reps,
weight: data.weight,
date: data.date,
name: data.name || "",
})
.executeTakeFirst();
}
/**
* Log a cardio exercise
*/
export async function createCardioExercise(data: {
exercise: (typeof CardioExerciseOptions)[number];
duration: number;
distance: number;
date: string;
name?: string;
}) {
const speed = data.distance / (data.duration / 60); // Calculate speed (km/h)
return await queryBuilder
.insertInto("cardio")
.values({
exercise: [data.exercise],
duration: data.duration,
distance: data.distance,
speed,
date: data.date,
name: data.name || "",
})
.executeTakeFirst();
}
/**
* Log calories/meal
*/
export async function createCalorieEntry(data: {
date: string;
timeOfDay: (typeof CaloriesTimeOfDayOptions)[number];
mealName: string;
mealIngredient: string;
quantity: number;
unit: (typeof CaloriesUnitOptions)[number];
totalCalories: number;
totalProtein: number;
totalCarbs: number;
totalFats: number;
}) {
return await queryBuilder
.insertInto("calories")
.values({
date: data.date,
timeOfDay: [data.timeOfDay],
mealName: data.mealName,
mealIngredient: data.mealIngredient,
quantity: data.quantity,
unit: [data.unit],
totalCalories: data.totalCalories,
totalProtein: data.totalProtein,
totalCarbs: data.totalCarbs,
totalFats: data.totalFats,
name: "",
proteinPer100G: 0,
carbsPer100G: 0,
fatsPer100G: 0,
quantityInGramsmL: 0,
quantityInFlOzozlb: 0,
})
.executeTakeFirst();
}
/**
* Create or update a setting
*/
export async function createSetting(data: {
name: string;
value: string;
description?: string;
}) {
return await queryBuilder
.insertInto("settings")
.values({
name: data.name,
value: data.value,
description: data.description || "",
})
.executeTakeFirst();
}
// ============================================================================
// UPDATE Operations
// ============================================================================
/**
* Update a weight record
* 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();
* }
*/
export async function updateWeightRecord(
id: number,
data: {
weight?: number;
date?: string;
name?: string;
}
) {
return await queryBuilder
.update("weight")
.set(data)
.where("id", "=", id)
.execute();
}
/**
* Update a goal
* 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();
* }
*/
export async function updateGoal(
id: number,
data: {
name?: string;
value?: string;
description?: string;
}
) {
return await queryBuilder
.update("goals")
.set(data)
.where("id", "=", id)
.execute();
}
/**
* Update a strength workout
*/
export async function updateStrengthWorkout(
id: number,
data: {
reps?: number;
weight?: number;
exercise?: (typeof StrengthExerciseOptions)[number];
date?: string;
name?: string;
}
) {
const updateData: Record<string, string | number | string[] | undefined> = {
...data,
exercise: data.exercise ? [data.exercise] : undefined,
};
return await queryBuilder
.update("strength")
.set(updateData)
.where("id", "=", id)
.execute();
}
/**
* Update a cardio exercise
*/
export async function updateCardioExercise(
id: number,
data: {
duration?: number;
distance?: number;
exercise?: (typeof CardioExerciseOptions)[number];
date?: string;
name?: string;
}
) {
const updateData: Record<string, string | number | string[] | undefined> = {
...data,
exercise: data.exercise ? [data.exercise] : undefined,
};
// Recalculate speed if duration or distance changed
if (data.duration || data.distance) {
const record = await queryBuilder
.selectFrom("cardio")
.select(["duration", "distance"])
.where("id", "=", id)
.executeTakeFirst();
if (record) {
const newDuration = data.duration ?? record.duration ?? 0;
const newDistance = data.distance ?? record.distance ?? 0;
updateData.speed = newDistance / (newDuration / 60);
}
}
return await queryBuilder
.update("cardio")
.set(updateData)
.where("id", "=", id)
.execute();
}
/**
* Update a calorie entry
*/
export async function updateCalorieEntry(
id: number,
data: {
totalCalories?: number;
totalProtein?: number;
totalCarbs?: number;
totalFats?: number;
quantity?: number;
mealName?: string;
}
) {
return await queryBuilder
.update("calories")
.set(data)
.where("id", "=", id)
.execute();
}
/**
* Update a setting
*/
export async function updateSetting(
id: number,
data: {
value?: string;
description?: string;
}
) {
return await queryBuilder
.update("settings")
.set(data)
.where("id", "=", id)
.execute();
}
// ============================================================================
// DELETE Operations
// ============================================================================
/**
* Delete a weight record
* Example: Delete a single record
*
* @example
* export async function deleteUser(id: number) {
* return await queryBuilder
* .deleteFrom("users")
* .where("id", "=", id)
* .execute();
* }
*/
export async function deleteWeightRecord(id: number) {
return await queryBuilder.deleteFrom("weight").where("id", "=", id).execute();
}
/**
* Delete multiple weight records by IDs
* Example: Delete multiple records by IDs
*
* @example
* export async function deleteUsers(ids: number[]) {
* return await queryBuilder
* .deleteFrom("users")
* .where("id", "hasAnyOf", ids)
* .execute();
* }
*/
export async function deleteWeightRecords(ids: number[]) {
return await queryBuilder
.deleteFrom("weight")
.where("id", "hasAnyOf", ids)
.execute();
}
/**
* Delete a goal
* Example: Delete with condition
*
* @example
* export async function deleteInactiveUsers() {
* return await queryBuilder
* .deleteFrom("users")
* .where("status", "=", "inactive")
* .execute();
* }
*/
export async function deleteGoal(id: number) {
return await queryBuilder.deleteFrom("goals").where("id", "=", id).execute();
}
/**
* Delete a strength workout
*/
export async function deleteStrengthWorkout(id: number) {
return await queryBuilder
.deleteFrom("strength")
.where("id", "=", id)
.execute();
}
/**
* Delete a cardio exercise
*/
export async function deleteCardioExercise(id: number) {
return await queryBuilder.deleteFrom("cardio").where("id", "=", id).execute();
}
/**
* Delete a calorie entry
*/
export async function deleteCalorieEntry(id: number) {
return await queryBuilder
.deleteFrom("calories")
.where("id", "=", id)
.execute();
}
/**
* Delete a setting
*/
export async function deleteSetting(id: number) {
return await queryBuilder
.deleteFrom("settings")
.where("id", "=", id)
.execute();
}
// ============================================================================
// AGGREGATION Operations (Advanced)
// AGGREGATION Operations (Manual)
// ============================================================================
/**
* Get weight statistics using aggregation
* 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),
* };
* }
*/
export async function getWeightStats() {
// Note: TaylorDB aggregation API may differ, this is a placeholder
// Use the aggregateFrom method if available
const weights = await queryBuilder
.selectFrom("weight")
.select(["weight"])
.execute();
if (weights.length === 0) {
return {
count: 0,
average: null,
min: null,
max: null,
};
}
const values = weights
.map((w) => w.weight)
.filter((w): w is number => w !== undefined);
return {
count: values.length,
average: values.reduce((a, b) => a + b, 0) / values.length,
min: Math.min(...values),
max: Math.max(...values),
};
}
/**
* Get total calories for a date
* 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),
* };
* }
*/
export async function getTotalCaloriesForDate(date: string) {
const entries = await queryBuilder
.selectFrom("calories")
.select(["totalCalories", "totalProtein", "totalCarbs", "totalFats"])
.where("date", "=", ["exactDay", date])
.execute();
return {
totalCalories: entries.reduce((sum, e) => sum + (e.totalCalories ?? 0), 0),
totalProtein: entries.reduce((sum, e) => sum + (e.totalProtein ?? 0), 0),
totalCarbs: entries.reduce((sum, e) => sum + (e.totalCarbs ?? 0), 0),
totalFats: entries.reduce((sum, e) => sum + (e.totalFats ?? 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,452 +0,0 @@
This package contains the official TypeScript query builder for TaylorDB. It provides a type-safe and intuitive API for building and executing queries against your TaylorDB database.
## Available Query Builder Methods
**IMPORTANT FOR AI AGENTS**: The following is a complete list of available methods. Do not assume methods exist that are not listed here.
### Query Types (Starting Methods)
- `selectFrom(tableName)` - Start a SELECT query
- `insertInto(tableName)` - Start an INSERT query
- `update(tableName)` - Start an UPDATE query
- `deleteFrom(tableName)` - Start a DELETE query
- `aggregateFrom(tableName)` - Start an aggregation query
- `batch(queries)` - Execute multiple queries in parallel
- `transaction(callback)` - Execute queries in a transaction
### Query Chain Methods
**For SELECT queries (`selectFrom`):**
- `.select(fields)` - Specify fields to select (array of field names)
- `.selectAll()` - Select all fields
- `.where(field, operator, value)` - Add a WHERE condition
- `.orderBy(field, direction)` - Sort results ('asc' or 'desc')
- `.paginate(page, pageSize)` - Paginate results
- `.with(relations)` - Include related records
- `.count()` - **Count records matching the query** (returns `Promise<number>` directly)
- `.execute()` - Execute query and return array of results
- `.executeTakeFirst()` - Execute query and return first result or undefined
**For INSERT queries (`insertInto`):**
- `.values(data)` - Set values to insert
- `.execute()` - Execute insert and return array of inserted records
- `.executeTakeFirst()` - Execute insert and return first inserted record
**For UPDATE queries (`update`):**
- `.set(data)` - Set values to update
- `.where(field, operator, value)` - Add a WHERE condition
- `.execute()` - Execute update and return `{ affectedRecords: number }`
**For DELETE queries (`deleteFrom`):**
- `.where(field, operator, value)` - Add a WHERE condition
- `.execute()` - Execute delete and return `{ affectedRecords: number }`
**For AGGREGATION queries (`aggregateFrom`):**
- `.groupBy(field, direction)` - Group by a field ('asc' or 'desc')
- `.metrics(aggregateFunctions)` - Specify aggregate functions (e.g., `{ total: count('id') }`)
- `.where(field, operator, value)` - Add a WHERE condition
- `.execute()` - Execute aggregation and return array of grouped results
### Aggregate Functions
Import these functions from `@taylordb/query-builder`:
- `count(field)` - Count records
- `sum(field)` - Sum numeric values
- `avg(field)` - Average numeric values
- `min(field)` - Minimum value
- `max(field)` - Maximum value
## Common Mistakes to Avoid
**⚠️ CRITICAL: The following methods DO NOT EXIST and will cause errors:**
- ❌ `.countRecords()` - **DOES NOT EXIST**
- ❌ `.getCount()` - **DOES NOT EXIST**
- ❌ `.length` - **DOES NOT EXIST** on query builder chains
**Note**: `.count()` DOES exist for `selectFrom` queries. See the "Counting Records" section below for proper usage.
## Usage Examples
### Selecting Data
You can select data from a table using the `selectFrom` method. You can specify which fields to return, and you can filter, sort, and paginate the results.
```typescript
const customers = await qb
.selectFrom('customers')
.select(['firstName', 'lastName'])
.where('firstName', '=', 'John')
.orderBy('lastName', 'asc')
.paginate(1, 10)
.execute();
```
### Counting Records
There are two ways to count records in TaylorDB, each suited for different use cases:
#### Method 1: Using `.count()` on `selectFrom` (Recommended for simple counts)
**Use this when**: You need a single count value and don't need grouping or multiple metrics.
```typescript
// Count all users - returns a number directly
const totalUsers = await qb
.selectFrom('users')
.count();
console.log(`Total users: ${totalUsers}`);
// Count with filters - respects all WHERE conditions
const activeUsers = await qb
.selectFrom('users')
.where('status', '=', 'active')
.where('age', '>', 18)
.count();
console.log(`Active adult users: ${activeUsers}`);
// Count with relation filters
const usersWithPosts = await qb
.selectFrom('users')
.where('posts', 'isNotEmpty')
.count();
console.log(`Users with posts: ${usersWithPosts}`);
```
**Key Points:**
- `.count()` returns `Promise<number>` directly (not an array)
- Works with all `selectFrom` chain methods (`.where()`, `.orderBy()`, etc.)
- Simple and efficient for single count values
- No need to import `count` function or use destructuring
#### Method 2: Using `aggregateFrom` with `count()` function (For grouped counts or multiple metrics)
**Use this when**: You need counts grouped by field(s) or multiple aggregate metrics.
```typescript
import { count } from '@taylordb/query-builder';
// Count grouped by status (returns array of results)
const statusCounts = await qb
.aggregateFrom('users')
.groupBy('status', 'asc')
.metrics({
total: count('id'),
})
.execute();
// Find specific status count
const activeCount = statusCounts.find(item => item.status === 'active')?.total || 0;
// Multiple metrics with grouping
const userStats = await qb
.aggregateFrom('users')
.groupBy('status', 'asc')
.metrics({
total: count('id'),
averageAge: avg('age'),
})
.execute();
```
**Key Points:**
- Must import `count` function from `@taylordb/query-builder`
- Returns an array of grouped results
- Use when you need grouping or multiple aggregate functions
- More flexible but slightly more verbose for simple counts
#### When to Use Which Method
- **Use `.count()`** when: You need a single count value (e.g., total users, active orders, etc.)
- **Use `aggregateFrom`** when: You need counts grouped by field(s) or multiple aggregate metrics together
### Inserting Data
You can insert data into a table using the `insertInto` method.
```typescript
const newCustomer = await qb
.insertInto('customers')
.values({
firstName: 'Jane',
lastName: 'Doe',
})
.execute();
```
### Updating Data
You can update data in a table using the `update` method.
```typescript
const { affectedRecords } = await qb
.update('customers')
.set({ lastName: 'Smith' })
.where('id', '=', 1)
.execute();
```
### Deleting Data
You can delete data from a table using the `deleteFrom` method.
```typescript
const { affectedRecords } = await qb
.deleteFrom('customers')
.where('id', '=', 1)
.execute();
```
### Aggregation Queries
You can perform powerful aggregation queries using the `aggregateFrom` method. You can group by one or more fields and specify aggregate functions to apply.
```typescript
import { count, sum, avg, max, min } from '@taylordb/query-builder';
const aggregates = await qb
.aggregateFrom('orders')
.groupBy('status', 'asc')
.metrics({
orderCount: count('id'),
totalRevenue: sum('total'),
averageOrder: avg('total'),
maxOrder: max('total'),
minOrder: min('total'),
})
.execute();
```
### Transactions
You can execute a series of operations within a single atomic transaction. If any operation within the transaction fails, all previous operations will be rolled back.
```typescript
const newCustomer = await qb.transaction(async tx => {
const customer = await tx
.insertInto('customers')
.values({
firstName: 'John',
lastName: 'Doe',
})
.executeTakeFirst();
if (!customer) {
throw new Error('Customer creation failed.');
}
await tx
.insertInto('orders')
.values({
customerId: customer.id,
orderDate: new Date().toISOString(),
total: 100,
})
.execute();
return customer;
});
```
### Handling Attachments
You can upload files and associate them with your records using the `uploadAttachments` method. This is useful for handling things like user avatars, product images, or any other file-based data.
First, upload the file(s) to get `Attachment` instances:
```typescript
const filesToUpload = [
{ file: new Blob(['file content']), name: 'avatar.png' },
];
const attachments = await qb.uploadAttachments(filesToUpload);
```
Then, you can use the returned `Attachment` instances when creating or updating records. The query builder will automatically convert them into the correct format.
```typescript
// Create a new customer with an avatar
const newCustomer = await qb
.insertInto('customers')
.values({
firstName: 'Jane',
lastName: 'Doe',
avatar: attachments[0], // Use the Attachment instance
})
.executeTakeFirst();
// Update an existing customer's avatar
const { affectedRecords } = await qb
.update('customers')
.set({
avatar: attachments[0], // Use the Attachment instance
})
.where('id', '=', 1)
.execute();
```
### Batch Queries
You can execute multiple queries in a single batch request for improved performance. The result will be a tuple that corresponds to the results of each query in the batch.
```typescript
const [customers, newCustomer] = await qb
.batch([
qb.selectFrom('customers').select(['firstName', 'lastName']),
qb.insertInto('customers').values({ firstName: 'John', lastName: 'Doe' }),
])
.execute();
```
## Best Practices for Performance
When working with large databases, optimizing your queries is crucial for fast and efficient data retrieval. Here are some best practices to follow:
### 1. Select Only Necessary Fields
To minimize data transfer and improve query speed, always select only the fields you need. Avoid fetching all columns from a table if you only require a subset of them.
**Bad Practice:**
```typescript
// Fetches all fields for all customers, which can be slow with large tables.
const customers = await qb
.selectFrom('customers')
.selectAll()
.execute();
```
**Good Practice:**
```typescript
// Fetches only the required fields, leading to a faster and more efficient query.
const customers = await qb
.selectFrom('customers')
.select(['firstName', 'lastName', 'email'])
.execute();
```
### 2. Use Aggregations for Metrics and Dashboards
When building dashboards or calculating metrics (e.g., total sales, user counts), it's more efficient to perform aggregations directly in the database rather than fetching raw data and processing it in your application. The `aggregateFrom` method is optimized for this purpose.
**Bad Practice (less efficient):**
```typescript
// Fetches all orders and then calculates the total count in the application.
const orders = await qb
.selectFrom('orders')
.select(['id'])
.execute();
const totalOrders = orders.length;
```
**Good Practice (more efficient):**
```typescript
// For simple counts, use .count() method - simpler and more efficient
const totalOrders = await qb
.selectFrom('orders')
.count();
// Or if you need grouping, use aggregateFrom
import { count } from '@taylordb/query-builder';
const [{ totalOrders }] = await qb
.aggregateFrom('orders')
.metrics({
totalOrders: count('id'),
})
.execute();
```
**Example: Counting records grouped by status**
```typescript
import { count } from '@taylordb/query-builder';
// Count tasks by status - returns array of grouped results
const statusCounts = await qb
.aggregateFrom('tasks')
.groupBy('status', 'asc')
.metrics({
total: count('id'),
})
.execute();
// Extract specific counts
const doneCount = statusCounts.find(item => item.status === 'Done')?.total || 0;
const pendingCount = statusCounts.find(item => item.status === 'Pending')?.total || 0;
```
**Example: Simple counts for individual metrics**
```typescript
// For simple counts without grouping, use .count() method
const totalDone = await qb
.selectFrom('tasks')
.where('status', '=', 'Done')
.count();
const totalPending = await qb
.selectFrom('tasks')
.where('status', '=', 'Pending')
.count();
```
**Alternative: Using batch queries for multiple filtered counts**
```typescript
// If you need multiple simple counts, batch queries can be efficient
const [totalDone, totalPending] = await qb.batch([
qb.selectFrom('tasks').where('status', '=', 'Done').count(),
qb.selectFrom('tasks').where('status', '=', 'Pending').count(),
]);
```
**Note**:
- Use `.count()` for simple single counts (most efficient)
- Use `aggregateFrom` with `groupBy` when you need counts grouped by field(s)
- Use `batch` when you need multiple different filtered counts in parallel
Using aggregations reduces the amount of data transferred over the network and leverages the database's power for calculations, resulting in better performance for your application.
## Recipes
### Select with Relations
You can use the `with` method to fetch related records from a linked table.
```typescript
// Assuming 'customers' has a link field 'orders' to the 'orders' table
const customersWithOrders = await qb
.selectFrom('customers')
.select(['firstName', 'lastName'])
.with({
orders: qb => qb.select(['orderDate', 'total']),
})
.execute();
```
### Cross-Filters
You can filter records in one table based on the values in a linked table.
```typescript
// Get all customers who have placed an order with a total greater than 100
const highValueCustomers = await qb
.selectFrom('customers')
.where('orders', 'hasAnyOf', qb => qb.where('total', '>', 100))
.execute();
```
### Conditional Updates
You can use `where` clauses to update only the records that match a specific condition.
```typescript
// Update the status of all orders placed before a certain date
const { affectedRecords } = await qb
.update('orders')
.set({ status: 'archived' })
.where('orderDate', '<', '2023-01-01')
.execute();
```

View File

@ -0,0 +1,651 @@
# shadcn/ui Dashboard Components Guide
This guide provides examples of using shadcn/ui components to build modern dashboard interfaces for your TaylorDB applications.
---
## 📦 Installation
### Install Individual Components
```bash
# Core components (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
```
---
## 🎨 Common Dashboard Patterns
### 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>
);
}
```
---
## 🎨 Design Tips
### Color Schemes
Use semantic color tokens:
- `bg-background` / `text-foreground` - Main background and text
- `bg-card` / `text-card-foreground` - Card surfaces
- `bg-primary` / `text-primary-foreground` - Primary actions
- `bg-destructive` - Destructive actions (delete, etc.)
- `bg-muted` / `text-muted-foreground` - Subtle UI elements
### Spacing
Use consistent spacing:
- `space-y-4` / `gap-4` - Between related items
- `space-y-6` / `gap-6` - Between sections
- `p-4` / `p-6` - Card padding
- `p-8` - Page padding
### Icons
Use `lucide-react` for consistent icons:
```typescript
import { Home, Users, Settings, Plus, Edit, Trash, Search } from "lucide-react";
<Button>
<Plus className="mr-2 h-4 w-4" />
Add Item
</Button>;
```
---
## 📱 Responsive Design
### Grid Layouts
```typescript
// 1 column on mobile, 2 on tablet, 4 on desktop
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{/* cards */}
</div>
// 1 column on mobile, 3 on desktop
<div className="grid gap-6 lg:grid-cols-3">
{/* cards */}
</div>
```
### Hide on Mobile
```typescript
// Hide text on mobile, show on desktop
<span className="hidden sm:inline">Dashboard</span>
// Different layout on mobile
<div className="flex flex-col md:flex-row gap-4">
{/* content */}
</div>
```
---
## 🚀 Performance Tips
1. **Lazy load dialogs**: Only render dialog content when open
2. **Virtualize long lists**: Use libraries like `react-window`
3. **Skeleton loaders**: Always show loading states
4. **Optimistic updates**: Update UI before server confirms
5. **Debounce search**: Don't query on every keystroke
---
## 📚 Resources
- **shadcn/ui**: https://ui.shadcn.com/
- **Tailwind CSS**: https://tailwindcss.com/
- **Lucide Icons**: https://lucide.dev/
- **React Hook Form**: https://react-hook-form.com/
---
**Remember**: Always test your components in both light and dark mode, and on different screen sizes!

View File

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

View File

@ -1,8 +1,3 @@
{
"$schema": "https://opencode.ai/config.json",
"instructions": [
"../src/lib/taylordb.types.ts",
"../src/lib/taylordb-client.ts",
"./docs/QUERY_BUILDER.md"
]
}