Compare commits
No commits in common. "26b00bcf61c720bb136c4079d2dfd3ef315bff6e" and "d51a2f372fdb3cd30e27dba3c8795ff7c022eb16" have entirely different histories.
26b00bcf61
...
d51a2f372f
111
AGENTS.md
111
AGENTS.md
|
|
@ -15,29 +15,6 @@ Build production-ready, modern web applications (primarily dashboards and CRUD i
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📚 Docs for Agents
|
|
||||||
|
|
||||||
Before you start implementing, skim these docs in the `docs/` folder:
|
|
||||||
|
|
||||||
- **TaylorDB Query Builder**
|
|
||||||
- `docs/TAYLORDB_QUERY_REFERENCE.md` – index for all query examples
|
|
||||||
- `docs/TAYLORDB_BASIC_QUERIES.md` – basic reads, filtering, dates
|
|
||||||
- `docs/TAYLORDB_WRITE_OPERATIONS.md` – inserts, updates, deletes
|
|
||||||
- `docs/TAYLORDB_ADVANCED_PATTERNS.md` – aggregations, pagination, conditional queries
|
|
||||||
- `docs/TAYLORDB_FIELD_TYPES.md` – field type mapping & enums
|
|
||||||
- `docs/TAYLORDB_ATTACHMENTS.md` – working with attachment fields
|
|
||||||
- `docs/TAYLORDB_PITFALLS_BEST_PRACTICES.md` – common mistakes & best practices
|
|
||||||
|
|
||||||
- **shadcn/ui Components & Dashboard Patterns**
|
|
||||||
- `docs/SHADCN_COMPONENTS_GUIDE.md` – index for all shadcn/ui docs
|
|
||||||
- `docs/SHADCN_INSTALLATION.md` – how to install shadcn/ui components
|
|
||||||
- `docs/SHADCN_DASHBOARD_PATTERNS.md` – ready-made dashboard/layout patterns
|
|
||||||
- `docs/SHADCN_DESIGN_AND_LAYOUT.md` – design tokens, layout, responsive & performance tips
|
|
||||||
|
|
||||||
Use `AGENTS.md` for **workflow and rules** and the `docs/` files for **detailed code examples**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Development Workflow
|
## 📋 Development Workflow
|
||||||
|
|
||||||
### **Phase 1: Understand Requirements & Design**
|
### **Phase 1: Understand Requirements & Design**
|
||||||
|
|
@ -77,15 +54,19 @@ Document your design decisions briefly before implementing.
|
||||||
|
|
||||||
#### Step 1: Set Up Server-Side Data Layer
|
#### Step 1: Set Up Server-Side Data Layer
|
||||||
|
|
||||||
Use querybuilder which is in **File: `apps/server/taylordb/query-builder.ts`**
|
**File: `apps/server/taylordb/query-builder.ts`**
|
||||||
|
|
||||||
You can access the query builder from
|
This file contains all database operations. Create type-safe CRUD functions for each table:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
publicProcedure.input({}).query(({ input, ctx }) => {
|
import { createQueryBuilder } from "@taylordb/query-builder";
|
||||||
const queryBuilder = ctx.queryBuilder;
|
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
|
// READ Operations
|
||||||
|
|
@ -258,16 +239,21 @@ Update the design tokens based on your chosen color scheme:
|
||||||
|
|
||||||
#### Step 2: Create shadcn/ui Components
|
#### Step 2: Create shadcn/ui Components
|
||||||
|
|
||||||
**Always use shadcn/ui components**. Install components as needed with:
|
**Always use shadcn/ui components**. Install components as needed:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dlx shadcn@latest add <component-name>
|
pnpm dlx shadcn@latest add <component-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
For concrete install commands and patterns:
|
**Available by default:**
|
||||||
|
|
||||||
- See `docs/SHADCN_INSTALLATION.md` for component install snippets
|
- `button`, `card`, `input`, `label`, `textarea`, `select`, `tabs`, `alert`
|
||||||
- See `docs/SHADCN_DASHBOARD_PATTERNS.md` for tables, dialogs, forms, toasts, sheets, command palette, etc.
|
|
||||||
|
**Common additions for dashboards:**
|
||||||
|
|
||||||
|
- `table`, `dialog`, `dropdown-menu`, `toast`, `sheet`, `form`, `badge`, `avatar`, `skeleton`
|
||||||
|
|
||||||
|
Install with: `pnpm dlx shadcn@latest add table dialog dropdown-menu toast sheet form badge avatar skeleton`
|
||||||
|
|
||||||
#### Step 3: Build Page Components
|
#### Step 3: Build Page Components
|
||||||
|
|
||||||
|
|
@ -594,17 +580,60 @@ apps/client/src/
|
||||||
|
|
||||||
## 🔧 TaylorDB Query Builder Reference
|
## 🔧 TaylorDB Query Builder Reference
|
||||||
|
|
||||||
Instead of duplicating examples here, use the dedicated docs:
|
### Common Query Patterns
|
||||||
|
|
||||||
- `docs/TAYLORDB_QUERY_REFERENCE.md` – index for all query builder docs
|
```typescript
|
||||||
- `docs/TAYLORDB_BASIC_QUERIES.md` – `selectFrom`, filtering, ordering, date filters
|
// SELECT with specific fields
|
||||||
- `docs/TAYLORDB_WRITE_OPERATIONS.md` – `insertInto`, `update`, `deleteFrom`
|
queryBuilder
|
||||||
- `docs/TAYLORDB_ADVANCED_PATTERNS.md` – aggregations, totals, conditional queries, pagination
|
.selectFrom("tableName")
|
||||||
- `docs/TAYLORDB_FIELD_TYPES.md` – field type mapping, nullable handling, enums
|
.select(["id", "name", "status"])
|
||||||
- `docs/TAYLORDB_ATTACHMENTS.md` – selecting and writing attachment fields
|
.execute();
|
||||||
- `docs/TAYLORDB_PITFALLS_BEST_PRACTICES.md` – pitfalls and best practices
|
|
||||||
|
|
||||||
When writing queries in `apps/server/taylordb/query-builder.ts`, mirror the patterns from these docs and keep everything **strongly typed** using `taylordb/types.ts`.
|
// WHERE conditions
|
||||||
|
.where("status", "=", "active")
|
||||||
|
.where("createdAt", ">=", ["exactDay", "2024-01-01"])
|
||||||
|
.where("tags", "hasAnyOf", ["tag1", "tag2"])
|
||||||
|
|
||||||
|
// ORDER BY
|
||||||
|
.orderBy("createdAt", "desc")
|
||||||
|
.orderBy("name", "asc")
|
||||||
|
|
||||||
|
// INSERT
|
||||||
|
queryBuilder
|
||||||
|
.insertInto("tableName")
|
||||||
|
.values({ name: "John", status: "active" })
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
// UPDATE
|
||||||
|
queryBuilder
|
||||||
|
.update("tableName")
|
||||||
|
.set({ status: "inactive" })
|
||||||
|
.where("id", "=", 123)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
// DELETE
|
||||||
|
queryBuilder
|
||||||
|
.deleteFrom("tableName")
|
||||||
|
.where("id", "=", 123)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
// DELETE multiple
|
||||||
|
queryBuilder
|
||||||
|
.deleteFrom("tableName")
|
||||||
|
.where("id", "hasAnyOf", [1, 2, 3])
|
||||||
|
.execute();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Field Type Handling
|
||||||
|
|
||||||
|
| TaylorDB Field Type | TypeScript Type | Query Value Format |
|
||||||
|
| ------------------- | --------------- | ---------------------------- |
|
||||||
|
| Text | `string` | `"value"` |
|
||||||
|
| Number | `number` | `42` |
|
||||||
|
| Date | `string` | `["exactDay", "2024-01-01"]` |
|
||||||
|
| Single Select | `string[]` | `["option"]` |
|
||||||
|
| Multi Select | `string[]` | `["opt1", "opt2"]` |
|
||||||
|
| Checkbox | `boolean` | `true` / `false` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,7 @@ This template is designed to deploy to TaylorDB's platform using the included `t
|
||||||
**Environment Variables Required:**
|
**Environment Variables Required:**
|
||||||
|
|
||||||
- `TAYLORDB_BASE_URL`
|
- `TAYLORDB_BASE_URL`
|
||||||
|
- `TAYLORDB_API_TOKEN`
|
||||||
- `TAYLORDB_SERVER_ID`
|
- `TAYLORDB_SERVER_ID`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -1,269 +0,0 @@
|
||||||
import { useState, useRef, useCallback } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Upload, X, FileIcon, CheckCircle2, Send } from "lucide-react";
|
|
||||||
import { DemoCard, InlineSpinner } from "@/components/demo";
|
|
||||||
import { trpc } from "@/lib/trpc";
|
|
||||||
|
|
||||||
interface FileMetadata {
|
|
||||||
originalName: string;
|
|
||||||
mimeType: string;
|
|
||||||
size: number;
|
|
||||||
sizeFormatted: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BASE_URL =
|
|
||||||
import.meta.env.VITE_TRPC_URL || "http://localhost:3001/api";
|
|
||||||
|
|
||||||
export function FileUploadExample() {
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
const [email, setEmail] = useState("");
|
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const utils = trpc.useUtils();
|
|
||||||
const { data: submissions } = trpc.submitUserData.getAll.useQuery();
|
|
||||||
|
|
||||||
const submitMutation = trpc.submitUserData.submit.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
utils.submitUserData.getAll.invalidate();
|
|
||||||
setName("");
|
|
||||||
setEmail("");
|
|
||||||
setFiles([]);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const addFiles = useCallback((newFiles: FileList | File[]) => {
|
|
||||||
setFiles((prev) => [...prev, ...Array.from(newFiles)]);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const removeFile = (index: number) => {
|
|
||||||
setFiles((prev) => prev.filter((_, i) => i !== index));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = useCallback(
|
|
||||||
(e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsDragging(false);
|
|
||||||
if (e.dataTransfer.files.length) {
|
|
||||||
addFiles(e.dataTransfer.files);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[addFiles]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Step 1: Upload files via /api/upload
|
|
||||||
let uploadedFiles: FileMetadata[] = [];
|
|
||||||
|
|
||||||
if (files.length > 0) {
|
|
||||||
const formData = new FormData();
|
|
||||||
files.forEach((file) => formData.append("files", file));
|
|
||||||
|
|
||||||
const uploadRes = await fetch(`${BASE_URL}/upload`, {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
credentials: "include",
|
|
||||||
});
|
|
||||||
const uploadJson = await uploadRes.json();
|
|
||||||
|
|
||||||
if (!uploadRes.ok || !uploadJson.success) {
|
|
||||||
throw new Error(uploadJson.error || "File upload failed");
|
|
||||||
}
|
|
||||||
uploadedFiles = uploadJson.data.files;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Submit user data with file references via tRPC
|
|
||||||
await submitMutation.mutateAsync({
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
files: uploadedFiles.map((f) => ({
|
|
||||||
originalName: f.originalName,
|
|
||||||
mimeType: f.mimeType,
|
|
||||||
size: f.size,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : "Something went wrong");
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatSize = (bytes: number) => {
|
|
||||||
if (bytes === 0) return "0 B";
|
|
||||||
const units = ["B", "KB", "MB", "GB"];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
||||||
return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const canSubmit = name.trim() && email.trim() && !isLoading;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DemoCard
|
|
||||||
title="Submit User Data"
|
|
||||||
description="Upload files via /api/upload, then submit data via tRPC"
|
|
||||||
icon={Send}
|
|
||||||
iconColorClass="bg-amber-500/10 text-amber-500"
|
|
||||||
glowClass="glow-primary"
|
|
||||||
>
|
|
||||||
{/* Form Fields */}
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<Input
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
placeholder="Name"
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
placeholder="Email"
|
|
||||||
type="email"
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Drop Zone */}
|
|
||||||
<div
|
|
||||||
className={`relative rounded-lg border-2 border-dashed p-6 text-center transition-colors cursor-pointer ${
|
|
||||||
isDragging
|
|
||||||
? "border-primary bg-primary/5"
|
|
||||||
: "border-border/60 hover:border-primary/40 hover:bg-muted/30"
|
|
||||||
}`}
|
|
||||||
onDragOver={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsDragging(true);
|
|
||||||
}}
|
|
||||||
onDragLeave={() => setIsDragging(false)}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
multiple
|
|
||||||
className="hidden"
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.files) addFiles(e.target.files);
|
|
||||||
e.target.value = "";
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Upload className="mx-auto h-8 w-8 text-muted-foreground/60 mb-2" />
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{isDragging ? (
|
|
||||||
<span className="text-primary font-medium">Drop files here</span>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="font-medium text-foreground">Click to browse</span>{" "}
|
|
||||||
or drag & drop files
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground/60 mt-1">
|
|
||||||
Any file type • Max 10 MB per file • Up to 10 files
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Selected Files */}
|
|
||||||
{files.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
||||||
{files.length} file{files.length > 1 ? "s" : ""} selected
|
|
||||||
</p>
|
|
||||||
{files.map((file, i) => (
|
|
||||||
<div
|
|
||||||
key={`${file.name}-${i}`}
|
|
||||||
className="flex items-center gap-3 p-3 rounded-lg bg-muted/30 border border-border/50"
|
|
||||||
>
|
|
||||||
<FileIcon className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-sm font-medium truncate">{file.name}</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{file.type || "unknown"} • {formatSize(file.size)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
className="h-7 w-7 text-muted-foreground hover:text-destructive shrink-0"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
removeFile(i);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<X className="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Submit Button */}
|
|
||||||
<Button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={!canSubmit}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<InlineSpinner /> Submitting...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Send className="h-4 w-4 mr-2" />
|
|
||||||
Submit User Data
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Error */}
|
|
||||||
{error && (
|
|
||||||
<div className="p-3 rounded-lg bg-destructive/10 border border-destructive/30 text-sm text-destructive">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Submissions List */}
|
|
||||||
{submissions && submissions.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
||||||
Recent submissions
|
|
||||||
</p>
|
|
||||||
{submissions.map((sub) => (
|
|
||||||
<div
|
|
||||||
key={sub.id}
|
|
||||||
className="p-3 rounded-lg bg-muted/30 border border-border/50 space-y-1"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CheckCircle2 className="h-3.5 w-3.5 text-green-500 shrink-0" />
|
|
||||||
<span className="text-sm font-medium">{sub.name}</span>
|
|
||||||
<span className="text-xs text-muted-foreground">{sub.email}</span>
|
|
||||||
</div>
|
|
||||||
{sub.files.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-1 ml-5.5">
|
|
||||||
{sub.files.map((f, i) => (
|
|
||||||
<span
|
|
||||||
key={i}
|
|
||||||
className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary"
|
|
||||||
>
|
|
||||||
<FileIcon className="h-3 w-3" />
|
|
||||||
{f.originalName}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DemoCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
export { HelloExample } from "./HelloExample";
|
export { HelloExample } from "./HelloExample";
|
||||||
export { UsersExample } from "./UsersExample";
|
export { UsersExample } from "./UsersExample";
|
||||||
export { PostsExample } from "./PostsExample";
|
export { PostsExample } from "./PostsExample";
|
||||||
export { FileUploadExample } from "./FileUploadExample";
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import {
|
||||||
HelloExample,
|
HelloExample,
|
||||||
UsersExample,
|
UsersExample,
|
||||||
PostsExample,
|
PostsExample,
|
||||||
FileUploadExample,
|
|
||||||
} from "@/components/demo/examples";
|
} from "@/components/demo/examples";
|
||||||
|
|
||||||
export default function TRPCDemoPage() {
|
export default function TRPCDemoPage() {
|
||||||
|
|
@ -30,7 +29,6 @@ export default function TRPCDemoPage() {
|
||||||
<HelloExample />
|
<HelloExample />
|
||||||
<UsersExample />
|
<UsersExample />
|
||||||
<PostsExample />
|
<PostsExample />
|
||||||
<FileUploadExample />
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,12 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import cookieParser from "cookie-parser";
|
|
||||||
import * as trpcExpress from "@trpc/server/adapters/express";
|
import * as trpcExpress from "@trpc/server/adapters/express";
|
||||||
import { appRouter } from "./router.js";
|
import { appRouter } from "./router.js";
|
||||||
import { createContext } from "./trpc.js";
|
import { createContext } from "./trpc.js";
|
||||||
import { fileUploadRouter } from "./routers/fileUpload.js";
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3001;
|
||||||
|
|
||||||
// Parse cookies
|
|
||||||
app.use(cookieParser());
|
|
||||||
|
|
||||||
// Enable CORS for frontend (adjust origin in production)
|
// Enable CORS for frontend (adjust origin in production)
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
|
|
@ -28,9 +23,6 @@ app.get("/api/health", (_req, res) => {
|
||||||
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
||||||
});
|
});
|
||||||
|
|
||||||
// File upload endpoint (multipart/form-data — not supported by tRPC Express adapter)
|
|
||||||
app.use("/api/upload", fileUploadRouter);
|
|
||||||
|
|
||||||
// tRPC middleware
|
// tRPC middleware
|
||||||
app.use(
|
app.use(
|
||||||
"/api/trpc",
|
"/api/trpc",
|
||||||
|
|
|
||||||
|
|
@ -14,19 +14,15 @@
|
||||||
"start": "node dist/index.js"
|
"start": "node dist/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@taylordb/query-builder": "^0.11.4",
|
"@taylordb/query-builder": "^0.10.3",
|
||||||
"@trpc/server": "^11.8.1",
|
"@trpc/server": "^11.8.1",
|
||||||
"cookie-parser": "^1.4.7",
|
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"multer": "^2.0.2",
|
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cookie-parser": "^1.4.10",
|
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/multer": "^2.0.0",
|
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"esbuild": "^0.27.2",
|
"esbuild": "^0.27.2",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { router, publicProcedure } from "./trpc";
|
import { router, publicProcedure } from "./trpc";
|
||||||
import { usersRouter, postsRouter, submitUserDataRouter } from "./routers";
|
import { usersRouter, postsRouter } from "./routers";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main tRPC Router
|
* Main tRPC Router
|
||||||
|
|
@ -20,7 +20,6 @@ export const appRouter = router({
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
users: usersRouter,
|
users: usersRouter,
|
||||||
posts: postsRouter,
|
posts: postsRouter,
|
||||||
submitUserData: submitUserDataRouter,
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Global / Utility Procedures
|
// Global / Utility Procedures
|
||||||
|
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
import { Router } from "express";
|
|
||||||
import multer from "multer";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* File Upload Router
|
|
||||||
*
|
|
||||||
* Express router for handling multipart FormData file uploads.
|
|
||||||
* tRPC v11's FormData support is designed for fetch-based adapters (Next.js, etc.),
|
|
||||||
* not the Express adapter. This dedicated Express route is the recommended pattern
|
|
||||||
* for handling file uploads in Express + tRPC stacks.
|
|
||||||
*
|
|
||||||
* Uses TaylorDB's uploadAttachments pattern to store files.
|
|
||||||
* See docs/TAYLORDB_ATTACHMENTS.md for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Configure multer with memory storage (files available as Buffer)
|
|
||||||
const upload = multer({
|
|
||||||
storage: multer.memoryStorage(),
|
|
||||||
limits: {
|
|
||||||
fileSize: 10 * 1024 * 1024, // 10 MB per file
|
|
||||||
files: 10, // max 10 files
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const fileUploadRouter = Router();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/upload
|
|
||||||
*
|
|
||||||
* Accepts multipart/form-data with:
|
|
||||||
* - files (File[], multiple files of any type)
|
|
||||||
*
|
|
||||||
* Returns metadata about each uploaded file.
|
|
||||||
*/
|
|
||||||
fileUploadRouter.post("/", upload.array("files", 10), (req, res) => {
|
|
||||||
const files = (req.files as Express.Multer.File[]) || [];
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: "At least one file is required.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileMetadata = files.map((file) => ({
|
|
||||||
originalName: file.originalname,
|
|
||||||
mimeType: file.mimetype,
|
|
||||||
size: file.size,
|
|
||||||
sizeFormatted: formatFileSize(file.size),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────
|
|
||||||
// TaylorDB Attachment Example (see docs/TAYLORDB_ATTACHMENTS.md)
|
|
||||||
//
|
|
||||||
// To store uploaded files in a TaylorDB record, convert multer buffers
|
|
||||||
// to Blobs and use qb.uploadAttachments():
|
|
||||||
//
|
|
||||||
// const attachments = await qb.uploadAttachments(
|
|
||||||
// files.map((file) => ({
|
|
||||||
// file: new Blob([file.buffer], { type: file.mimetype }),
|
|
||||||
// name: file.originalname,
|
|
||||||
// }))
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// // Then use the attachments when inserting/updating a record:
|
|
||||||
// await qb.insertInto("tableName").values({
|
|
||||||
// name: "Some record",
|
|
||||||
// documents: attachments, // attachment column
|
|
||||||
// }).execute();
|
|
||||||
//
|
|
||||||
// // Or when updating an existing record:
|
|
||||||
// await qb.update("tableName").set({
|
|
||||||
// documents: attachments,
|
|
||||||
// }).where("id", "=", recordId).execute();
|
|
||||||
// ────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
filesCount: files.length,
|
|
||||||
files: fileMetadata,
|
|
||||||
totalSize: formatFileSize(files.reduce((sum, f) => sum + f.size, 0)),
|
|
||||||
uploadedAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Format bytes to human-readable string */
|
|
||||||
function formatFileSize(bytes: number): string {
|
|
||||||
if (bytes === 0) return "0 B";
|
|
||||||
const units = ["B", "KB", "MB", "GB"];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
||||||
return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { fileUploadRouter };
|
|
||||||
|
|
@ -7,4 +7,3 @@
|
||||||
|
|
||||||
export { usersRouter } from "./users";
|
export { usersRouter } from "./users";
|
||||||
export { postsRouter } from "./posts";
|
export { postsRouter } from "./posts";
|
||||||
export { submitUserDataRouter } from "./submitUserData";
|
|
||||||
|
|
|
||||||
|
|
@ -6,39 +6,6 @@ import { router, publicProcedure } from "../trpc";
|
||||||
*
|
*
|
||||||
* Another example sub-router showing a different domain.
|
* Another example sub-router showing a different domain.
|
||||||
* Demonstrates relationships (author references users).
|
* Demonstrates relationships (author references users).
|
||||||
*
|
|
||||||
* Example using TaylorDB queryBuilder (available via ctx.queryBuilder):
|
|
||||||
*
|
|
||||||
* // Query all posts
|
|
||||||
* const posts = await ctx.queryBuilder.from("posts").select("*");
|
|
||||||
*
|
|
||||||
* // Query with filters
|
|
||||||
* const publishedPosts = await ctx.queryBuilder
|
|
||||||
* .from("posts")
|
|
||||||
* .where({ published: true })
|
|
||||||
* .select("*");
|
|
||||||
*
|
|
||||||
* // Create a new post
|
|
||||||
* const newPost = await ctx.queryBuilder
|
|
||||||
* .from("posts")
|
|
||||||
* .insert({
|
|
||||||
* title: "My Title",
|
|
||||||
* content: "My Content",
|
|
||||||
* authorId: 1,
|
|
||||||
* published: false
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // Update a post
|
|
||||||
* const updatedPost = await ctx.queryBuilder
|
|
||||||
* .from("posts")
|
|
||||||
* .where({ id: 1 })
|
|
||||||
* .update({ published: true });
|
|
||||||
*
|
|
||||||
* // Delete a post
|
|
||||||
* await ctx.queryBuilder
|
|
||||||
* .from("posts")
|
|
||||||
* .where({ id: 1 })
|
|
||||||
* .delete();
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// In-memory store for demonstration
|
// In-memory store for demonstration
|
||||||
|
|
@ -69,7 +36,7 @@ export const postsRouter = router({
|
||||||
published: z.boolean().optional(),
|
published: z.boolean().optional(),
|
||||||
authorId: z.number().optional(),
|
authorId: z.number().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional()
|
||||||
)
|
)
|
||||||
.query(({ input }) => {
|
.query(({ input }) => {
|
||||||
let result = posts;
|
let result = posts;
|
||||||
|
|
@ -121,7 +88,7 @@ export const postsRouter = router({
|
||||||
title: z.string().min(1).max(200).optional(),
|
title: z.string().min(1).max(200).optional(),
|
||||||
content: z.string().min(1).optional(),
|
content: z.string().min(1).optional(),
|
||||||
published: z.boolean().optional(),
|
published: z.boolean().optional(),
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.mutation(({ input }) => {
|
.mutation(({ input }) => {
|
||||||
const post = posts.find((p) => p.id === input.id);
|
const post = posts.find((p) => p.id === input.id);
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import { z } from "zod";
|
|
||||||
import { router, publicProcedure } from "../trpc";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submit User Data Router
|
|
||||||
*
|
|
||||||
* tRPC router for submitting user data (name, email) with file attachment references.
|
|
||||||
* Files should first be uploaded via /api/upload, then referenced here.
|
|
||||||
*
|
|
||||||
* In a real app, you'd use qb.uploadAttachments() to store files in TaylorDB
|
|
||||||
* and insert the record with the attachment column. This demo uses in-memory storage.
|
|
||||||
*
|
|
||||||
* TaylorDB attachment pattern (see docs/TAYLORDB_ATTACHMENTS.md):
|
|
||||||
* await qb.insertInto("users").values({
|
|
||||||
* name: "Jane",
|
|
||||||
* avatar: await qb.uploadAttachments([
|
|
||||||
* { file: new Blob([buffer]), name: "photo.png" },
|
|
||||||
* ]),
|
|
||||||
* }).execute();
|
|
||||||
*/
|
|
||||||
|
|
||||||
// In-memory store for demonstration
|
|
||||||
interface Submission {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
files: { originalName: string; mimeType: string; size: number }[];
|
|
||||||
submittedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const submissions: Submission[] = [];
|
|
||||||
let nextId = 1;
|
|
||||||
|
|
||||||
export const submitUserDataRouter = router({
|
|
||||||
/** Submit user data with file metadata */
|
|
||||||
submit: publicProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
name: z.string().min(1, "Name is required"),
|
|
||||||
email: z.string().email("Valid email is required"),
|
|
||||||
files: z.array(
|
|
||||||
z.object({
|
|
||||||
originalName: z.string(),
|
|
||||||
mimeType: z.string(),
|
|
||||||
size: z.number(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.mutation(({ input }) => {
|
|
||||||
const submission: Submission = {
|
|
||||||
id: nextId++,
|
|
||||||
name: input.name,
|
|
||||||
email: input.email,
|
|
||||||
files: input.files,
|
|
||||||
submittedAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
submissions.push(submission);
|
|
||||||
return submission;
|
|
||||||
}),
|
|
||||||
|
|
||||||
/** Get all submissions */
|
|
||||||
getAll: publicProcedure.query(() => {
|
|
||||||
return submissions;
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
319
apps/server/taylordb/query-builder.ts
Normal file
319
apps/server/taylordb/query-builder.ts
Normal file
|
|
@ -0,0 +1,319 @@
|
||||||
|
import { createQueryBuilder } from "@taylordb/query-builder";
|
||||||
|
import type { TaylorDatabase } from "./types.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TaylorDB Query Builder Instance
|
||||||
|
*
|
||||||
|
* This is the main query builder instance configured with your TaylorDB credentials.
|
||||||
|
* Use this to perform all database operations in a type-safe manner.
|
||||||
|
*/
|
||||||
|
export const queryBuilder = createQueryBuilder<TaylorDatabase>({
|
||||||
|
baseUrl: process.env.TAYLORDB_BASE_URL!,
|
||||||
|
baseId: process.env.TAYLORDB_SERVER_ID!,
|
||||||
|
apiKey: process.env.TAYLORDB_API_TOKEN!,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ============================================================================
|
||||||
|
* Example Query Functions
|
||||||
|
* ============================================================================
|
||||||
|
*
|
||||||
|
* Below are example patterns for common database operations.
|
||||||
|
* Replace these with your own functions based on your actual schema.
|
||||||
|
*
|
||||||
|
* For comprehensive examples, see: /docs/TAYLORDB_QUERY_REFERENCE.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// READ Operations (Queries)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Get all records from a table
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* export async function getAllUsers() {
|
||||||
|
* return await queryBuilder
|
||||||
|
* .selectFrom("users")
|
||||||
|
* .select(["id", "name", "email", "createdAt"])
|
||||||
|
* .orderBy("createdAt", "desc")
|
||||||
|
* .execute();
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Get a single record by ID
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* export async function getUserById(id: number) {
|
||||||
|
* return await queryBuilder
|
||||||
|
* .selectFrom("users")
|
||||||
|
* .where("id", "=", id)
|
||||||
|
* .executeTakeFirst();
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Get records with filtering
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* export async function getActiveUsers() {
|
||||||
|
* return await queryBuilder
|
||||||
|
* .selectFrom("users")
|
||||||
|
* .where("status", "=", "active")
|
||||||
|
* .orderBy("name", "asc")
|
||||||
|
* .execute();
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Get records with date range filtering
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* export async function getRecordsInDateRange(startDate: string, endDate: string) {
|
||||||
|
* return await queryBuilder
|
||||||
|
* .selectFrom("records")
|
||||||
|
* .where("date", ">=", ["exactDay", startDate])
|
||||||
|
* .where("date", "<=", ["exactDay", endDate])
|
||||||
|
* .orderBy("date", "asc")
|
||||||
|
* .execute();
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// CREATE Operations (Insert)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Insert a new record
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* export async function createUser(data: { name: string; email: string }) {
|
||||||
|
* return await queryBuilder
|
||||||
|
* .insertInto("users")
|
||||||
|
* .values({
|
||||||
|
* name: data.name,
|
||||||
|
* email: data.email,
|
||||||
|
* status: "active",
|
||||||
|
* })
|
||||||
|
* .executeTakeFirst();
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Insert with single-select field
|
||||||
|
*
|
||||||
|
* Note: Single-select fields must be wrapped in an array
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* export async function createTask(data: { title: string; priority: "low" | "medium" | "high" }) {
|
||||||
|
* return await queryBuilder
|
||||||
|
* .insertInto("tasks")
|
||||||
|
* .values({
|
||||||
|
* title: data.title,
|
||||||
|
* priority: [data.priority], // Wrap in array for single-select
|
||||||
|
* })
|
||||||
|
* .executeTakeFirst();
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Insert with computed fields
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* export async function createOrder(data: { quantity: number; pricePerUnit: number }) {
|
||||||
|
* const totalPrice = data.quantity * data.pricePerUnit;
|
||||||
|
*
|
||||||
|
* return await queryBuilder
|
||||||
|
* .insertInto("orders")
|
||||||
|
* .values({
|
||||||
|
* quantity: data.quantity,
|
||||||
|
* pricePerUnit: data.pricePerUnit,
|
||||||
|
* totalPrice: totalPrice,
|
||||||
|
* })
|
||||||
|
* .executeTakeFirst();
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// UPDATE Operations
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Update a record
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* export async function updateUser(id: number, data: { name?: string; email?: string }) {
|
||||||
|
* return await queryBuilder
|
||||||
|
* .update("users")
|
||||||
|
* .set(data)
|
||||||
|
* .where("id", "=", id)
|
||||||
|
* .execute();
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Update with conditional recalculation
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* export async function updateOrder(id: number, data: { quantity?: number; pricePerUnit?: number }) {
|
||||||
|
* // Fetch current record to compute total
|
||||||
|
* const currentOrder = await queryBuilder
|
||||||
|
* .selectFrom("orders")
|
||||||
|
* .select(["quantity", "pricePerUnit"])
|
||||||
|
* .where("id", "=", id)
|
||||||
|
* .executeTakeFirst();
|
||||||
|
*
|
||||||
|
* if (!currentOrder) {
|
||||||
|
* throw new Error("Order not found");
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const newQuantity = data.quantity ?? currentOrder.quantity ?? 0;
|
||||||
|
* const newPrice = data.pricePerUnit ?? currentOrder.pricePerUnit ?? 0;
|
||||||
|
* const totalPrice = newQuantity * newPrice;
|
||||||
|
*
|
||||||
|
* return await queryBuilder
|
||||||
|
* .update("orders")
|
||||||
|
* .set({
|
||||||
|
* ...data,
|
||||||
|
* totalPrice,
|
||||||
|
* })
|
||||||
|
* .where("id", "=", id)
|
||||||
|
* .execute();
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// DELETE Operations
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Delete a single record
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* export async function deleteUser(id: number) {
|
||||||
|
* return await queryBuilder
|
||||||
|
* .deleteFrom("users")
|
||||||
|
* .where("id", "=", id)
|
||||||
|
* .execute();
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Delete multiple records by IDs
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* export async function deleteUsers(ids: number[]) {
|
||||||
|
* return await queryBuilder
|
||||||
|
* .deleteFrom("users")
|
||||||
|
* .where("id", "hasAnyOf", ids)
|
||||||
|
* .execute();
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Delete with condition
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* export async function deleteInactiveUsers() {
|
||||||
|
* return await queryBuilder
|
||||||
|
* .deleteFrom("users")
|
||||||
|
* .where("status", "=", "inactive")
|
||||||
|
* .execute();
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// AGGREGATION Operations (Manual)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Calculate statistics
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* export async function getUserStats() {
|
||||||
|
* const users = await queryBuilder
|
||||||
|
* .selectFrom("users")
|
||||||
|
* .select(["age"])
|
||||||
|
* .execute();
|
||||||
|
*
|
||||||
|
* if (users.length === 0) {
|
||||||
|
* return { count: 0, average: null, min: null, max: null };
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const ages = users.map(u => u.age).filter((a): a is number => a !== undefined);
|
||||||
|
*
|
||||||
|
* return {
|
||||||
|
* count: ages.length,
|
||||||
|
* average: ages.reduce((a, b) => a + b, 0) / ages.length,
|
||||||
|
* min: Math.min(...ages),
|
||||||
|
* max: Math.max(...ages),
|
||||||
|
* };
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Sum totals for a date
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* export async function getTotalSalesForDate(date: string) {
|
||||||
|
* const sales = await queryBuilder
|
||||||
|
* .selectFrom("sales")
|
||||||
|
* .select(["amount", "quantity"])
|
||||||
|
* .where("date", "=", ["exactDay", date])
|
||||||
|
* .execute();
|
||||||
|
*
|
||||||
|
* return {
|
||||||
|
* totalAmount: sales.reduce((sum, s) => sum + (s.amount ?? 0), 0),
|
||||||
|
* totalQuantity: sales.reduce((sum, s) => sum + (s.quantity ?? 0), 0),
|
||||||
|
* };
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ============================================================================
|
||||||
|
* Query Builder Quick Reference
|
||||||
|
* ============================================================================
|
||||||
|
*
|
||||||
|
* SELECT:
|
||||||
|
* - .selectFrom("tableName")
|
||||||
|
* - .select(["field1", "field2"])
|
||||||
|
* - .execute() // Returns array
|
||||||
|
* - .executeTakeFirst() // Returns single record or undefined
|
||||||
|
*
|
||||||
|
* WHERE:
|
||||||
|
* - .where("field", "=", value)
|
||||||
|
* - .where("field", ">", value)
|
||||||
|
* - .where("field", "hasAnyOf", [value1, value2])
|
||||||
|
* - .where("date", ">=", ["exactDay", "2024-01-01"])
|
||||||
|
*
|
||||||
|
* ORDER BY:
|
||||||
|
* - .orderBy("field", "asc")
|
||||||
|
* - .orderBy("field", "desc")
|
||||||
|
*
|
||||||
|
* INSERT:
|
||||||
|
* - .insertInto("tableName")
|
||||||
|
* - .values({ field1: value1, field2: value2 })
|
||||||
|
* - .executeTakeFirst()
|
||||||
|
*
|
||||||
|
* UPDATE:
|
||||||
|
* - .update("tableName")
|
||||||
|
* - .set({ field1: value1 })
|
||||||
|
* - .where("id", "=", id)
|
||||||
|
* - .execute()
|
||||||
|
*
|
||||||
|
* DELETE:
|
||||||
|
* - .deleteFrom("tableName")
|
||||||
|
* - .where("id", "=", id)
|
||||||
|
* - .execute()
|
||||||
|
*
|
||||||
|
* Field Types:
|
||||||
|
* - Text: string
|
||||||
|
* - Number: number
|
||||||
|
* - Date: ["exactDay", "YYYY-MM-DD"]
|
||||||
|
* - Single Select: ["option"]
|
||||||
|
* - Multi Select: ["opt1", "opt2"]
|
||||||
|
* - Boolean: true/false
|
||||||
|
*
|
||||||
|
* For comprehensive examples, see: /docs/TAYLORDB_QUERY_REFERENCE.md
|
||||||
|
*/
|
||||||
|
|
@ -1,30 +1,15 @@
|
||||||
import { createQueryBuilder } from "@taylordb/query-builder";
|
|
||||||
import { initTRPC } from "@trpc/server";
|
import { initTRPC } from "@trpc/server";
|
||||||
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
||||||
import type { TaylorDatabase } from "./taylordb/types";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create context for each tRPC request
|
* Create context for each tRPC request
|
||||||
* This is where you can add user session, database clients, etc.
|
* This is where you can add user session, database clients, etc.
|
||||||
*/
|
*/
|
||||||
export const createContext = ({ req, res }: CreateExpressContextOptions) => {
|
export const createContext = ({ req, res }: CreateExpressContextOptions) => {
|
||||||
// Extract app_access_token from cookies
|
|
||||||
const appAccessToken = req.cookies?.app_access_token;
|
|
||||||
|
|
||||||
if (!appAccessToken) {
|
|
||||||
throw new Error("Unauthorized: app_access_token cookie is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryBuilder = createQueryBuilder<TaylorDatabase>({
|
|
||||||
baseUrl: process.env.TAYLORDB_BASE_URL!,
|
|
||||||
baseId: process.env.TAYLORDB_SERVER_ID!,
|
|
||||||
apiKey: appAccessToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
queryBuilder,
|
// Add any shared context here (e.g., database client, user session)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,651 @@
|
||||||
# shadcn/ui Dashboard Components Guide
|
# shadcn/ui Dashboard Components Guide
|
||||||
|
|
||||||
This is the **entry point** for shadcn/ui documentation in this template. The guide is split into smaller files so agents (and humans) can jump to the topic they need.
|
This guide provides examples of using shadcn/ui components to build modern dashboard interfaces for your TaylorDB applications.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📚 Topics
|
## 📦 Installation
|
||||||
|
|
||||||
- **Installation**
|
### Install Individual Components
|
||||||
See `SHADCN_INSTALLATION.md` for:
|
|
||||||
- Installing individual components (core, dashboard, data display, advanced)
|
|
||||||
- Command examples for `pnpm dlx shadcn@latest add`
|
|
||||||
|
|
||||||
- **Dashboard Patterns**
|
```bash
|
||||||
See `SHADCN_DASHBOARD_PATTERNS.md` for:
|
# Core components (already included)
|
||||||
- Stats cards layout
|
pnpm dlx shadcn@latest add button card input label textarea select tabs alert
|
||||||
- Data table with dropdown actions
|
|
||||||
- Create/edit form in a dialog
|
|
||||||
- Loading states with Skeleton
|
|
||||||
- Tabs for different views
|
|
||||||
- Status badges
|
|
||||||
- Toast notifications (with tRPC mutations)
|
|
||||||
- Form with validation (react-hook-form + zod)
|
|
||||||
- Side sheet for details
|
|
||||||
- Command palette (search, Cmd/Ctrl+K)
|
|
||||||
|
|
||||||
- **Design & Layout**
|
# Dashboard-specific components
|
||||||
See `SHADCN_DESIGN_AND_LAYOUT.md` for:
|
pnpm dlx shadcn@latest add table dialog dropdown-menu toast sheet form badge avatar skeleton
|
||||||
- Color schemes (semantic tokens)
|
|
||||||
- Spacing conventions
|
# Data display components
|
||||||
- Icons (lucide-react)
|
pnpm dlx shadcn@latest add separator progress scroll-area tooltip
|
||||||
- Responsive grids and hide-on-mobile
|
|
||||||
- Performance tips (lazy dialogs, virtualization, skeletons, optimistic updates, debounce)
|
# Advanced components
|
||||||
|
pnpm dlx shadcn@latest add command popover calendar date-picker
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## How Agents Should Use These Docs
|
## 🎨 Common Dashboard Patterns
|
||||||
|
|
||||||
1. **Need to add a component?**
|
### 1. Dashboard Layout with Stats Cards
|
||||||
Use `SHADCN_INSTALLATION.md` for the exact `pnpm dlx shadcn@latest add` commands.
|
|
||||||
|
|
||||||
2. **Building a dashboard or CRUD UI?**
|
```typescript
|
||||||
Use `SHADCN_DASHBOARD_PATTERNS.md` for copy-paste patterns (tables, dialogs, forms, toasts, sheets, etc.).
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
|
||||||
3. **Styling and layout?**
|
export default function DashboardPage() {
|
||||||
Use `SHADCN_DESIGN_AND_LAYOUT.md` for tokens, spacing, responsive patterns, and performance.
|
return (
|
||||||
|
<div className="container mx-auto p-8">
|
||||||
|
<h1 className="text-3xl font-bold mb-6">Dashboard</h1>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
Total Users
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold">1,234</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
+20% from last month
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
Revenue
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-3xl font-bold">$45,231</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
+12% from last month
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* More stats cards... */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Data Table with Actions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { MoreHorizontal, Pencil, Trash } from "lucide-react";
|
||||||
|
|
||||||
|
export default function UsersTable({ users }: { users: User[] }) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Users</CardTitle>
|
||||||
|
<CardDescription>Manage your user accounts</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Email</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{users.map((user) => (
|
||||||
|
<TableRow key={user.id}>
|
||||||
|
<TableCell className="font-medium">{user.name}</TableCell>
|
||||||
|
<TableCell>{user.email}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge
|
||||||
|
variant={user.status === "active" ? "default" : "secondary"}
|
||||||
|
>
|
||||||
|
{user.status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Pencil className="mr-2 h-4 w-4" />
|
||||||
|
Edit
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem className="text-destructive">
|
||||||
|
<Trash className="mr-2 h-4 w-4" />
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Create/Edit Form in Dialog
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { PlusIcon } from "lucide-react";
|
||||||
|
|
||||||
|
export default function CreateUserDialog() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
|
||||||
|
const createMutation = trpc.users.create.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
setOpen(false);
|
||||||
|
setName("");
|
||||||
|
setEmail("");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button>
|
||||||
|
<PlusIcon className="mr-2 h-4 w-4" />
|
||||||
|
Add User
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create New User</DialogTitle>
|
||||||
|
<DialogDescription>Add a new user to your database</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="name">Name</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="John Doe"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="email">Email</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="john@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => createMutation.mutate({ name, email })}
|
||||||
|
disabled={createMutation.isPending}
|
||||||
|
>
|
||||||
|
Create User
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Loading States with Skeleton
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
|
|
||||||
|
export default function DashboardSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-8">
|
||||||
|
<Skeleton className="h-10 w-48 mb-6" />
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<Card key={i}>
|
||||||
|
<CardHeader>
|
||||||
|
<Skeleton className="h-4 w-24" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Skeleton className="h-8 w-20 mb-2" />
|
||||||
|
<Skeleton className="h-3 w-32" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage in main component
|
||||||
|
export default function DashboardPage() {
|
||||||
|
const { data: stats, isLoading } = trpc.dashboard.getStats.useQuery();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <DashboardSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>{/* Actual dashboard */}</div>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Tabs for Different Views
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
|
||||||
|
export default function DataExplorer() {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Data Explorer</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Tabs defaultValue="overview">
|
||||||
|
<TabsList className="grid w-full grid-cols-3">
|
||||||
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||||
|
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
||||||
|
<TabsTrigger value="reports">Reports</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="overview" className="space-y-4">
|
||||||
|
{/* Overview content */}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="analytics" className="space-y-4">
|
||||||
|
{/* Analytics content */}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="reports" className="space-y-4">
|
||||||
|
{/* Reports content */}
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Status Badges
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
|
function StatusBadge({ status }: { status: string }) {
|
||||||
|
const variants = {
|
||||||
|
active: "default",
|
||||||
|
pending: "secondary",
|
||||||
|
inactive: "outline",
|
||||||
|
error: "destructive",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge variant={variants[status as keyof typeof variants] || "outline"}>
|
||||||
|
{status}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
<StatusBadge status="active" />;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Toast Notifications
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
export default function ActionsPage() {
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const deleteMutation = trpc.items.delete.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Success!",
|
||||||
|
description: "Item deleted successfully",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={() => deleteMutation.mutate({ id: 1 })}>
|
||||||
|
Delete Item
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't forget to add <Toaster /> in your App.tsx or layout
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Form with Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z.string().min(2, "Name must be at least 2 characters"),
|
||||||
|
email: z.string().email("Invalid email address"),
|
||||||
|
age: z.number().min(0).max(120),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function UserForm() {
|
||||||
|
const form = useForm({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
age: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createMutation = trpc.users.create.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
form.reset();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (data: z.infer<typeof formSchema>) => {
|
||||||
|
createMutation.mutate(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="John Doe" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Your full name as it appears on documents.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="email" placeholder="john@example.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" disabled={createMutation.isPending}>
|
||||||
|
{createMutation.isPending ? "Submitting..." : "Submit"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Side Sheet for Details
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from "@/components/ui/sheet";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
export default function UserDetailsSheet({ userId }: { userId: number }) {
|
||||||
|
const { data: user } = trpc.users.getById.useQuery({ id: userId });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<Button variant="outline">View Details</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent>
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle>User Details</SheetTitle>
|
||||||
|
<SheetDescription>Information about this user</SheetDescription>
|
||||||
|
</SheetHeader>
|
||||||
|
|
||||||
|
{user && (
|
||||||
|
<div className="mt-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-muted-foreground">
|
||||||
|
Name
|
||||||
|
</h4>
|
||||||
|
<p className="text-base">{user.name}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-muted-foreground">
|
||||||
|
Email
|
||||||
|
</h4>
|
||||||
|
<p className="text-base">{user.email}</p>
|
||||||
|
</div>
|
||||||
|
{/* More fields... */}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10. Command Palette (Search)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
export default function GlobalSearch() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const down = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen((open) => !open);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", down);
|
||||||
|
return () => document.removeEventListener("keydown", down);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||||
|
<CommandInput placeholder="Search..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
|
<CommandGroup heading="Users">
|
||||||
|
<CommandItem>John Doe</CommandItem>
|
||||||
|
<CommandItem>Jane Smith</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
<CommandGroup heading="Projects">
|
||||||
|
<CommandItem>Project Alpha</CommandItem>
|
||||||
|
<CommandItem>Project Beta</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</CommandDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Resources
|
## 🎨 Design Tips
|
||||||
|
|
||||||
|
### Color Schemes
|
||||||
|
|
||||||
|
Use semantic color tokens:
|
||||||
|
|
||||||
|
- `bg-background` / `text-foreground` - Main background and text
|
||||||
|
- `bg-card` / `text-card-foreground` - Card surfaces
|
||||||
|
- `bg-primary` / `text-primary-foreground` - Primary actions
|
||||||
|
- `bg-destructive` - Destructive actions (delete, etc.)
|
||||||
|
- `bg-muted` / `text-muted-foreground` - Subtle UI elements
|
||||||
|
|
||||||
|
### Spacing
|
||||||
|
|
||||||
|
Use consistent spacing:
|
||||||
|
|
||||||
|
- `space-y-4` / `gap-4` - Between related items
|
||||||
|
- `space-y-6` / `gap-6` - Between sections
|
||||||
|
- `p-4` / `p-6` - Card padding
|
||||||
|
- `p-8` - Page padding
|
||||||
|
|
||||||
|
### Icons
|
||||||
|
|
||||||
|
Use `lucide-react` for consistent icons:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Home, Users, Settings, Plus, Edit, Trash, Search } from "lucide-react";
|
||||||
|
|
||||||
|
<Button>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Add Item
|
||||||
|
</Button>;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 Responsive Design
|
||||||
|
|
||||||
|
### Grid Layouts
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1 column on mobile, 2 on tablet, 4 on desktop
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{/* cards */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// 1 column on mobile, 3 on desktop
|
||||||
|
<div className="grid gap-6 lg:grid-cols-3">
|
||||||
|
{/* cards */}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hide on Mobile
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Hide text on mobile, show on desktop
|
||||||
|
<span className="hidden sm:inline">Dashboard</span>
|
||||||
|
|
||||||
|
// Different layout on mobile
|
||||||
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
|
{/* content */}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Performance Tips
|
||||||
|
|
||||||
|
1. **Lazy load dialogs**: Only render dialog content when open
|
||||||
|
2. **Virtualize long lists**: Use libraries like `react-window`
|
||||||
|
3. **Skeleton loaders**: Always show loading states
|
||||||
|
4. **Optimistic updates**: Update UI before server confirms
|
||||||
|
5. **Debounce search**: Don't query on every keystroke
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Resources
|
||||||
|
|
||||||
- **shadcn/ui**: https://ui.shadcn.com/
|
- **shadcn/ui**: https://ui.shadcn.com/
|
||||||
- **Tailwind CSS**: https://tailwindcss.com/
|
- **Tailwind CSS**: https://tailwindcss.com/
|
||||||
- **Lucide Icons**: https://lucide.dev/
|
- **Lucide Icons**: https://lucide.dev/
|
||||||
- **React Hook Form**: https://react-hook-form.com/
|
- **React Hook Form**: https://react-hook-form.com/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Remember**: Always test your components in both light and dark mode, and on different screen sizes!
|
||||||
|
|
|
||||||
|
|
@ -1,566 +0,0 @@
|
||||||
# shadcn/ui — Dashboard Patterns
|
|
||||||
|
|
||||||
Examples of using shadcn/ui components to build dashboard interfaces for TaylorDB applications.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Dashboard Layout with Stats Cards
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
|
|
||||||
export default function DashboardPage() {
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto p-8">
|
|
||||||
<h1 className="text-3xl font-bold mb-6">Dashboard</h1>
|
|
||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
||||||
Total Users
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-3xl font-bold">1,234</div>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
+20% from last month
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
||||||
Revenue
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-3xl font-bold">$45,231</div>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
+12% from last month
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* More stats cards... */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Data Table with Actions
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { MoreHorizontal, Pencil, Trash } from "lucide-react";
|
|
||||||
|
|
||||||
export default function UsersTable({ users }: { users: User[] }) {
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Users</CardTitle>
|
|
||||||
<CardDescription>Manage your user accounts</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Name</TableHead>
|
|
||||||
<TableHead>Email</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{users.map((user) => (
|
|
||||||
<TableRow key={user.id}>
|
|
||||||
<TableCell className="font-medium">{user.name}</TableCell>
|
|
||||||
<TableCell>{user.email}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge
|
|
||||||
variant={user.status === "active" ? "default" : "secondary"}
|
|
||||||
>
|
|
||||||
{user.status}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon">
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<Pencil className="mr-2 h-4 w-4" />
|
|
||||||
Edit
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem className="text-destructive">
|
|
||||||
<Trash className="mr-2 h-4 w-4" />
|
|
||||||
Delete
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Create/Edit Form in Dialog
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { PlusIcon } from "lucide-react";
|
|
||||||
|
|
||||||
export default function CreateUserDialog() {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
const [email, setEmail] = useState("");
|
|
||||||
|
|
||||||
const createMutation = trpc.users.create.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
setOpen(false);
|
|
||||||
setName("");
|
|
||||||
setEmail("");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button>
|
|
||||||
<PlusIcon className="mr-2 h-4 w-4" />
|
|
||||||
Add User
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Create New User</DialogTitle>
|
|
||||||
<DialogDescription>Add a new user to your database</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="grid gap-4 py-4">
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="name">Name</Label>
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
placeholder="John Doe"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="email">Email</Label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
placeholder="john@example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => createMutation.mutate({ name, email })}
|
|
||||||
disabled={createMutation.isPending}
|
|
||||||
>
|
|
||||||
Create User
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Loading States with Skeleton
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
|
||||||
|
|
||||||
export default function DashboardSkeleton() {
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto p-8">
|
|
||||||
<Skeleton className="h-10 w-48 mb-6" />
|
|
||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
|
||||||
{[...Array(4)].map((_, i) => (
|
|
||||||
<Card key={i}>
|
|
||||||
<CardHeader>
|
|
||||||
<Skeleton className="h-4 w-24" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Skeleton className="h-8 w-20 mb-2" />
|
|
||||||
<Skeleton className="h-3 w-32" />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage in main component
|
|
||||||
export default function DashboardPage() {
|
|
||||||
const { data: stats, isLoading } = trpc.dashboard.getStats.useQuery();
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <DashboardSkeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div>{/* Actual dashboard */}</div>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Tabs for Different Views
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
|
|
||||||
export default function DataExplorer() {
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Data Explorer</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Tabs defaultValue="overview">
|
|
||||||
<TabsList className="grid w-full grid-cols-3">
|
|
||||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
||||||
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
|
||||||
<TabsTrigger value="reports">Reports</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="overview" className="space-y-4">
|
|
||||||
{/* Overview content */}
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="analytics" className="space-y-4">
|
|
||||||
{/* Analytics content */}
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="reports" className="space-y-4">
|
|
||||||
{/* Reports content */}
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Status Badges
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
|
|
||||||
function StatusBadge({ status }: { status: string }) {
|
|
||||||
const variants = {
|
|
||||||
active: "default",
|
|
||||||
pending: "secondary",
|
|
||||||
inactive: "outline",
|
|
||||||
error: "destructive",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Badge variant={variants[status as keyof typeof variants] || "outline"}>
|
|
||||||
{status}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage
|
|
||||||
<StatusBadge status="active" />;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Toast Notifications
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
|
|
||||||
export default function ActionsPage() {
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const deleteMutation = trpc.items.delete.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
toast({
|
|
||||||
title: "Success!",
|
|
||||||
description: "Item deleted successfully",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: error.message,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button onClick={() => deleteMutation.mutate({ id: 1 })}>
|
|
||||||
Delete Item
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't forget to add <Toaster /> in your App.tsx or layout
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Form with Validation
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { z } from "zod";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
|
||||||
name: z.string().min(2, "Name must be at least 2 characters"),
|
|
||||||
email: z.string().email("Invalid email address"),
|
|
||||||
age: z.number().min(0).max(120),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function UserForm() {
|
|
||||||
const form = useForm({
|
|
||||||
resolver: zodResolver(formSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "",
|
|
||||||
email: "",
|
|
||||||
age: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const createMutation = trpc.users.create.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
form.reset();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = (data: z.infer<typeof formSchema>) => {
|
|
||||||
createMutation.mutate(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="John Doe" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Your full name as it appears on documents.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Email</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="email" placeholder="john@example.com" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button type="submit" disabled={createMutation.isPending}>
|
|
||||||
{createMutation.isPending ? "Submitting..." : "Submit"}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Side Sheet for Details
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {
|
|
||||||
Sheet,
|
|
||||||
SheetContent,
|
|
||||||
SheetDescription,
|
|
||||||
SheetHeader,
|
|
||||||
SheetTitle,
|
|
||||||
SheetTrigger,
|
|
||||||
} from "@/components/ui/sheet";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
|
|
||||||
export default function UserDetailsSheet({ userId }: { userId: number }) {
|
|
||||||
const { data: user } = trpc.users.getById.useQuery({ id: userId });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Sheet>
|
|
||||||
<SheetTrigger asChild>
|
|
||||||
<Button variant="outline">View Details</Button>
|
|
||||||
</SheetTrigger>
|
|
||||||
<SheetContent>
|
|
||||||
<SheetHeader>
|
|
||||||
<SheetTitle>User Details</SheetTitle>
|
|
||||||
<SheetDescription>Information about this user</SheetDescription>
|
|
||||||
</SheetHeader>
|
|
||||||
|
|
||||||
{user && (
|
|
||||||
<div className="mt-6 space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium text-muted-foreground">
|
|
||||||
Name
|
|
||||||
</h4>
|
|
||||||
<p className="text-base">{user.name}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium text-muted-foreground">
|
|
||||||
Email
|
|
||||||
</h4>
|
|
||||||
<p className="text-base">{user.email}</p>
|
|
||||||
</div>
|
|
||||||
{/* More fields... */}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Command Palette (Search)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {
|
|
||||||
Command,
|
|
||||||
CommandDialog,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
} from "@/components/ui/command";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
|
|
||||||
export default function GlobalSearch() {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const down = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
setOpen((open) => !open);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("keydown", down);
|
|
||||||
return () => document.removeEventListener("keydown", down);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
|
||||||
<CommandInput placeholder="Search..." />
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty>No results found.</CommandEmpty>
|
|
||||||
<CommandGroup heading="Users">
|
|
||||||
<CommandItem>John Doe</CommandItem>
|
|
||||||
<CommandItem>Jane Smith</CommandItem>
|
|
||||||
</CommandGroup>
|
|
||||||
<CommandGroup heading="Projects">
|
|
||||||
<CommandItem>Project Alpha</CommandItem>
|
|
||||||
<CommandItem>Project Beta</CommandItem>
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</CommandDialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For installation and design/layout, see:
|
|
||||||
|
|
||||||
- **Installation**: `SHADCN_INSTALLATION.md`
|
|
||||||
- **Design & layout**: `SHADCN_DESIGN_AND_LAYOUT.md`
|
|
||||||
- **Main guide index**: `SHADCN_COMPONENTS_GUIDE.md`
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
# shadcn/ui — Design & Layout
|
|
||||||
|
|
||||||
Design tokens, spacing, icons, responsive layouts, and performance tips for shadcn/ui dashboards.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Design Tips
|
|
||||||
|
|
||||||
### Color Schemes
|
|
||||||
|
|
||||||
Use semantic color tokens:
|
|
||||||
|
|
||||||
- `bg-background` / `text-foreground` — Main background and text
|
|
||||||
- `bg-card` / `text-card-foreground` — Card surfaces
|
|
||||||
- `bg-primary` / `text-primary-foreground` — Primary actions
|
|
||||||
- `bg-destructive` — Destructive actions (delete, etc.)
|
|
||||||
- `bg-muted` / `text-muted-foreground` — Subtle UI elements
|
|
||||||
|
|
||||||
### Spacing
|
|
||||||
|
|
||||||
Use consistent spacing:
|
|
||||||
|
|
||||||
- `space-y-4` / `gap-4` — Between related items
|
|
||||||
- `space-y-6` / `gap-6` — Between sections
|
|
||||||
- `p-4` / `p-6` — Card padding
|
|
||||||
- `p-8` — Page padding
|
|
||||||
|
|
||||||
### Icons
|
|
||||||
|
|
||||||
Use `lucide-react` for consistent icons:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Home, Users, Settings, Plus, Edit, Trash, Search } from "lucide-react";
|
|
||||||
|
|
||||||
<Button>
|
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
|
||||||
Add Item
|
|
||||||
</Button>;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Responsive Design
|
|
||||||
|
|
||||||
### Grid Layouts
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 1 column on mobile, 2 on tablet, 4 on desktop
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
||||||
{/* cards */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
// 1 column on mobile, 3 on desktop
|
|
||||||
<div className="grid gap-6 lg:grid-cols-3">
|
|
||||||
{/* cards */}
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Hide on Mobile
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Hide text on mobile, show on desktop
|
|
||||||
<span className="hidden sm:inline">Dashboard</span>
|
|
||||||
|
|
||||||
// Different layout on mobile
|
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
|
||||||
{/* content */}
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Performance Tips
|
|
||||||
|
|
||||||
1. **Lazy load dialogs** — Only render dialog content when open
|
|
||||||
2. **Virtualize long lists** — Use libraries like `react-window`
|
|
||||||
3. **Skeleton loaders** — Always show loading states
|
|
||||||
4. **Optimistic updates** — Update UI before server confirms
|
|
||||||
5. **Debounce search** — Don't query on every keystroke
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Remember**: Always test your components in both light and dark mode, and on different screen sizes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For more, see:
|
|
||||||
|
|
||||||
- **Installation**: `SHADCN_INSTALLATION.md`
|
|
||||||
- **Dashboard patterns**: `SHADCN_DASHBOARD_PATTERNS.md`
|
|
||||||
- **Main guide index**: `SHADCN_COMPONENTS_GUIDE.md`
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
# shadcn/ui — Installation
|
|
||||||
|
|
||||||
How to add shadcn/ui components to your TaylorDB app.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Install Individual Components
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Core components (often already included)
|
|
||||||
pnpm dlx shadcn@latest add button card input label textarea select tabs alert
|
|
||||||
|
|
||||||
# Dashboard-specific components
|
|
||||||
pnpm dlx shadcn@latest add table dialog dropdown-menu toast sheet form badge avatar skeleton
|
|
||||||
|
|
||||||
# Data display components
|
|
||||||
pnpm dlx shadcn@latest add separator progress scroll-area tooltip
|
|
||||||
|
|
||||||
# Advanced components
|
|
||||||
pnpm dlx shadcn@latest add command popover calendar date-picker
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For patterns and examples, see:
|
|
||||||
|
|
||||||
- **Dashboard patterns**: `SHADCN_DASHBOARD_PATTERNS.md`
|
|
||||||
- **Design & layout**: `SHADCN_DESIGN_AND_LAYOUT.md`
|
|
||||||
- **Main guide index**: `SHADCN_COMPONENTS_GUIDE.md`
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
# TaylorDB Advanced Patterns
|
|
||||||
|
|
||||||
This document covers **advanced query patterns** using the TaylorDB query builder:
|
|
||||||
|
|
||||||
- Manual aggregations
|
|
||||||
- Summation helpers
|
|
||||||
- Conditional queries
|
|
||||||
- Pagination
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Aggregations (Manual)
|
|
||||||
|
|
||||||
Since TaylorDB query builder might not have built-in aggregations, compute manually:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function getUserStats() {
|
|
||||||
const users = await queryBuilder
|
|
||||||
.selectFrom("users")
|
|
||||||
.select(["age"])
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
if (users.length === 0) {
|
|
||||||
return { count: 0, average: null, min: null, max: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
const ages = users
|
|
||||||
.map((u) => u.age)
|
|
||||||
.filter((a): a is number => a !== undefined);
|
|
||||||
|
|
||||||
return {
|
|
||||||
count: ages.length,
|
|
||||||
average: ages.reduce((a, b) => a + b, 0) / ages.length,
|
|
||||||
min: Math.min(...ages),
|
|
||||||
max: Math.max(...ages),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sum Totals
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function getTotalCaloriesForDate(date: string) {
|
|
||||||
const entries = await queryBuilder
|
|
||||||
.selectFrom("meals")
|
|
||||||
.select(["calories", "protein", "carbs", "fats"])
|
|
||||||
.where("date", "=", ["exactDay", date])
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
return {
|
|
||||||
totalCalories: entries.reduce((sum, e) => sum + (e.calories ?? 0), 0),
|
|
||||||
totalProtein: entries.reduce((sum, e) => sum + (e.protein ?? 0), 0),
|
|
||||||
totalCarbs: entries.reduce((sum, e) => sum + (e.carbs ?? 0), 0),
|
|
||||||
totalFats: entries.reduce((sum, e) => sum + (e.fats ?? 0), 0),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Conditional Queries
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function searchTasks(filters: {
|
|
||||||
projectId?: number;
|
|
||||||
status?: string;
|
|
||||||
dueAfter?: string;
|
|
||||||
}) {
|
|
||||||
let query = queryBuilder
|
|
||||||
.selectFrom("tasks")
|
|
||||||
.select(["id", "title", "status", "dueDate"]);
|
|
||||||
|
|
||||||
if (filters.projectId) {
|
|
||||||
query = query.where("projectId", "=", filters.projectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.status) {
|
|
||||||
query = query.where("status", "=", filters.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.dueAfter) {
|
|
||||||
query = query.where("dueDate", ">=", ["exactDay", filters.dueAfter]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query.execute();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Pagination
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function getPaginatedUsers(page: number, pageSize: number) {
|
|
||||||
const offset = (page - 1) * pageSize;
|
|
||||||
|
|
||||||
return await queryBuilder
|
|
||||||
.selectFrom("users")
|
|
||||||
.select(["id", "name", "email"])
|
|
||||||
.orderBy("createdAt", "desc")
|
|
||||||
.limit(pageSize)
|
|
||||||
.offset(offset)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For more topics, see:
|
|
||||||
|
|
||||||
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
|
|
||||||
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
|
|
||||||
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums
|
|
||||||
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices
|
|
||||||
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
# TaylorDB Attachments
|
|
||||||
|
|
||||||
Attachments are treated as **standard columns** and can be selected and written like other fields, using helper utilities for uploads.
|
|
||||||
|
|
||||||
This document covers:
|
|
||||||
|
|
||||||
- Selecting attachment fields
|
|
||||||
- Creating records with attachments
|
|
||||||
- Updating attachments
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Select Attachments
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// New Standard: Use regular .select() like any other field.
|
|
||||||
const expenses = await qb
|
|
||||||
.selectFrom("expenses")
|
|
||||||
.select(["id", "amount", "receipt"])
|
|
||||||
.execute();
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Create with Attachments
|
|
||||||
|
|
||||||
Use `qb.uploadAttachments` to upload files before inserting.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
await qb
|
|
||||||
.insertInto("customers")
|
|
||||||
.values({
|
|
||||||
firstName: "Jane",
|
|
||||||
lastName: "Doe",
|
|
||||||
avatar: await qb.uploadAttachments([
|
|
||||||
{ file: new Blob([""]), name: "test.png" },
|
|
||||||
]),
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Update with Attachments
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
await qb
|
|
||||||
.update("customers")
|
|
||||||
.set({
|
|
||||||
lastName: "Smith",
|
|
||||||
avatar: await qb.uploadAttachments([
|
|
||||||
{ file: new Blob([""]), name: "test.png" },
|
|
||||||
]),
|
|
||||||
})
|
|
||||||
.where("id", "=", 1)
|
|
||||||
.execute();
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For more topics, see:
|
|
||||||
|
|
||||||
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
|
|
||||||
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
|
|
||||||
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
|
|
||||||
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums
|
|
||||||
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices
|
|
||||||
|
|
||||||
|
|
@ -1,187 +0,0 @@
|
||||||
# TaylorDB Basic Queries & Filtering
|
|
||||||
|
|
||||||
This document covers **core read operations** using the TaylorDB query builder:
|
|
||||||
|
|
||||||
- **Basic select queries**
|
|
||||||
- **Ordering**
|
|
||||||
- **Filtering with where clauses**
|
|
||||||
- **Date filters**
|
|
||||||
- **Array and select field filters**
|
|
||||||
- **Simple text search**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Basic Queries
|
|
||||||
|
|
||||||
### Get All Records
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function getAllUsers() {
|
|
||||||
return await queryBuilder
|
|
||||||
.selectFrom("users")
|
|
||||||
.select(["id", "name", "email", "createdAt"])
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Get All Records (All Fields)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function getAllUsers() {
|
|
||||||
return await queryBuilder.selectFrom("users").execute();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Get Single Record by ID
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function getUserById(id: number) {
|
|
||||||
return await queryBuilder
|
|
||||||
.selectFrom("users")
|
|
||||||
.where("id", "=", id)
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note**: `.executeTakeFirst()` returns a single record or `undefined`.
|
|
||||||
|
|
||||||
### Get Records with Ordering
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Descending order (newest first)
|
|
||||||
export async function getRecentUsers() {
|
|
||||||
return await queryBuilder
|
|
||||||
.selectFrom("users")
|
|
||||||
.orderBy("createdAt", "desc")
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ascending order (oldest first)
|
|
||||||
export async function getOldestUsers() {
|
|
||||||
return await queryBuilder
|
|
||||||
.selectFrom("users")
|
|
||||||
.orderBy("createdAt", "asc")
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Filtering & Conditions
|
|
||||||
|
|
||||||
### Basic Where Clauses
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Exact match
|
|
||||||
.where("status", "=", "active")
|
|
||||||
|
|
||||||
// Not equal
|
|
||||||
.where("status", "!=", "deleted")
|
|
||||||
|
|
||||||
// Greater than / Less than
|
|
||||||
.where("age", ">", 18)
|
|
||||||
.where("age", ">=", 18)
|
|
||||||
.where("age", "<", 65)
|
|
||||||
.where("age", "<=", 65)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Multiple Conditions (AND logic)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function getActiveAdults() {
|
|
||||||
return await queryBuilder
|
|
||||||
.selectFrom("users")
|
|
||||||
.where("status", "=", "active")
|
|
||||||
.where("age", ">=", 18)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Date Filtering
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Exact date
|
|
||||||
export async function getUsersForDate(date: string) {
|
|
||||||
return await queryBuilder
|
|
||||||
.selectFrom("users")
|
|
||||||
.where("createdAt", "=", ["exactDay", date])
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date range
|
|
||||||
export async function getUsersInRange(startDate: string, endDate: string) {
|
|
||||||
return await queryBuilder
|
|
||||||
.selectFrom("users")
|
|
||||||
.where("createdAt", ">=", ["exactDay", startDate])
|
|
||||||
.where("createdAt", "<=", ["exactDay", endDate])
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Before/After a date
|
|
||||||
.where("dueDate", "<", ["exactDay", "2024-01-01"])
|
|
||||||
.where("startDate", ">", ["exactDay", "2024-12-31"])
|
|
||||||
```
|
|
||||||
|
|
||||||
### Array/Multi-Select Filtering
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Check if array contains any of the values
|
|
||||||
export async function getUsersByTags(tags: string[]) {
|
|
||||||
return await queryBuilder
|
|
||||||
.selectFrom("users")
|
|
||||||
.where("tags", "hasAnyOf", tags)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Example: Get users tagged with "admin" OR "moderator"
|
|
||||||
const adminUsers = await getUsersByTags(["admin", "moderator"]);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Select Field Filtering
|
|
||||||
|
|
||||||
#### Single Select
|
|
||||||
|
|
||||||
For single-select fields, the query builder now returns a single string value.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function getUsersByRole(role: string) {
|
|
||||||
return await queryBuilder
|
|
||||||
.selectFrom("users")
|
|
||||||
.where("role", "=", role)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Multi Select
|
|
||||||
|
|
||||||
For multi-select fields, the query builder returns and accepts multiple values.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function getUsersByInterests(interests: string[]) {
|
|
||||||
return await queryBuilder
|
|
||||||
.selectFrom("users")
|
|
||||||
.where("interests", "hasAnyOf", interests)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Text Search (Contains)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function searchUsersByName(query: string) {
|
|
||||||
return await queryBuilder
|
|
||||||
.selectFrom("users")
|
|
||||||
.where("name", "contains", query)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For more topics, see:
|
|
||||||
|
|
||||||
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
|
|
||||||
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
|
|
||||||
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums
|
|
||||||
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices
|
|
||||||
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
# TaylorDB Field Types & Enums
|
|
||||||
|
|
||||||
This document explains how TaylorDB field types map to TypeScript and how to handle them correctly:
|
|
||||||
|
|
||||||
- Field type reference
|
|
||||||
- Nullable fields
|
|
||||||
- Enums and options from generated types
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Field Type Reference
|
|
||||||
|
|
||||||
| TaylorDB Field Type | TypeScript Type | Insert Value | Query Value |
|
|
||||||
| ------------------- | ----------------------- | --------------------- | ---------------------------- |
|
|
||||||
| **Text** | `string` | `"Hello"` | `"Hello"` |
|
|
||||||
| **Number** | `number` | `42` | `42` |
|
|
||||||
| **Date** | `string` (ISO) | `"2024-01-15"` | `["exactDay", "2024-01-15"]` |
|
|
||||||
| **Checkbox** | `boolean` | `true` | `true` |
|
|
||||||
| **Single Select** | `string` | `"option"` | `"option"` |
|
|
||||||
| **Multi Select** | `string[]` | `["opt1", "opt2"]` | `["opt1", "opt2"]` |
|
|
||||||
| **Attachment** | `string[]` (File Paths) | `uploadAttachments()` | `"file-path"` |
|
|
||||||
| **Email** | `string` | `"user@example.com"` | `"user@example.com"` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Handling Nullable Fields
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function createUserSafe(data: {
|
|
||||||
name: string;
|
|
||||||
email?: string | null;
|
|
||||||
age?: number | null;
|
|
||||||
}) {
|
|
||||||
return await queryBuilder
|
|
||||||
.insertInto("users")
|
|
||||||
.values({
|
|
||||||
name: data.name,
|
|
||||||
email: data.email ?? "", // Default to empty string
|
|
||||||
age: data.age ?? 0, // Default to 0
|
|
||||||
})
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Working with Enums
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Import from generated types
|
|
||||||
import type { TaskStatusOptions } from "./types";
|
|
||||||
|
|
||||||
export async function createTask(data: {
|
|
||||||
title: string;
|
|
||||||
status: (typeof TaskStatusOptions)[number]; // e.g., "todo" | "in-progress" | "done"
|
|
||||||
}) {
|
|
||||||
return await queryBuilder
|
|
||||||
.insertInto("tasks")
|
|
||||||
.values({
|
|
||||||
title: data.title,
|
|
||||||
status: data.status,
|
|
||||||
})
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getTasksByStatus(
|
|
||||||
status: (typeof TaskStatusOptions)[number],
|
|
||||||
) {
|
|
||||||
return await queryBuilder
|
|
||||||
.selectFrom("tasks")
|
|
||||||
.where("status", "=", status)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For more topics, see:
|
|
||||||
|
|
||||||
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
|
|
||||||
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
|
|
||||||
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
|
|
||||||
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices
|
|
||||||
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
# TaylorDB Pitfalls & Best Practices
|
|
||||||
|
|
||||||
This document captures **common mistakes** and **recommended patterns** when using the TaylorDB query builder.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common Pitfalls
|
|
||||||
|
|
||||||
### ❌ Pitfall: Not Using exactDay for Dates
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ❌ WRONG
|
|
||||||
.where("date", "=", "2024-01-15")
|
|
||||||
|
|
||||||
// ✅ CORRECT
|
|
||||||
.where("date", "=", ["exactDay", "2024-01-15"])
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ Pitfall: Ignoring Nullable Fields
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ❌ WRONG (assumes field is always present)
|
|
||||||
const user = await queryBuilder
|
|
||||||
.selectFrom("users")
|
|
||||||
.where("id", "=", 1)
|
|
||||||
.executeTakeFirst();
|
|
||||||
console.log(user.email); // Could be undefined!
|
|
||||||
|
|
||||||
// ✅ CORRECT
|
|
||||||
const user = await queryBuilder
|
|
||||||
.selectFrom("users")
|
|
||||||
.where("id", "=", 1)
|
|
||||||
.executeTakeFirst();
|
|
||||||
if (user && user.email) {
|
|
||||||
console.log(user.email);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ Pitfall: Using execute() for Single Record
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ❌ WRONG (returns array)
|
|
||||||
const user = await queryBuilder
|
|
||||||
.selectFrom("users")
|
|
||||||
.where("id", "=", 1)
|
|
||||||
.execute();
|
|
||||||
console.log(user.name); // Error: user is an array!
|
|
||||||
|
|
||||||
// ✅ CORRECT
|
|
||||||
const user = await queryBuilder
|
|
||||||
.selectFrom("users")
|
|
||||||
.where("id", "=", 1)
|
|
||||||
.executeTakeFirst();
|
|
||||||
if (user) {
|
|
||||||
console.log(user.name);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ Pitfall: Not Handling Empty Arrays
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ❌ WRONG (fails if users is empty)
|
|
||||||
const ages = users.map((u) => u.age);
|
|
||||||
const avg = ages.reduce((a, b) => a + b) / ages.length; // Division by zero!
|
|
||||||
|
|
||||||
// ✅ CORRECT
|
|
||||||
if (users.length === 0) {
|
|
||||||
return { average: null };
|
|
||||||
}
|
|
||||||
const ages = users
|
|
||||||
.map((u) => u.age)
|
|
||||||
.filter((a): a is number => a !== undefined);
|
|
||||||
const avg = ages.reduce((a, b) => a + b, 0) / ages.length;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Always handle `undefined` and `null`** when working with query results.
|
|
||||||
2. **Use TypeScript types** from `taylordb/types.ts` for type safety.
|
|
||||||
3. **Use `executeTakeFirst()`** when you expect a single record.
|
|
||||||
4. **Filter nullish values** before aggregations.
|
|
||||||
5. **Provide defaults** for optional fields.
|
|
||||||
6. **Use `["exactDay", date]`** format for date comparisons.
|
|
||||||
7. **Group related queries** in the same function file.
|
|
||||||
8. **Export functions**, not raw queries.
|
|
||||||
9. **Document complex queries** with JSDoc comments.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For more topics, see:
|
|
||||||
|
|
||||||
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
|
|
||||||
- `TAYLORDB_WRITE_OPERATIONS.md` for inserts, updates, and deletes
|
|
||||||
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
|
|
||||||
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums
|
|
||||||
|
|
||||||
|
|
@ -1,78 +1,700 @@
|
||||||
# TaylorDB Query Builder Reference
|
# TaylorDB Query Builder Reference
|
||||||
|
|
||||||
This is the **entry point** for all TaylorDB query builder docs in this template.
|
This document provides comprehensive examples of how to use the TaylorDB query builder for all common database operations.
|
||||||
The content has been split into smaller, focused files to make it easier for agents (and humans) to scan and reuse.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📚 Topics
|
## 📚 Table of Contents
|
||||||
|
|
||||||
- **Basic Reads & Filtering**
|
1. [Setup & Configuration](#setup--configuration)
|
||||||
See `TAYLORDB_BASIC_QUERIES.md` for:
|
2. [Basic Queries](#basic-queries)
|
||||||
- Basic `selectFrom` usage
|
3. [Filtering & Conditions](#filtering--conditions)
|
||||||
- Ordering
|
4. [Inserting Data](#inserting-data)
|
||||||
- `where` clauses
|
5. [Updating Data](#updating-data)
|
||||||
- Date filters
|
6. [Deleting Data](#deleting-data)
|
||||||
- Array/select field filters
|
7. [Advanced Patterns](#advanced-patterns)
|
||||||
- Text search (`contains`)
|
8. [Field Type Handling](#field-type-handling)
|
||||||
|
9. [Common Pitfalls](#common-pitfalls)
|
||||||
- **Write Operations (Insert, Update, Delete)**
|
|
||||||
See `TAYLORDB_WRITE_OPERATIONS.md` for:
|
|
||||||
- Inserting records (including single-/multi-select fields)
|
|
||||||
- Updates (single/multiple fields, conditional updates)
|
|
||||||
- Bulk updates
|
|
||||||
- Deleting single/multiple records and conditional deletes
|
|
||||||
|
|
||||||
- **Advanced Patterns**
|
|
||||||
See `TAYLORDB_ADVANCED_PATTERNS.md` for:
|
|
||||||
- Manual aggregations
|
|
||||||
- Summation helpers
|
|
||||||
- Conditional query builders
|
|
||||||
- Pagination patterns
|
|
||||||
|
|
||||||
- **Field Types & Enums**
|
|
||||||
See `TAYLORDB_FIELD_TYPES.md` for:
|
|
||||||
- TaylorDB field type → TypeScript mappings
|
|
||||||
- Nullable field handling
|
|
||||||
- Using generated enum options (`...Options`) types
|
|
||||||
|
|
||||||
- **Attachments**
|
|
||||||
See `TAYLORDB_ATTACHMENTS.md` for:
|
|
||||||
- Selecting attachment fields
|
|
||||||
- Creating/updating records with attachments via `uploadAttachments`
|
|
||||||
|
|
||||||
- **Pitfalls & Best Practices**
|
|
||||||
See `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for:
|
|
||||||
- Common mistakes (e.g., forgetting `["exactDay", date]`, misusing `execute`)
|
|
||||||
- Recommended patterns for safe, type-accurate queries
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## How Agents Should Use These Docs
|
## Setup & Configuration
|
||||||
|
|
||||||
1. **Start from your use case**
|
### Initialize Query Builder
|
||||||
- Need a simple read? Open `TAYLORDB_BASIC_QUERIES.md`.
|
|
||||||
- Doing writes? Use `TAYLORDB_WRITE_OPERATIONS.md`.
|
|
||||||
- Need aggregations or pagination? Use `TAYLORDB_ADVANCED_PATTERNS.md`.
|
|
||||||
|
|
||||||
2. **Combine with generated types**
|
```typescript
|
||||||
Always cross-reference:
|
import { createQueryBuilder } from "@taylordb/query-builder";
|
||||||
- `apps/server/taylordb/types.ts` (schema-derived types)
|
import type { TaylorDatabase } from "./types.js";
|
||||||
- `apps/server/taylordb/query-builder.ts` (project-specific query functions)
|
|
||||||
|
|
||||||
3. **Check pitfalls before finalizing**
|
export const queryBuilder = createQueryBuilder<TaylorDatabase>({
|
||||||
Before shipping queries, skim `TAYLORDB_PITFALLS_BEST_PRACTICES.md` to avoid common errors.
|
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
|
## Additional Resources
|
||||||
|
|
||||||
- **Generated Types**: `apps/server/taylordb/types.ts`
|
- **Generated Types**: Check `apps/server/taylordb/types.ts` for your schema
|
||||||
- **Example Queries in This Template**: `apps/server/taylordb/query-builder.ts`
|
- **Example Queries**: See `apps/server/taylordb/query-builder.ts`
|
||||||
- **tRPC Integration**: `apps/server/router.ts`
|
- **tRPC Integration**: See `apps/server/router.ts`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Note**: These docs mirror the TaylorDB query builder patterns used in this template.
|
**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.
|
||||||
For the most up-to-date API details, always refer to the official TaylorDB documentation.
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,265 +0,0 @@
|
||||||
# TaylorDB Write Operations (Insert, Update, Delete)
|
|
||||||
|
|
||||||
This document covers **write operations** using the TaylorDB query builder:
|
|
||||||
|
|
||||||
- **Inserting data**
|
|
||||||
- **Updating single/multiple records**
|
|
||||||
- **Deleting records**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Inserting Data
|
|
||||||
|
|
||||||
### Insert Single Record
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function createUser(data: {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
age: number;
|
|
||||||
}) {
|
|
||||||
return await queryBuilder
|
|
||||||
.insertInto("users")
|
|
||||||
.values({
|
|
||||||
name: data.name,
|
|
||||||
email: data.email,
|
|
||||||
age: data.age,
|
|
||||||
status: "active", // Default value
|
|
||||||
})
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Returns**: The created record with its generated `id`.
|
|
||||||
|
|
||||||
### Insert with Single-Select Field
|
|
||||||
|
|
||||||
Single-select fields now accept a single string value directly.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function createTask(data: {
|
|
||||||
title: string;
|
|
||||||
priority: "low" | "medium" | "high";
|
|
||||||
}) {
|
|
||||||
return await queryBuilder
|
|
||||||
.insertInto("tasks")
|
|
||||||
.values({
|
|
||||||
title: data.title,
|
|
||||||
priority: data.priority,
|
|
||||||
})
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Insert with Multi-Select Field
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function createProject(data: { name: string; tags: string[] }) {
|
|
||||||
return await queryBuilder
|
|
||||||
.insertInto("projects")
|
|
||||||
.values({
|
|
||||||
name: data.name,
|
|
||||||
tags: data.tags, // Already an array
|
|
||||||
})
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Insert with Computed Fields
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function createCardioSession(data: {
|
|
||||||
distance: number;
|
|
||||||
duration: number; // in minutes
|
|
||||||
}) {
|
|
||||||
const speed = data.distance / (data.duration / 60); // km/h
|
|
||||||
|
|
||||||
return await queryBuilder
|
|
||||||
.insertInto("cardio")
|
|
||||||
.values({
|
|
||||||
distance: data.distance,
|
|
||||||
duration: data.duration,
|
|
||||||
speed: speed, // Computed field
|
|
||||||
})
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Insert with Optional Fields
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function createPost(data: {
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
tags?: string[];
|
|
||||||
}) {
|
|
||||||
return await queryBuilder
|
|
||||||
.insertInto("posts")
|
|
||||||
.values({
|
|
||||||
title: data.title,
|
|
||||||
content: data.content,
|
|
||||||
tags: data.tags || [], // Default to empty array
|
|
||||||
})
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Updating Data
|
|
||||||
|
|
||||||
### Update Single Field
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function updateUserName(id: number, name: string) {
|
|
||||||
return await queryBuilder
|
|
||||||
.update("users")
|
|
||||||
.set({ name })
|
|
||||||
.where("id", "=", id)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Update Multiple Fields
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function updateUser(
|
|
||||||
id: number,
|
|
||||||
data: {
|
|
||||||
name?: string;
|
|
||||||
email?: string;
|
|
||||||
age?: number;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
return await queryBuilder
|
|
||||||
.update("users")
|
|
||||||
.set(data)
|
|
||||||
.where("id", "=", id)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note**: Only provided fields will be updated.
|
|
||||||
|
|
||||||
### Update with Single-Select Field
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function updateTaskPriority(
|
|
||||||
id: number,
|
|
||||||
priority: "low" | "medium" | "high",
|
|
||||||
) {
|
|
||||||
return await queryBuilder
|
|
||||||
.update("tasks")
|
|
||||||
.set({ priority })
|
|
||||||
.where("id", "=", id)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Update with Conditional Logic
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function updateCardioSession(
|
|
||||||
id: number,
|
|
||||||
data: {
|
|
||||||
distance?: number;
|
|
||||||
duration?: number;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
// Fetch current record to compute speed
|
|
||||||
const currentRecord = await queryBuilder
|
|
||||||
.selectFrom("cardio")
|
|
||||||
.select(["distance", "duration"])
|
|
||||||
.where("id", "=", id)
|
|
||||||
.executeTakeFirst();
|
|
||||||
|
|
||||||
if (!currentRecord) {
|
|
||||||
throw new Error("Record not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const newDistance = data.distance ?? currentRecord.distance ?? 0;
|
|
||||||
const newDuration = data.duration ?? currentRecord.duration ?? 0;
|
|
||||||
const speed = newDistance / (newDuration / 60);
|
|
||||||
|
|
||||||
return await queryBuilder
|
|
||||||
.update("cardio")
|
|
||||||
.set({
|
|
||||||
...data,
|
|
||||||
speed,
|
|
||||||
})
|
|
||||||
.where("id", "=", id)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Update Multiple Records
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function activateAllUsers() {
|
|
||||||
return await queryBuilder.update("users").set({ status: "active" }).execute(); // No where clause = update all
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update with condition
|
|
||||||
export async function activateInactiveUsers() {
|
|
||||||
return await queryBuilder
|
|
||||||
.update("users")
|
|
||||||
.set({ status: "active" })
|
|
||||||
.where("status", "=", "inactive")
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Deleting Data
|
|
||||||
|
|
||||||
### Delete Single Record
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function deleteUser(id: number) {
|
|
||||||
return await queryBuilder.deleteFrom("users").where("id", "=", id).execute();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Delete Multiple Records by IDs
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function deleteUsers(ids: number[]) {
|
|
||||||
return await queryBuilder
|
|
||||||
.deleteFrom("users")
|
|
||||||
.where("id", "hasAnyOf", ids)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Delete with Condition
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function deleteInactiveUsers() {
|
|
||||||
return await queryBuilder
|
|
||||||
.deleteFrom("users")
|
|
||||||
.where("status", "=", "inactive")
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Delete Old Records
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function deleteOldLogs(beforeDate: string) {
|
|
||||||
return await queryBuilder
|
|
||||||
.deleteFrom("logs")
|
|
||||||
.where("createdAt", "<", ["exactDay", beforeDate])
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For more topics, see:
|
|
||||||
|
|
||||||
- `TAYLORDB_BASIC_QUERIES.md` for basic reads and filtering
|
|
||||||
- `TAYLORDB_ADVANCED_PATTERNS.md` for aggregations, pagination, and conditional queries
|
|
||||||
- `TAYLORDB_FIELD_TYPES.md` for field type handling and enums
|
|
||||||
- `TAYLORDB_PITFALLS_BEST_PRACTICES.md` for pitfalls and best practices
|
|
||||||
|
|
||||||
199
pnpm-lock.yaml
199
pnpm-lock.yaml
|
|
@ -139,39 +139,27 @@ importers:
|
||||||
apps/server:
|
apps/server:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@taylordb/query-builder':
|
'@taylordb/query-builder':
|
||||||
specifier: ^0.11.4
|
specifier: ^0.10.3
|
||||||
version: 0.11.4
|
version: 0.10.3
|
||||||
'@trpc/server':
|
'@trpc/server':
|
||||||
specifier: ^11.8.1
|
specifier: ^11.8.1
|
||||||
version: 11.8.1(typescript@5.9.3)
|
version: 11.8.1(typescript@5.9.3)
|
||||||
cookie-parser:
|
|
||||||
specifier: ^1.4.7
|
|
||||||
version: 1.4.7
|
|
||||||
cors:
|
cors:
|
||||||
specifier: ^2.8.5
|
specifier: ^2.8.5
|
||||||
version: 2.8.5
|
version: 2.8.5
|
||||||
express:
|
express:
|
||||||
specifier: ^5.2.1
|
specifier: ^5.2.1
|
||||||
version: 5.2.1
|
version: 5.2.1
|
||||||
multer:
|
|
||||||
specifier: ^2.0.2
|
|
||||||
version: 2.0.2
|
|
||||||
zod:
|
zod:
|
||||||
specifier: ^4.3.5
|
specifier: ^4.3.5
|
||||||
version: 4.3.5
|
version: 4.3.5
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/cookie-parser':
|
|
||||||
specifier: ^1.4.10
|
|
||||||
version: 1.4.10(@types/express@5.0.6)
|
|
||||||
'@types/cors':
|
'@types/cors':
|
||||||
specifier: ^2.8.19
|
specifier: ^2.8.19
|
||||||
version: 2.8.19
|
version: 2.8.19
|
||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: ^5.0.6
|
specifier: ^5.0.6
|
||||||
version: 5.0.6
|
version: 5.0.6
|
||||||
'@types/multer':
|
|
||||||
specifier: ^2.0.0
|
|
||||||
version: 2.0.0
|
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^24.10.1
|
specifier: ^24.10.1
|
||||||
version: 24.10.1
|
version: 24.10.1
|
||||||
|
|
@ -1285,15 +1273,9 @@ packages:
|
||||||
'@taylordb/query-builder@0.10.3':
|
'@taylordb/query-builder@0.10.3':
|
||||||
resolution: {integrity: sha512-GwsrSdCQelTvghPY4uzoSWHI2vsWFOFSOb4IKRS0qQT8Z6jG8VWMOSRwocvcMoQfOf2RqCDt9Wew1dK6FZNBHA==}
|
resolution: {integrity: sha512-GwsrSdCQelTvghPY4uzoSWHI2vsWFOFSOb4IKRS0qQT8Z6jG8VWMOSRwocvcMoQfOf2RqCDt9Wew1dK6FZNBHA==}
|
||||||
|
|
||||||
'@taylordb/query-builder@0.11.4':
|
|
||||||
resolution: {integrity: sha512-5hGFtxTKtRK76BUq08c95lqzYBcCTNRm1W04z1P8ufAVvSrr9Iuq1sPf8D7ayBJrAZ8KHThwvAR9w4n0vqiwYQ==}
|
|
||||||
|
|
||||||
'@taylordb/shared@0.4.4':
|
'@taylordb/shared@0.4.4':
|
||||||
resolution: {integrity: sha512-Xykr4I26JapNLePkapBGjz15t9Ep1iLs30VbfCcc2z30x8Qy/2tm+sSa5LcacCP2EaxNVDp2UuYKhZ7kOWBLBQ==}
|
resolution: {integrity: sha512-Xykr4I26JapNLePkapBGjz15t9Ep1iLs30VbfCcc2z30x8Qy/2tm+sSa5LcacCP2EaxNVDp2UuYKhZ7kOWBLBQ==}
|
||||||
|
|
||||||
'@taylordb/shared@0.5.4':
|
|
||||||
resolution: {integrity: sha512-85tqzVQXsbx3rUAhpcdeA/BKHaHEFgpMxjmRAb12bD4U4ghGrcFw1eyaq+/YtoBfiWChPCruCK+d03e+rEMoiA==}
|
|
||||||
|
|
||||||
'@trpc/client@11.8.1':
|
'@trpc/client@11.8.1':
|
||||||
resolution: {integrity: sha512-L/SJFGanr9xGABmuDoeXR4xAdHJmsXsiF9OuH+apecJ+8sUITzVT1EPeqp0ebqA6lBhEl5pPfg3rngVhi/h60Q==}
|
resolution: {integrity: sha512-L/SJFGanr9xGABmuDoeXR4xAdHJmsXsiF9OuH+apecJ+8sUITzVT1EPeqp0ebqA6lBhEl5pPfg3rngVhi/h60Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -1333,11 +1315,6 @@ packages:
|
||||||
'@types/connect@3.4.38':
|
'@types/connect@3.4.38':
|
||||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||||
|
|
||||||
'@types/cookie-parser@1.4.10':
|
|
||||||
resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/express': '*'
|
|
||||||
|
|
||||||
'@types/cors@2.8.19':
|
'@types/cors@2.8.19':
|
||||||
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
|
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
|
||||||
|
|
||||||
|
|
@ -1383,9 +1360,6 @@ packages:
|
||||||
'@types/json-schema@7.0.15':
|
'@types/json-schema@7.0.15':
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
'@types/multer@2.0.0':
|
|
||||||
resolution: {integrity: sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==}
|
|
||||||
|
|
||||||
'@types/node@24.10.1':
|
'@types/node@24.10.1':
|
||||||
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
|
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
|
||||||
|
|
||||||
|
|
@ -1502,9 +1476,6 @@ packages:
|
||||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
append-field@1.0.0:
|
|
||||||
resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==}
|
|
||||||
|
|
||||||
argparse@2.0.1:
|
argparse@2.0.1:
|
||||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
|
|
||||||
|
|
@ -1538,13 +1509,6 @@ packages:
|
||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
buffer-from@1.1.2:
|
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
|
||||||
|
|
||||||
busboy@1.6.0:
|
|
||||||
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
|
||||||
engines: {node: '>=10.16.0'}
|
|
||||||
|
|
||||||
bytes@3.1.2:
|
bytes@3.1.2:
|
||||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
@ -1589,10 +1553,6 @@ packages:
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
|
|
||||||
concat-stream@2.0.0:
|
|
||||||
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
|
|
||||||
engines: {'0': node >= 6.0}
|
|
||||||
|
|
||||||
concurrently@9.2.1:
|
concurrently@9.2.1:
|
||||||
resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==}
|
resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
@ -1609,13 +1569,6 @@ packages:
|
||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
cookie-parser@1.4.7:
|
|
||||||
resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==}
|
|
||||||
engines: {node: '>= 0.8.0'}
|
|
||||||
|
|
||||||
cookie-signature@1.0.6:
|
|
||||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
|
||||||
|
|
||||||
cookie-signature@1.2.2:
|
cookie-signature@1.2.2:
|
||||||
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
||||||
engines: {node: '>=6.6.0'}
|
engines: {node: '>=6.6.0'}
|
||||||
|
|
@ -2171,10 +2124,6 @@ packages:
|
||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
media-typer@0.3.0:
|
|
||||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
|
||||||
engines: {node: '>= 0.6'}
|
|
||||||
|
|
||||||
media-typer@1.1.0:
|
media-typer@1.1.0:
|
||||||
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
|
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
@ -2191,18 +2140,10 @@ packages:
|
||||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
|
|
||||||
mime-db@1.52.0:
|
|
||||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
|
||||||
engines: {node: '>= 0.6'}
|
|
||||||
|
|
||||||
mime-db@1.54.0:
|
mime-db@1.54.0:
|
||||||
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
|
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
mime-types@2.1.35:
|
|
||||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
|
||||||
engines: {node: '>= 0.6'}
|
|
||||||
|
|
||||||
mime-types@3.0.2:
|
mime-types@3.0.2:
|
||||||
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
|
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
@ -2214,20 +2155,9 @@ packages:
|
||||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
minimist@1.2.8:
|
|
||||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
|
||||||
|
|
||||||
mkdirp@0.5.6:
|
|
||||||
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
multer@2.0.2:
|
|
||||||
resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==}
|
|
||||||
engines: {node: '>= 10.16.0'}
|
|
||||||
|
|
||||||
nanoid@3.3.11:
|
nanoid@3.3.11:
|
||||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
|
|
@ -2402,10 +2332,6 @@ packages:
|
||||||
resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
|
resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
readable-stream@3.6.2:
|
|
||||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
|
||||||
engines: {node: '>= 6'}
|
|
||||||
|
|
||||||
recharts@3.5.0:
|
recharts@3.5.0:
|
||||||
resolution: {integrity: sha512-jWqBtu8L3VICXWa3g/y+bKjL8DDHSRme7DHD/70LQ/Tk0di1h11Y0kKC0nPh6YJ2oaa0k6anIFNhg6SfzHWdEA==}
|
resolution: {integrity: sha512-jWqBtu8L3VICXWa3g/y+bKjL8DDHSRme7DHD/70LQ/Tk0di1h11Y0kKC0nPh6YJ2oaa0k6anIFNhg6SfzHWdEA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
@ -2455,9 +2381,6 @@ packages:
|
||||||
rxjs@7.8.2:
|
rxjs@7.8.2:
|
||||||
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
||||||
|
|
||||||
safe-buffer@5.2.1:
|
|
||||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
|
||||||
|
|
||||||
safer-buffer@2.1.2:
|
safer-buffer@2.1.2:
|
||||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
|
|
||||||
|
|
@ -2528,17 +2451,10 @@ packages:
|
||||||
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
streamsearch@1.1.0:
|
|
||||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
|
||||||
engines: {node: '>=10.0.0'}
|
|
||||||
|
|
||||||
string-width@4.2.3:
|
string-width@4.2.3:
|
||||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
string_decoder@1.3.0:
|
|
||||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
|
||||||
|
|
||||||
strip-ansi@6.0.1:
|
strip-ansi@6.0.1:
|
||||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
@ -2605,17 +2521,10 @@ packages:
|
||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
type-is@1.6.18:
|
|
||||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
|
||||||
engines: {node: '>= 0.6'}
|
|
||||||
|
|
||||||
type-is@2.0.1:
|
type-is@2.0.1:
|
||||||
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
typedarray@0.0.6:
|
|
||||||
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
|
||||||
|
|
||||||
typescript-eslint@8.47.0:
|
typescript-eslint@8.47.0:
|
||||||
resolution: {integrity: sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==}
|
resolution: {integrity: sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
@ -2669,9 +2578,6 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
util-deprecate@1.0.2:
|
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
|
||||||
|
|
||||||
vary@1.1.2:
|
vary@1.1.2:
|
||||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
@ -2751,10 +2657,6 @@ packages:
|
||||||
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
|
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
xtend@4.0.2:
|
|
||||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
|
||||||
engines: {node: '>=0.4'}
|
|
||||||
|
|
||||||
y18n@5.0.8:
|
y18n@5.0.8:
|
||||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
@ -3684,28 +3586,10 @@ snapshots:
|
||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
'@taylordb/query-builder@0.11.4':
|
|
||||||
dependencies:
|
|
||||||
'@taylordb/shared': 0.5.4
|
|
||||||
eventemitter3: 5.0.1
|
|
||||||
fast-json-patch: 3.1.1
|
|
||||||
json-to-graphql-query: 2.3.0
|
|
||||||
lodash: 4.17.21
|
|
||||||
socket.io-client: 4.8.3
|
|
||||||
zod: 4.3.5
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bufferutil
|
|
||||||
- supports-color
|
|
||||||
- utf-8-validate
|
|
||||||
|
|
||||||
'@taylordb/shared@0.4.4':
|
'@taylordb/shared@0.4.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
|
|
||||||
'@taylordb/shared@0.5.4':
|
|
||||||
dependencies:
|
|
||||||
lodash: 4.17.21
|
|
||||||
|
|
||||||
'@trpc/client@11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3)':
|
'@trpc/client@11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@trpc/server': 11.8.1(typescript@5.9.3)
|
'@trpc/server': 11.8.1(typescript@5.9.3)
|
||||||
|
|
@ -3754,10 +3638,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.10.1
|
'@types/node': 24.10.1
|
||||||
|
|
||||||
'@types/cookie-parser@1.4.10(@types/express@5.0.6)':
|
|
||||||
dependencies:
|
|
||||||
'@types/express': 5.0.6
|
|
||||||
|
|
||||||
'@types/cors@2.8.19':
|
'@types/cors@2.8.19':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.10.1
|
'@types/node': 24.10.1
|
||||||
|
|
@ -3805,10 +3685,6 @@ snapshots:
|
||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
'@types/multer@2.0.0':
|
|
||||||
dependencies:
|
|
||||||
'@types/express': 5.0.6
|
|
||||||
|
|
||||||
'@types/node@24.10.1':
|
'@types/node@24.10.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.16.0
|
undici-types: 7.16.0
|
||||||
|
|
@ -3965,8 +3841,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-convert: 2.0.1
|
color-convert: 2.0.1
|
||||||
|
|
||||||
append-field@1.0.0: {}
|
|
||||||
|
|
||||||
argparse@2.0.1: {}
|
argparse@2.0.1: {}
|
||||||
|
|
||||||
aria-hidden@1.2.6:
|
aria-hidden@1.2.6:
|
||||||
|
|
@ -4012,12 +3886,6 @@ snapshots:
|
||||||
node-releases: 2.0.27
|
node-releases: 2.0.27
|
||||||
update-browserslist-db: 1.1.4(browserslist@4.28.0)
|
update-browserslist-db: 1.1.4(browserslist@4.28.0)
|
||||||
|
|
||||||
buffer-from@1.1.2: {}
|
|
||||||
|
|
||||||
busboy@1.6.0:
|
|
||||||
dependencies:
|
|
||||||
streamsearch: 1.1.0
|
|
||||||
|
|
||||||
bytes@3.1.2: {}
|
bytes@3.1.2: {}
|
||||||
|
|
||||||
call-bind-apply-helpers@1.0.2:
|
call-bind-apply-helpers@1.0.2:
|
||||||
|
|
@ -4059,13 +3927,6 @@ snapshots:
|
||||||
|
|
||||||
concat-map@0.0.1: {}
|
concat-map@0.0.1: {}
|
||||||
|
|
||||||
concat-stream@2.0.0:
|
|
||||||
dependencies:
|
|
||||||
buffer-from: 1.1.2
|
|
||||||
inherits: 2.0.4
|
|
||||||
readable-stream: 3.6.2
|
|
||||||
typedarray: 0.0.6
|
|
||||||
|
|
||||||
concurrently@9.2.1:
|
concurrently@9.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
|
|
@ -4081,13 +3942,6 @@ snapshots:
|
||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
cookie-parser@1.4.7:
|
|
||||||
dependencies:
|
|
||||||
cookie: 0.7.2
|
|
||||||
cookie-signature: 1.0.6
|
|
||||||
|
|
||||||
cookie-signature@1.0.6: {}
|
|
||||||
|
|
||||||
cookie-signature@1.2.2: {}
|
cookie-signature@1.2.2: {}
|
||||||
|
|
||||||
cookie@0.7.2: {}
|
cookie@0.7.2: {}
|
||||||
|
|
@ -4659,8 +4513,6 @@ snapshots:
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
media-typer@0.3.0: {}
|
|
||||||
|
|
||||||
media-typer@1.1.0: {}
|
media-typer@1.1.0: {}
|
||||||
|
|
||||||
merge-descriptors@2.0.0: {}
|
merge-descriptors@2.0.0: {}
|
||||||
|
|
@ -4672,14 +4524,8 @@ snapshots:
|
||||||
braces: 3.0.3
|
braces: 3.0.3
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
|
|
||||||
mime-db@1.52.0: {}
|
|
||||||
|
|
||||||
mime-db@1.54.0: {}
|
mime-db@1.54.0: {}
|
||||||
|
|
||||||
mime-types@2.1.35:
|
|
||||||
dependencies:
|
|
||||||
mime-db: 1.52.0
|
|
||||||
|
|
||||||
mime-types@3.0.2:
|
mime-types@3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-db: 1.54.0
|
mime-db: 1.54.0
|
||||||
|
|
@ -4692,24 +4538,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 2.0.2
|
brace-expansion: 2.0.2
|
||||||
|
|
||||||
minimist@1.2.8: {}
|
|
||||||
|
|
||||||
mkdirp@0.5.6:
|
|
||||||
dependencies:
|
|
||||||
minimist: 1.2.8
|
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
multer@2.0.2:
|
|
||||||
dependencies:
|
|
||||||
append-field: 1.0.0
|
|
||||||
busboy: 1.6.0
|
|
||||||
concat-stream: 2.0.0
|
|
||||||
mkdirp: 0.5.6
|
|
||||||
object-assign: 4.1.1
|
|
||||||
type-is: 1.6.18
|
|
||||||
xtend: 4.0.2
|
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
@ -4854,12 +4684,6 @@ snapshots:
|
||||||
|
|
||||||
react@19.2.0: {}
|
react@19.2.0: {}
|
||||||
|
|
||||||
readable-stream@3.6.2:
|
|
||||||
dependencies:
|
|
||||||
inherits: 2.0.4
|
|
||||||
string_decoder: 1.3.0
|
|
||||||
util-deprecate: 1.0.2
|
|
||||||
|
|
||||||
recharts@3.5.0(@types/react@19.2.6)(eslint@9.39.1(jiti@2.6.1))(react-dom@19.2.0(react@19.2.0))(react-is@19.2.0)(react@19.2.0)(redux@5.0.1):
|
recharts@3.5.0(@types/react@19.2.6)(eslint@9.39.1(jiti@2.6.1))(react-dom@19.2.0(react@19.2.0))(react-is@19.2.0)(react@19.2.0)(redux@5.0.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@reduxjs/toolkit': 2.11.0(react-redux@9.2.0(@types/react@19.2.6)(react@19.2.0)(redux@5.0.1))(react@19.2.0)
|
'@reduxjs/toolkit': 2.11.0(react-redux@9.2.0(@types/react@19.2.6)(react@19.2.0)(redux@5.0.1))(react@19.2.0)
|
||||||
|
|
@ -4944,8 +4768,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
safe-buffer@5.2.1: {}
|
|
||||||
|
|
||||||
safer-buffer@2.1.2: {}
|
safer-buffer@2.1.2: {}
|
||||||
|
|
||||||
scheduler@0.27.0: {}
|
scheduler@0.27.0: {}
|
||||||
|
|
@ -5039,18 +4861,12 @@ snapshots:
|
||||||
|
|
||||||
statuses@2.0.2: {}
|
statuses@2.0.2: {}
|
||||||
|
|
||||||
streamsearch@1.1.0: {}
|
|
||||||
|
|
||||||
string-width@4.2.3:
|
string-width@4.2.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
emoji-regex: 8.0.0
|
emoji-regex: 8.0.0
|
||||||
is-fullwidth-code-point: 3.0.0
|
is-fullwidth-code-point: 3.0.0
|
||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
string_decoder@1.3.0:
|
|
||||||
dependencies:
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
|
|
||||||
strip-ansi@6.0.1:
|
strip-ansi@6.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex: 5.0.1
|
ansi-regex: 5.0.1
|
||||||
|
|
@ -5105,19 +4921,12 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
|
|
||||||
type-is@1.6.18:
|
|
||||||
dependencies:
|
|
||||||
media-typer: 0.3.0
|
|
||||||
mime-types: 2.1.35
|
|
||||||
|
|
||||||
type-is@2.0.1:
|
type-is@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
media-typer: 1.1.0
|
media-typer: 1.1.0
|
||||||
mime-types: 3.0.2
|
mime-types: 3.0.2
|
||||||
|
|
||||||
typedarray@0.0.6: {}
|
|
||||||
|
|
||||||
typescript-eslint@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
|
typescript-eslint@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/eslint-plugin': 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/eslint-plugin': 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
|
||||||
|
|
@ -5164,8 +4973,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
|
||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
victory-vendor@37.3.6:
|
victory-vendor@37.3.6:
|
||||||
|
|
@ -5218,8 +5025,6 @@ snapshots:
|
||||||
|
|
||||||
xmlhttprequest-ssl@2.1.2: {}
|
xmlhttprequest-ssl@2.1.2: {}
|
||||||
|
|
||||||
xtend@4.0.2: {}
|
|
||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ services:
|
||||||
env:
|
env:
|
||||||
vars:
|
vars:
|
||||||
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
|
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
|
||||||
|
TAYLORDB_API_TOKEN: secrets.TAYLORDB_API_TOKEN
|
||||||
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
|
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
|
||||||
FRONTEND_URL: routing.client.url
|
FRONTEND_URL: routing.client.url
|
||||||
|
|
||||||
|
|
@ -30,6 +31,7 @@ services:
|
||||||
env:
|
env:
|
||||||
vars:
|
vars:
|
||||||
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
|
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
|
||||||
|
TAYLORDB_API_TOKEN: secrets.TAYLORDB_API_TOKEN
|
||||||
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
|
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
|
||||||
FRONTEND_URL: routing.client.url
|
FRONTEND_URL: routing.client.url
|
||||||
|
|
||||||
|
|
@ -39,6 +41,7 @@ services:
|
||||||
env:
|
env:
|
||||||
vars:
|
vars:
|
||||||
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
|
TAYLORDB_BASE_URL: vars.TAYLORDB_INTERNAL_BASE_URL
|
||||||
|
TAYLORDB_API_TOKEN: secrets.TAYLORDB_API_TOKEN
|
||||||
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
|
TAYLORDB_SERVER_ID: vars.TAYLORDB_SERVER_ID
|
||||||
FRONTEND_URL: routing.client.url
|
FRONTEND_URL: routing.client.url
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user