Search
Build typed searches for posts, profiles, and packs without writing raw request objects.
Packbase search can find posts, profiles, and packs. The SDK includes a query builder named from(). Use it to select the model, filters, sort order, and result limit.
Build a query by chaining methods. Then, .build() changes the chain into the object that pb.search() sends to the API.
Quick example
const = await .({
: ('packs').('last_activity_at').(10).(),
: ('profiles').('created_at').(5).(),
})
if (!(.)) {
.(.topPacks)}In this example, the application specifies the names topPacks and recentUsers. They are not SDK keywords. The response uses the same names. Thus, one HTTP request can contain multiple independent searches.
Queryable models
| Model | Fields available |
|---|---|
'posts' | id, created_at, tenant_id, content_type, body, user_id, parent, tags, assets |
'profiles' | id, created_at, username, bio, slug, display_name, images_avatar, images_header, type, space_type, is_r18 |
'packs' | id, created_at, display_name, images_avatar, description, owner_id, images_header, last_activity_at |
The model name specifies the fields that TypeScript permits in the chain. If a 'packs' query uses a profile-only field, your editor shows an error before the request.
Search results use these flat database column names. The result types are SearchPost, SearchProfile, and SearchPack. They are not the normalized Howl, Profile, and Pack resource types. For example, a searched pack has images_avatar and last_activity_at. It does not have a nested images object.
Building a query: from(model)
Start each query with from() and the kind of data you want to search:
const = ('packs').()This example does not apply a filter. Chain the applicable methods. Then, call .build() when the query is ready.
Filtering: .where()
.where() keeps only records that match a condition. The builder combines multiple calls with AND. Thus, each condition must match:
// Exact match (shorthand for { equals: 'rek' })
const = ('profiles').({ : 'rek' })
// Operator map
const = ('profiles').({ : { : 'r' } })
// Multiple conditions (chained .where() calls are merged)
const = ('packs')
.({ : 'some-uuid' })
.({ : { : 'art' } })Available filter operators
A value such as { username: 'rek' } means “equal to rek.” Use an operator object for a different comparison:
| Operator | Use case |
|---|---|
equals | Exact match (default for scalars) |
not | Negate an equality check |
in | Match any value in an array |
notIn | Exclude values in an array |
lt / lte | Less-than / less-than-or-equal |
gt / gte | Greater-than / greater-than-or-equal |
contains | Substring / array contains |
startsWith | Prefix match |
endsWith | Suffix match |
mode | Case-insensitive ('insensitive') |
has | JSON array contains value |
hasSome | JSON array has any of these values |
hasEvery | JSON array has all of these values |
isEmpty | JSON array is empty |
AND / OR / NOT clauses
Use these methods when a filter requires explicit logic. AND requires each condition. OR requires one or more conditions. NOT removes matching records:
// AND: both conditions must match
const = ('posts').([
{ : { : '2024-01-01' } },
{ : { : '2024-12-31' } },
])
// OR: either condition matches
const = ('profiles').([
{ : 'alice' },
{ : 'bob' },
])
// NOT: exclude matching records
const = ('profiles').({ : 'ALUMNI' })Sorting: .orderBy()
// Single sort
const = ('packs').('last_activity_at', 'desc')
// Multiple sort keys: first call has highest priority
const = ('packs')
.('last_activity_at', 'desc')
.('display_name', 'asc')The direction can be 'asc' or 'desc'. Use 'asc' for the smallest or oldest value first. Use 'desc' for the largest or newest value first. The default is 'desc'.
Limiting results: .take()
.take() sets the maximum number of records in the response. Use a small value to decrease the response size and time:
const = ('packs').(20)Offset pagination: .skip() and .page()
.skip() omits a specified number of matching records. .page() calculates the skip value and starts page numbers at 1:
// Manual offset
const = ('posts').(25).(50) // items 51-75
// Page-based (1-indexed)
const = ('posts').(3, 25) // page 3, 25 items per page → items 51-75Executing queries: pb.search()
Give pb.search() an object that contains one or more built queries. Select the property names. The result uses the same names:
const { , } = await .({
: ('packs').('last_activity_at').(10).(),
: ('profiles').('created_at', 'asc').(10).(),
})Handling per-query errors: isErrorEntry()
One request can contain multiple queries. One query can fail while the other queries are successful. isErrorEntry() identifies whether a specified result is data or an error:
const { } = await .({
: ('packs').({ : { : 'art' } }).(),
})
if (()) {
.('Query failed:', packs.)} else {
.('Packs found:', packs.)}Complete example
This example searches for packs that a specified user owns. It sorts the most recently active packs first. It returns a maximum of 50 packs.
import { PackbaseSDK, from, isErrorEntry } from '@packbase/sdk-ts'
const pb = new PackbaseSDK()
// ---cut---
// Find all packs owned by a user, most recently active first
const { myPacks } = await pb.search({
myPacks: from('packs')
.where({ owner_id: 'user-uuid' })
.orderBy('last_activity_at', 'desc')
.take(50)
.build(),
})
if (!isErrorEntry(myPacks)) {
for (const pack of myPacks) {
console.log(pack.display_name, pack.last_activity_at)
}
}TypeScript tips
You do not have to write a generic type argument. TypeScript infers it from from('packs'). It restricts .where() and .orderBy() to pack fields:
// ✅ fine
('packs').({ : { : 'art' } })
// ❌ TypeScript error: 'email' is not a field on 'packs'
('packs').({ email: 'foo@bar.com' })