giant-dodos-lie/docs/TAYLORDB_QUERY_REFERENCE.md
2026-01-09 02:02:12 +05:00

15 KiB

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
  2. Basic Queries
  3. Filtering & Conditions
  4. Inserting Data
  5. Updating Data
  6. Deleting Data
  7. Advanced Patterns
  8. Field Type Handling
  9. Common Pitfalls

Setup & Configuration

Initialize Query Builder

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

export async function getAllUsers() {
  return await queryBuilder
    .selectFrom("users")
    .select(["id", "name", "email", "createdAt"])
    .execute();
}

Get All Records (All Fields)

export async function getAllUsers() {
  return await queryBuilder.selectFrom("users").execute();
}

Get Single Record by ID

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

// 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

// 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)

export async function getActiveAdults() {
  return await queryBuilder
    .selectFrom("users")
    .where("status", "=", "active")
    .where("age", ">=", 18)
    .execute();
}

Date Filtering

// 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

// 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

// 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)

export async function searchUsersByName(query: string) {
  return await queryBuilder
    .selectFrom("users")
    .where("name", "contains", query)
    .execute();
}

Inserting Data

Insert Single Record

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

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

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

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

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

export async function updateUserName(id: number, name: string) {
  return await queryBuilder
    .update("users")
    .set({ name })
    .where("id", "=", id)
    .execute();
}

Update Multiple Fields

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

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

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

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

export async function deleteUser(id: number) {
  return await queryBuilder.deleteFrom("users").where("id", "=", id).execute();
}

Delete Multiple Records by IDs

export async function deleteUsers(ids: number[]) {
  return await queryBuilder
    .deleteFrom("users")
    .where("id", "hasAnyOf", ids)
    .execute();
}

Delete with Condition

export async function deleteInactiveUsers() {
  return await queryBuilder
    .deleteFrom("users")
    .where("status", "=", "inactive")
    .execute();
}

Delete Old Records

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:

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

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

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

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

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

// 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

// ❌ WRONG
.values({ priority: "high" })

// ✅ CORRECT
.values({ priority: ["high"] })

Pitfall 2: Not Using exactDay for Dates

// ❌ WRONG
.where("date", "=", "2024-01-15")

// ✅ CORRECT
.where("date", "=", ["exactDay", "2024-01-15"])

Pitfall 3: Ignoring Nullable Fields

// ❌ 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

// ❌ 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

// ❌ 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.