Skip to content

Read Data records from JavaScript

@ebitex/data-sdk is the client for the Data Delivery API. It lists the models a key can read, then lists, filters, sorts, counts and pages records, follows references between them, and reads records by key. It has no runtime dependencies and works in a browser and on a server.

For what the API itself does, see Reading Data records. This page is about the package.

You need a delivery key for a partition. In Data, open Settings and Delivery keys.

  • In a browser, use a browser-safe key: one restricted to your site’s origins. It is visible to anyone who views the page, which is safe only because it works from the origins you listed, and it reads only the models and fields marked public.
  • On a server, use a server key. It reads everything in its partition. Never put it in a page.
Terminal window
npm install @ebitex/data-sdk
import { createDataClient } from '@ebitex/data-sdk'
const data = createDataClient({ apiKey: 'ebx_your_key' })

baseUrl defaults to https://api.ebitex.io. fetch defaults to the global one; pass your own to add retries, a cache or tracing, because the client adds none of its own.

Read models() first. It lists what your key may ask for: each field’s type, and for a field the model’s owner made queryable, whether it can be filtered, sorted or counted.

quick-start.ts
import { createDataClient } from '@ebitex/data-sdk'
// A key restricted to your site's origins is safe to ship to a browser, because it reads only what your models mark public.
// A key with no restriction reads everything in its partition: keep it on a server.
export const data = createDataClient({ apiKey: 'ebx_your_key' })
export async function list(): Promise<void> {
// Read this first: it is the list of what you may ask. Each field says whether it can be filtered, sorted or counted.
const { partition, models } = await data.models()
console.log(partition.key, models.map((m) => m.key))
// Records of "dog" and of every model that inherits from it.
const page = await data.records({
model: 'dog',
filters: { colour: { in: ['black', 'red'] }, born: { gte: new Date('2024-01-01T00:00:00Z') } },
sort: '-born',
limit: 25,
facets: ['colour'],
})
for (const dog of page.items) console.log(dog.key, dog.name)
console.log(page.facets?.colour) // the 20 most common values with counts, over every match, not only this page
console.log(page.nextCursor) // null on the last page
}
export async function one(): Promise<void> {
// null when there is no such record, or this key may not read it. The two answers are the same on purpose.
const dog = await data.record('rex-of-drakeshead')
console.log(dog?.name)
}

records() takes a model (a model’s records and those of every model that inherits from it) and any of:

Option Meaning
filters A bare value means equals. An object names operators: eq, in (an array), prefix, gt, gte, lt, lte and exists. A Date is sent as an RFC 3339 instant. Several filters are combined with AND, up to 10 operators in all.
search Matches the start of a record’s name, or of any word in it, without regard to case.
sort name (the default), updated, or a sortable field. A leading - sorts descending.
facets Up to 5 facetable fields, and the built-in model. Counts cover every match, not only the page.
limit 1 to 100.
generations, expand Follow references. See Follow references.

Filters, field sorts and facets need a model. A field that is private, undeclared or absent answers the same way: DataApiError with code field_not_queryable.

A request the API would certainly refuse (a limit of 500, more than 5 facets, a filter with no model) is refused by the client first, with the same code, as a DataApiError whose local is true. The API stays the authority: these checks only save a round trip.

records() returns one page and a nextCursor, which is null on the last page. A cursor belongs to its query, so it must be sent back with exactly the same parameters. eachRecord() and eachPage() do that for you: they capture the query once and send it unchanged with every cursor. If a cursor is ever rejected, the error is raised. The iteration never starts over, because that would silently repeat or skip records.

paging-and-batches.ts
import { createDataClient } from '@ebitex/data-sdk'
const data = createDataClient({ apiKey: 'ebx_your_key' })
export async function everyDog(): Promise<string[]> {
const keys: string[] = []
// The query is captured once and sent unchanged with each page's cursor: a cursor belongs to its query.
for await (const dog of data.eachRecord({ model: 'dog', sort: 'name', limit: 100 })) keys.push(dog.key)
return keys
}
export async function byKey(): Promise<void> {
// Up to 50 keys in one round trip, in the order asked. A key that does not exist, or that this key may not read, is in `missing`.
const { items, missing } = await data.recordsByKeys(['rex-of-drakeshead', 'no-such-dog'])
console.log(items.length, missing)
}
export async function revalidate(etag: string | null): Promise<string | null> {
// Responses are `private, no-cache` with an ETag: ask again, and an unchanged record costs a 304 instead of a body.
const result = await data.recordResponse('rex-of-drakeshead', { ifNoneMatch: etag ?? undefined })
if (result.status === 'notModified') return etag
if (result.status === 'missing') return null
return result.etag
}

recordsByKeys() reads up to 50 records in one round trip, in the order you asked. A key that does not exist, or that your key may not read, is listed in missing.

record() returns one record, or null when there is none. Those two answers are the same on purpose, so a browser-safe key cannot learn that a private record exists. Pass throwIfMissing: true to get an error instead.

Responses carry an ETag and are private, no-cache. recordResponse() keeps the tag and sends If-None-Match when you pass ifNoneMatch, so an unchanged record costs a 304 and not a body.

Add generations (0 to 5) to put each referenced record beside its pointer, as record. expand follows only the reference fields you name. A request expands at most 200 records; when that cuts the result short, meta.truncated is true.

expanded(value) returns the record a reference was expanded to, or undefined for a pointer that was not followed: a cycle, a missing target, or one your key may not read. None of those is an error.

references-and-errors.ts
import { createDataClient, DataApiError, DataNetworkError, expanded } from '@ebitex/data-sdk'
const data = createDataClient({ apiKey: 'ebx_your_key' })
interface DogValues {
sire?: unknown
colour?: string
}
export async function pedigree(): Promise<void> {
// Follow references two hops. The target sits beside its pointer, in the same shape as any record.
const dog = await data.record<DogValues>('rex-of-drakeshead', { generations: 2 })
if (!dog) return
const sire = expanded<DogValues>(dog.values.sire)
// undefined for a pointer that was not followed: a cycle, a missing target, or one this key may not read.
console.log(sire?.name ?? 'sire unknown')
// At most 200 records are expanded in one request; `meta.truncated` says the budget cut it short.
if (dog.meta?.truncated) console.log('fetch the rest with recordsByKeys')
}
export async function handle(): Promise<void> {
try {
await data.records({ model: 'dog', filters: { weight: { gt: 10 } } })
} catch (error) {
if (error instanceof DataApiError) {
// `code` is the server's: 'field_not_queryable' covers a field that is private, undeclared and absent alike.
console.error(error.status, error.code, error.message, 'quote this to support:', error.requestId)
if (error.status === 429) console.error('try again in', error.retryAfterSeconds, 'seconds')
} else if (error instanceof DataNetworkError) {
console.error('could not reach ebitex', error.cause)
} else {
throw error
}
}
}
Error Meaning
DataApiError The API answered with an error, or the client refused the request. status, code (the API’s error code), message, and requestId, the reference to quote to support. On a 429, retryAfterSeconds.
DataNetworkError The request could not complete. cause is the underlying error.

Nothing is retried. A 429 is reported, with how long to wait, and the decision is yours.

On a .NET server, use Ebitex.Data.Delivery, which reads the same API and is held to this package by shared test cases. For anything else, the API reference describes each request.