Use when building advanced queries with complex filtering conditions in Bknd. Covers all filter operators ($eq, $ne, $gt, $lt, $like, $ilike, $in, $nin, $isnull, $between), logical operators ($or, $and), nested conditions, combining filters, and dynamic query building.
npx claudepluginhub cameronapak/bknd-expert --plugin bknd-research-skillsThis skill uses the workspace's default tool permissions.
Build complex queries with multiple conditions, logical operators, and dynamic filters in Bknd.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Build complex queries with multiple conditions, logical operators, and dynamic filters in Bknd.
readMany (see bknd-crud-read)UI steps: Admin Panel > Data > Select Entity > Use filter controls
Bknd supports these filter operators:
| Category | Operators |
|---|---|
| Equality | $eq, $ne |
| Comparison | $gt, $gte, $lt, $lte |
| Range | $between |
| Pattern | $like, $ilike |
| Array | $in, $nin (alias: $notin) |
| Null | $isnull |
| Logical | $or, $and (implicit) |
import { Api } from "bknd";
const api = new Api({ host: "http://localhost:7654" });
// Equality (implicit $eq)
const { data } = await api.data.readMany("products", {
where: { status: "active" }, // Same as { status: { $eq: "active" } }
});
// Not equal
const { data } = await api.data.readMany("products", {
where: { status: { $ne: "deleted" } },
});
// Numeric comparisons
const { data } = await api.data.readMany("products", {
where: {
price: { $gte: 10 }, // price >= 10
stock: { $gt: 0 }, // stock > 0
},
});
// Date comparisons
const { data } = await api.data.readMany("orders", {
where: {
created_at: { $gte: "2024-01-01" },
created_at: { $lt: "2024-02-01" },
},
});
// Price between 10 and 100 (inclusive)
const { data } = await api.data.readMany("products", {
where: {
price: { $between: [10, 100] },
},
});
// Date range
const { data } = await api.data.readMany("orders", {
where: {
created_at: { $between: ["2024-01-01", "2024-12-31"] },
},
});
// LIKE (case-sensitive) - use % as wildcard
const { data } = await api.data.readMany("posts", {
where: { title: { $like: "%React%" } },
});
// ILIKE (case-insensitive) - preferred for search
const { data } = await api.data.readMany("posts", {
where: { title: { $ilike: "%react%" } },
});
// Starts with
const { data } = await api.data.readMany("users", {
where: { name: { $like: "John%" } },
});
// Ends with
const { data } = await api.data.readMany("users", {
where: { email: { $like: "%@gmail.com" } },
});
// Wildcard alternative: use * instead of %
const { data } = await api.data.readMany("posts", {
where: { title: { $like: "*React*" } }, // Converted to %React%
});
// In array - match any value
const { data } = await api.data.readMany("posts", {
where: { status: { $in: ["published", "featured"] } },
});
// Not in array - exclude values
const { data } = await api.data.readMany("posts", {
where: { status: { $nin: ["deleted", "archived"] } },
});
// Get specific records by IDs
const { data } = await api.data.readMany("products", {
where: { id: { $in: [1, 5, 10, 15] } },
});
// Is NULL
const { data } = await api.data.readMany("posts", {
where: { deleted_at: { $isnull: true } },
});
// Is NOT NULL
const { data } = await api.data.readMany("posts", {
where: { published_at: { $isnull: false } },
});
// Combine: active records (not deleted, has been published)
const { data } = await api.data.readMany("posts", {
where: {
deleted_at: { $isnull: true },
published_at: { $isnull: false },
},
});
Multiple fields at same level = AND:
// status = "published" AND category = "news" AND views > 100
const { data } = await api.data.readMany("posts", {
where: {
status: { $eq: "published" },
category: { $eq: "news" },
views: { $gt: 100 },
},
});
// status = "published" OR featured = true
const { data } = await api.data.readMany("posts", {
where: {
$or: [
{ status: { $eq: "published" } },
{ featured: { $eq: true } },
],
},
});
// Multiple OR conditions
const { data } = await api.data.readMany("users", {
where: {
$or: [
{ role: { $eq: "admin" } },
{ role: { $eq: "moderator" } },
{ is_verified: { $eq: true } },
],
},
});
// category = "news" AND (status = "published" OR author_id = currentUser)
const { data } = await api.data.readMany("posts", {
where: {
category: { $eq: "news" },
$or: [
{ status: { $eq: "published" } },
{ author_id: { $eq: currentUserId } },
],
},
});
// Complex: (price < 50 OR on_sale = true) AND in_stock = true AND category IN ["electronics", "books"]
const { data } = await api.data.readMany("products", {
where: {
in_stock: { $eq: true },
category: { $in: ["electronics", "books"] },
$or: [
{ price: { $lt: 50 } },
{ on_sale: { $eq: true } },
],
},
});
Use join to filter by fields in related entities:
// Posts where author.role = "admin"
const { data } = await api.data.readMany("posts", {
join: ["author"],
where: {
"author.role": { $eq: "admin" },
},
});
// Orders where customer.country = "US" AND product.category = "electronics"
const { data } = await api.data.readMany("orders", {
join: ["customer", "product"],
where: {
"customer.country": { $eq: "US" },
"product.category": { $eq: "electronics" },
},
});
// Combine with regular filters
const { data } = await api.data.readMany("posts", {
join: ["author"],
where: {
status: { $eq: "published" },
"author.is_verified": { $eq: true },
},
});
type WhereClause = Record<string, any>;
function buildProductQuery(filters: {
search?: string;
minPrice?: number;
maxPrice?: number;
categories?: string[];
inStock?: boolean;
}): WhereClause {
const where: WhereClause = {};
if (filters.search) {
where.name = { $ilike: `%${filters.search}%` };
}
if (filters.minPrice !== undefined) {
where.price = { ...where.price, $gte: filters.minPrice };
}
if (filters.maxPrice !== undefined) {
where.price = { ...where.price, $lte: filters.maxPrice };
}
if (filters.categories?.length) {
where.category = { $in: filters.categories };
}
if (filters.inStock !== undefined) {
where.stock = filters.inStock ? { $gt: 0 } : { $eq: 0 };
}
return where;
}
// Usage
const filters = { search: "laptop", minPrice: 500, categories: ["electronics"] };
const { data } = await api.data.readMany("products", {
where: buildProductQuery(filters),
sort: { price: "asc" },
limit: 20,
});
function buildOrConditions(conditions: WhereClause[]): WhereClause {
const validConditions = conditions.filter(c => Object.keys(c).length > 0);
if (validConditions.length === 0) return {};
if (validConditions.length === 1) return validConditions[0];
return { $or: validConditions };
}
// Search across multiple fields
const searchTerm = "john";
const { data } = await api.data.readMany("users", {
where: buildOrConditions([
{ name: { $ilike: `%${searchTerm}%` } },
{ email: { $ilike: `%${searchTerm}%` } },
{ username: { $ilike: `%${searchTerm}%` } },
]),
});
type Facets = {
category?: string;
brand?: string;
priceRange?: "budget" | "mid" | "premium";
rating?: number;
};
const PRICE_RANGES = {
budget: { $lt: 50 },
mid: { $between: [50, 200] },
premium: { $gt: 200 },
};
async function facetedSearch(query: string, facets: Facets) {
const where: WhereClause = {};
// Text search
if (query) {
where.name = { $ilike: `%${query}%` };
}
// Facet filters
if (facets.category) {
where.category = { $eq: facets.category };
}
if (facets.brand) {
where.brand = { $eq: facets.brand };
}
if (facets.priceRange) {
where.price = PRICE_RANGES[facets.priceRange];
}
if (facets.rating) {
where.rating = { $gte: facets.rating };
}
return api.data.readMany("products", { where, limit: 50 });
}
import { useState, useCallback } from "react";
import { useApp } from "bknd/react";
import useSWR from "swr";
import { useDebouncedValue } from "@mantine/hooks";
type Filters = {
search: string;
status: string;
minDate: string;
};
function FilteredList() {
const { api } = useApp();
const [filters, setFilters] = useState<Filters>({
search: "",
status: "",
minDate: "",
});
const [debouncedFilters] = useDebouncedValue(filters, 300);
const buildWhere = useCallback((f: Filters) => {
const where: Record<string, any> = {};
if (f.search) {
where.title = { $ilike: `%${f.search}%` };
}
if (f.status) {
where.status = { $eq: f.status };
}
if (f.minDate) {
where.created_at = { $gte: f.minDate };
}
return where;
}, []);
const { data: posts, isLoading } = useSWR(
["posts", debouncedFilters],
() => api.data.readMany("posts", {
where: buildWhere(debouncedFilters),
sort: { created_at: "desc" },
limit: 20,
}).then(r => r.data)
);
return (
<div>
<input
placeholder="Search..."
value={filters.search}
onChange={e => setFilters(f => ({ ...f, search: e.target.value }))}
/>
<select
value={filters.status}
onChange={e => setFilters(f => ({ ...f, status: e.target.value }))}
>
<option value="">All statuses</option>
<option value="draft">Draft</option>
<option value="published">Published</option>
</select>
<input
type="date"
value={filters.minDate}
onChange={e => setFilters(f => ({ ...f, minDate: e.target.value }))}
/>
{isLoading ? <p>Loading...</p> : (
<ul>
{posts?.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
)}
</div>
);
}
# Simple filter
curl "http://localhost:7654/api/data/posts?where=%7B%22status%22%3A%22published%22%7D"
# URL-decoded: where={"status":"published"}
For complex queries, use POST to /api/data/:entity/query:
curl -X POST http://localhost:7654/api/data/posts/query \
-H "Content-Type: application/json" \
-d '{
"where": {
"category": {"$eq": "news"},
"$or": [
{"status": {"$eq": "published"}},
{"featured": {"$eq": true}}
]
},
"sort": {"created_at": "desc"},
"limit": 20
}'
import { Api } from "bknd";
const api = new Api({ host: "http://localhost:7654" });
// 1. Simple equality filter
const published = await api.data.readMany("posts", {
where: { status: "published" },
});
// 2. Numeric range
const midPriced = await api.data.readMany("products", {
where: { price: { $between: [50, 200] } },
});
// 3. Text search (case-insensitive)
const searchResults = await api.data.readMany("products", {
where: { name: { $ilike: "%laptop%" } },
});
// 4. Multiple values
const specificCategories = await api.data.readMany("products", {
where: { category: { $in: ["electronics", "computers"] } },
});
// 5. Exclude soft-deleted
const activeRecords = await api.data.readMany("posts", {
where: { deleted_at: { $isnull: true } },
});
// 6. Complex AND + OR
const complexQuery = await api.data.readMany("orders", {
where: {
created_at: { $gte: "2024-01-01" },
status: { $nin: ["cancelled", "refunded"] },
$or: [
{ total: { $gt: 100 } },
{ is_priority: { $eq: true } },
],
},
sort: { created_at: "desc" },
limit: 50,
});
// 7. Filter by related entity
const adminPosts = await api.data.readMany("posts", {
join: ["author"],
where: {
"author.role": { $eq: "admin" },
status: { $eq: "published" },
},
});
Problem: Overwriting previous condition.
// Wrong - second assignment overwrites first
where: {
price: { $gte: 10 },
price: { $lte: 100 }, // Overwrites!
}
// Correct - use $between or spread
where: {
price: { $between: [10, 100] },
}
// Or
where: {
price: { $gte: 10, $lte: 100 },
}
Problem: $or must be at top level of where clause.
// Wrong - nested $or
where: {
status: {
$or: [{ $eq: "a" }, { $eq: "b" }], // Invalid!
},
}
// Correct - use $in for same field
where: {
status: { $in: ["a", "b"] },
}
// Correct - $or at top level for different fields
where: {
$or: [
{ status: { $eq: "a" } },
{ featured: { $eq: true } },
],
}
Problem: Filtering by related field without join.
// Wrong - won't work
where: { "author.role": { $eq: "admin" } }
// Correct - add join
{
join: ["author"],
where: { "author.role": { $eq: "admin" } },
}
Problem: $like is case-sensitive.
// May miss results
where: { title: { $like: "%React%" } }
// Use $ilike for case-insensitive
where: { title: { $ilike: "%react%" } }
Problem: Empty where returns all records.
// Returns everything (no filter)
where: {}
// Always validate filters exist
const where = buildFilters(userInput);
if (Object.keys(where).length === 0) {
// Handle: show default view or require at least one filter
}
Test filters in admin panel first:
Or log the where clause:
const where = buildFilters(input);
console.log("Query:", JSON.stringify(where, null, 2));
const { data } = await api.data.readMany("posts", { where });
DO:
$ilike for user-facing search (case-insensitive)$in instead of multiple $or for same field$between for numeric/date rangesjoin when filtering by related fieldsDON'T:
$like for user search (case-sensitive issues)$or inside field conditionsjoin for related field filters