oneie
Version:
Build apps, websites, and AI agents in English. Zero-interaction setup for AI agents (Claude Code, Cursor, Windsurf). Download to your computer, run in the cloud, deploy to the edge. Open source and free forever.
287 lines (239 loc) • 7.54 kB
Markdown
---
title: Query Template
dimension: knowledge
category: patterns
tags: ai, auth, backend, things
related_dimensions: events, groups, people, things
scope: global
created: 2025-11-03
updated: 2025-11-03
version: 1.0.0
ai_context: |
This document is part of the knowledge dimension in the patterns category.
Location: one/knowledge/patterns/backend/query-template.md
Purpose: Documents pattern: convex query template
Related dimensions: events, groups, people, things
For AI agents: Read this to understand query template.
---
# Pattern: Convex Query Template
**Category:** Backend
**Context:** When creating Convex queries for reading data (list, get by ID, search)
**Problem:** Need consistent query structure that respects multi-tenancy, uses indexes efficiently, and handles auth
## Solution
Keep queries simple and fast. Use indexes, filter by organization, return only necessary fields. No business logic in queries.
## Template
```typescript
// backend/convex/queries/{entities}.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
export const list = query({
args: {
type: v.optional(v.string()),
status: v.optional(v.union(v.literal("draft"), v.literal("active"), v.literal("archived"))),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
// 1. Check authentication
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
return [];
}
// 2. Build query with indexes
let query = ctx.db
.query("things")
.withIndex("by_group_type", (q) =>
q.eq("groupId", identity.groupId).eq("type", args.type)
);
// 3. Apply filters
if (args.type) {
query = query.filter((q) => q.eq(q.field("type"), args.type));
}
if (args.status) {
query = query.filter((q) => q.eq(q.field("status"), args.status));
}
// 4. Execute with limit
const entities = await query
.order("desc")
.take(args.limit ?? 100);
return entities;
},
});
export const getById = query({
args: {
id: v.id("{entities}"),
},
handler: async (ctx, args) => {
// 1. Check authentication
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
return null;
}
// 2. Get entity
const entity = await ctx.db.get(args.id);
// 3. Check ownership (multi-tenancy)
if (!entity || entity.groupId !== identity.groupId) {
return null;
}
return entity;
},
});
export const getByType = query({
args: {
type: v.string(),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
// 1. Check authentication
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
return [];
}
// 2. Query with compound index
const entities = await ctx.db
.query("things")
.withIndex("by_group_type", (q) =>
q.eq("groupId", identity.groupId).eq("type", args.type)
)
.order("desc")
.take(args.limit ?? 100);
return entities;
},
});
export const search = query({
args: {
query: v.string(),
type: v.optional(v.string()),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
// 1. Check authentication
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
return [];
}
// 2. Search by group first (use index)
let results = ctx.db
.query("things")
.withIndex("by_group_type", (q) =>
q.eq("groupId", identity.groupId).eq("type", "all")
);
// 3. Filter by type if provided
if (args.type) {
results = results.filter((q) => q.eq(q.field("type"), args.type));
}
// 4. Search in name and properties (text search)
const searchTerm = args.query.toLowerCase();
const entities = await results.collect();
const filtered = entities.filter((entity) => {
const nameMatch = entity.name?.toLowerCase().includes(searchTerm);
const propsMatch = JSON.stringify(entity.properties)
.toLowerCase()
.includes(searchTerm);
return nameMatch || propsMatch;
});
// 5. Return with limit
return filtered.slice(0, args.limit ?? 50);
},
});
export const count = query({
args: {
type: v.optional(v.string()),
status: v.optional(v.union(v.literal("draft"), v.literal("active"), v.literal("archived"))),
},
handler: async (ctx, args) => {
// 1. Check authentication
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
return 0;
}
// 2. Build query
let query = ctx.db
.query("things")
.withIndex("by_group_type", (q) =>
q.eq("groupId", identity.groupId)
);
// 3. Apply filters
if (args.type) {
query = query.filter((q) => q.eq(q.field("type"), args.type));
}
if (args.status) {
query = query.filter((q) => q.eq(q.field("status"), args.status));
}
// 4. Count
const entities = await query.collect();
return entities.length;
},
});
```
## Variables
- `{entities}` - Table name (always "entities" in our ontology)
## Usage
1. Copy template to `backend/convex/queries/{entities}.ts`
2. Add entity-specific query methods as needed
3. Ensure indexes exist in schema (`by_group_type` for groupId + type)
4. Export from `backend/convex/_generated/api.ts`
## Example (Course Queries)
```typescript
// backend/convex/queries/entities.ts
export const listCourses = query({
args: {
status: v.optional(v.string()),
},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) return [];
return await ctx.db
.query("things")
.withIndex("by_group_type", (q) =>
q.eq("groupId", identity.groupId).eq("type", "course")
)
.filter((q) =>
args.status ? q.eq(q.field("status"), args.status) : true
)
.collect();
},
});
export const getCourseWithLessons = query({
args: {
courseId: v.id("entities"),
},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) return null;
// Get course
const course = await ctx.db.get(args.courseId);
if (!course || course.groupId !== identity.groupId) {
return null;
}
// Get lessons (via connections)
const lessonConnections = await ctx.db
.query("connections")
.withIndex("from_type", (q) =>
q.eq("fromThingId", args.courseId).eq("relationshipType", "part_of")
)
.collect();
const lessons = await Promise.all(
lessonConnections.map((conn) => ctx.db.get(conn.toThingId))
);
return {
course,
lessons: lessons.filter(Boolean),
};
},
});
```
## Common Mistakes
- **Mistake:** Not checking authentication
- **Fix:** Always verify `ctx.auth.getUserIdentity()` first
- **Mistake:** Not filtering by group (multi-tenancy leak)
- **Fix:** Always use `by_group_type` index first
- **Mistake:** Not using indexes (slow queries)
- **Fix:** Use `withIndex()` for frequently queried fields like groupId, type
- **Mistake:** Returning too many results
- **Fix:** Always apply `.take(limit)` or slice results
- **Mistake:** Doing heavy computation in queries
- **Fix:** Keep queries simple, pre-compute in mutations if needed
## Related Patterns
- **service-template.md** - Business logic that queries might need
- **mutation-template.md** - Write operations
- **Schema indexes** - Define indexes in `schema.ts` for efficient queries