Packbase

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)
topPacks: SearchPack[]
}

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

ModelFields 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:

OperatorUse case
equalsExact match (default for scalars)
notNegate an equality check
inMatch any value in an array
notInExclude values in an array
lt / lteLess-than / less-than-or-equal
gt / gteGreater-than / greater-than-or-equal
containsSubstring / array contains
startsWithPrefix match
endsWithSuffix match
modeCase-insensitive ('insensitive')
hasJSON array contains value
hasSomeJSON array has any of these values
hasEveryJSON array has all of these values
isEmptyJSON 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-75

Executing 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.)
const packs: ErrorEntry
} else { .('Packs found:', packs.)
const packs: SearchPack[]
}

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' })
Object literal may only specify known properties, and 'email' does not exist in type 'Partial<Record<"id" | "created_at" | "display_name" | "images_avatar" | "description" | "owner_id" | "images_header" | "last_activity_at", FilterValue>>'.

On this page