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