Use when querying and retrieving data from Bknd entities via SDK or REST API. Covers readOne, readMany, readOneBy, filtering (where clause), sorting, field selection, loading relations (with/join), and response handling.
npx claudepluginhub cameronapak/bknd-expert --plugin bknd-research-skillsThis skill uses the workspace's default tool permissions.
Query and retrieve data from your Bknd database using the SDK or REST API.
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`.
Query and retrieve data from your Bknd database using the SDK or REST API.
bknd-create-entity, bknd-crud-create)UI steps: Admin Panel > Data > Select Entity > Browse/search records
import { Api } from "bknd";
const api = new Api({
host: "http://localhost:7654",
});
// If auth required:
api.updateToken("your-jwt-token");
Use readOne(entity, id, query?):
const { ok, data, error } = await api.data.readOne("posts", 1);
if (ok) {
console.log("Post:", data.title);
} else {
console.error("Not found or error:", error.message);
}
Use readOneBy(entity, query) to find by field value:
const { data } = await api.data.readOneBy("users", {
where: { email: { $eq: "user@example.com" } },
});
if (data) {
console.log("Found user:", data.id);
}
Use readMany(entity, query?):
const { ok, data, meta } = await api.data.readMany("posts", {
where: { status: { $eq: "published" } },
sort: { created_at: "desc" },
limit: 20,
offset: 0,
});
console.log(`Found ${meta.total} total, showing ${data.length}`);
The response object structure:
type ReadResponse = {
ok: boolean;
data?: T | T[]; // Single object or array
meta?: { // For readMany
total: number; // Total matching records
limit: number; // Current page size
offset: number; // Current offset
};
error?: {
message: string;
code: string;
};
};
// Equality (implicit or explicit)
{ where: { status: "published" } }
{ where: { status: { $eq: "published" } } }
// Not equal
{ where: { status: { $ne: "deleted" } } }
// Numeric comparisons
{ where: { age: { $gt: 18 } } } // Greater than
{ where: { age: { $gte: 18 } } } // Greater or equal
{ where: { price: { $lt: 100 } } } // Less than
{ where: { price: { $lte: 100 } } } // Less or equal
// LIKE patterns (% = wildcard)
{ where: { title: { $like: "%hello%" } } } // Contains (case-sensitive)
{ where: { title: { $ilike: "%hello%" } } } // Contains (case-insensitive)
// Convenience methods
{ where: { name: { $startswith: "John" } } }
{ where: { email: { $endswith: "@gmail.com" } } }
{ where: { bio: { $contains: "developer" } } }
// In array
{ where: { id: { $in: [1, 2, 3] } } }
// Not in array
{ where: { type: { $nin: ["archived", "deleted"] } } }
// Is NULL
{ where: { deleted_at: { $isnull: true } } }
// Is NOT NULL
{ where: { published_at: { $isnull: false } } }
// AND (implicit - multiple fields)
{
where: {
status: { $eq: "published" },
category: { $eq: "news" },
}
}
// OR
{
where: {
$or: [
{ status: { $eq: "published" } },
{ featured: { $eq: true } },
]
}
}
// Combined AND/OR
{
where: {
category: { $eq: "news" },
$or: [
{ status: { $eq: "published" } },
{ author_id: { $eq: currentUserId } },
]
}
}
// Object syntax (preferred)
{ sort: { created_at: "desc" } }
{ sort: { name: "asc", created_at: "desc" } } // Multi-sort
// String syntax (- prefix = descending)
{ sort: "-created_at" }
{ sort: "name,-created_at" }
Reduce payload by selecting specific fields:
const { data } = await api.data.readMany("users", {
select: ["id", "email", "name"],
});
// data[0] only has id, email, name
// Simple - load relations
{ with: "author" }
{ with: ["author", "comments"] }
{ with: "author,comments" }
// Nested with subquery options
{
with: {
author: {
select: ["id", "name", "avatar"],
},
comments: {
where: { approved: { $eq: true } },
sort: { created_at: "desc" },
limit: 10,
with: ["user"], // Nested loading
},
}
}
Result structure:
const { data } = await api.data.readOne("posts", 1, {
with: ["author", "comments"],
});
console.log(data.author.name); // Nested object
console.log(data.comments[0].text); // Nested array
Use join to filter by related fields:
const { data } = await api.data.readMany("posts", {
join: ["author"],
where: {
"author.role": { $eq: "admin" }, // Filter by joined field
},
sort: "-author.created_at", // Sort by joined field
});
| Feature | with | join |
|---|---|---|
| Query method | Separate queries | SQL JOIN |
| Return structure | Nested objects | Flat (unless also with) |
| Use case | Load related data | Filter by related fields |
| Performance | Multiple queries | Single query |
// Page 1 (records 0-19)
{ limit: 20, offset: 0 }
// Page 2 (records 20-39)
{ limit: 20, offset: 20 }
// Generic page formula
{ limit: pageSize, offset: (page - 1) * pageSize }
Default limit is 10 if not specified.
async function paginate<T>(
entity: string,
page: number,
pageSize: number,
query: object = {}
) {
const { data, meta } = await api.data.readMany(entity, {
...query,
limit: pageSize,
offset: (page - 1) * pageSize,
});
return {
data,
page,
pageSize,
total: meta.total,
totalPages: Math.ceil(meta.total / pageSize),
hasNext: page * pageSize < meta.total,
hasPrev: page > 1,
};
}
# Basic
curl http://localhost:7654/api/data/posts
# With query params
curl "http://localhost:7654/api/data/posts?limit=20&offset=0&sort=-created_at"
# With where clause
curl "http://localhost:7654/api/data/posts?where=%7B%22status%22%3A%22published%22%7D"
curl http://localhost:7654/api/data/posts/1
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": {"status": {"$eq": "published"}},
"sort": {"created_at": "desc"},
"limit": 20,
"with": ["author"]
}'
# Get user's posts
curl http://localhost:7654/api/data/users/1/posts
import { useApp } from "bknd/react";
import { useEffect, useState } from "react";
function PostsList() {
const { api } = useApp();
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
api.data.readMany("posts", {
where: { status: { $eq: "published" } },
sort: { created_at: "desc" },
limit: 20,
}).then(({ data }) => {
setPosts(data);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
import { useApp } from "bknd/react";
import useSWR from "swr";
function PostsList() {
const { api } = useApp();
const { data: posts, isLoading, error } = useSWR(
"posts-published",
() => api.data.readMany("posts", {
where: { status: { $eq: "published" } },
sort: { created_at: "desc" },
}).then((r) => r.data)
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading posts</div>;
return (
<ul>
{posts?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
function PostDetail({ postId }: { postId: number }) {
const { api } = useApp();
const { data: post, isLoading } = useSWR(
`post-${postId}`,
() => api.data.readOne("posts", postId, {
with: ["author", "comments"],
}).then((r) => r.data)
);
if (isLoading) return <div>Loading...</div>;
if (!post) return <div>Post not found</div>;
return (
<article>
<h1>{post.title}</h1>
<p>By {post.author?.name}</p>
<div>{post.content}</div>
<h2>Comments ({post.comments?.length})</h2>
</article>
);
}
import { useState, useMemo } from "react";
import { useApp } from "bknd/react";
import useSWR from "swr";
import { useDebouncedValue } from "@mantine/hooks"; // or custom hook
function SearchPosts() {
const { api } = useApp();
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 300);
const { data: results, isLoading } = useSWR(
debouncedSearch ? `search-${debouncedSearch}` : null,
() => api.data.readMany("posts", {
where: { title: { $ilike: `%${debouncedSearch}%` } },
limit: 10,
}).then((r) => r.data)
);
return (
<div>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search posts..."
/>
{isLoading && <p>Searching...</p>}
<ul>
{results?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
import { Api } from "bknd";
const api = new Api({ host: "http://localhost:7654" });
// Get single post with relations
const { data: post } = await api.data.readOne("posts", 1, {
with: {
author: { select: ["id", "name"] },
tags: true,
},
});
console.log(post.title, "by", post.author.name);
// Find user by email
const { data: user } = await api.data.readOneBy("users", {
where: { email: { $eq: "admin@example.com" } },
});
// List published posts with pagination
const { data: posts, meta } = await api.data.readMany("posts", {
where: {
status: { $eq: "published" },
deleted_at: { $isnull: true },
},
sort: { created_at: "desc" },
limit: 10,
offset: 0,
with: ["author"],
});
console.log(`Page 1 of ${Math.ceil(meta.total / 10)}`);
// Complex query: posts by admin authors in category
const { data: adminPosts } = await api.data.readMany("posts", {
join: ["author"],
where: {
"author.role": { $eq: "admin" },
category: { $eq: "announcements" },
$or: [
{ status: { $eq: "published" } },
{ featured: { $eq: true } },
],
},
select: ["id", "title", "created_at"],
sort: "-created_at",
});
const { data } = await api.data.count("posts", {
status: { $eq: "published" },
});
console.log(`${data.count} published posts`);
const { data } = await api.data.exists("users", {
email: { $eq: "test@example.com" },
});
if (data.exists) {
console.log("Email already registered");
}
// Always exclude soft-deleted
const { data } = await api.data.readMany("posts", {
where: { deleted_at: { $isnull: true } },
});
// Using readManyByReference
const { data: userPosts } = await api.data.readManyByReference(
"users", userId, "posts",
{ sort: { created_at: "desc" }, limit: 10 }
);
Problem: Assuming data exists.
Fix: Always check ok or handle undefined:
// Wrong
const { data } = await api.data.readOne("posts", 999);
console.log(data.title); // Error if not found!
// Correct
const { ok, data } = await api.data.readOne("posts", 999);
if (!ok || !data) {
console.log("Post not found");
return;
}
console.log(data.title);
Problem: Using operators incorrectly.
Fix: Wrap values in operator object:
// Wrong
{ where: { age: ">18" } }
// Correct
{ where: { age: { $gt: 18 } } }
Problem: Filtering by related field without join.
Fix: Add join clause:
// Wrong - won't work
{ where: { "author.role": { $eq: "admin" } } }
// Correct - add join
{
join: ["author"],
where: { "author.role": { $eq: "admin" } }
}
Problem: Loading relations in a loop.
Fix: Use with to load relations in batch:
// Wrong - N+1 queries
const { data: posts } = await api.data.readMany("posts");
for (const post of posts) {
const { data: author } = await api.data.readOne("users", post.author_id);
}
// Correct - single batch query
const { data: posts } = await api.data.readMany("posts", {
with: ["author"],
});
posts.forEach(p => console.log(p.author.name));
Problem: $like is case-sensitive.
Fix: Use $ilike for case-insensitive:
// Case-sensitive (may miss results)
{ where: { title: { $like: "%React%" } } }
// Case-insensitive
{ where: { title: { $ilike: "%react%" } } }
Test queries in admin panel first:
Or log response in code:
const response = await api.data.readMany("posts", query);
console.log("Response:", JSON.stringify(response, null, 2));
DO:
ok before accessing datawith for loading relationsjoin when filtering by related fields$ilike for case-insensitive text searchselect to reduce payload sizeDON'T:
join when filtering by relation fields$like when case-insensitive neededwith