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.
Before you start
Section titled “Before you start”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.
Install
Section titled “Install”npm install @ebitex/data-sdkCreate the client and look around
Section titled “Create the client and look around”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.
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)}Filters, sorts and facets
Section titled “Filters, sorts and facets”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.
Paging
Section titled “Paging”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.
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}Read by key
Section titled “Read by key”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.
Follow references
Section titled “Follow references”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.
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 } }}Errors
Section titled “Errors”| 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.
Other languages
Section titled “Other languages”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.