firebirdrest-js
v0.4.0
Published
JavaScript/TypeScript client for FirebirdREST with the supabase-js query API
Maintainers
Readme
firebirdrest-js
The supabase-js query API for
FirebirdREST: from(), select(), filters, embedding,
rpc(), inserts, upserts, counts, and the rest, against a Firebird database.
FirebirdREST speaks PostgREST's URL grammar, so this package is a thin layer over
@supabase/postgrest-js, the query builder inside
supabase-js, plus what Firebird adds: its own filter operators, raw file downloads and time zone
handling. There is no auth, storage or realtime module: FirebirdREST authenticates with JWTs
you issue, and Firebird has none of the rest.
npm install firebirdrest-jsimport { createClient } from 'firebirdrest-js'
const client = createClient('http://localhost:3000', token) // token: a JWT, or omit for anonymous
const { data, error } = await client
.from('books')
.select('title, price, author:authors(name), tags(name)')
.eq('in_stock', true)
.order('price', { ascending: false })
.limit(10)Works in Node 18+, Deno, Bun and browsers (anything with fetch).
What works
Everything postgrest-js offers that FirebirdREST implements, checked by the compatibility suite in CI against the FirebirdREST example database:
| | |
|---|---|
| Reads | select with aliases, casts, embedding (to-one, to-many, many-to-many, !inner), aggregates (count(), sum()...), computed fields |
| Filters | eq, neq, gt, gte, lt, lte, like, ilike, is, in, not, or, match, filter, filters on embedded resources |
| Shaping | order (including by an embedded column), limit, range, single, maybeSingle, csv, explain, abortSignal |
| Counts | { count: 'exact' \| 'planned' \| 'estimated', head: true } |
| Writes | insert (one or many), upsert with onConflict, update, delete, .select() for the representation |
| RPC | rpc(name, args, { get: true }) for functions, selectable procedures with .select().order()..., package routines ('catalog.book_count') |
| Schemas | client.schema('inventory') on Firebird 6 with db-schemas |
| Types | npx firebirdrest-js gen types writes the Database type for createClient<Database>() (typed queries) |
| Events | client.channel('order_placed').on(...).subscribe() streams Firebird POST_EVENTs (FirebirdREST 0.5.0 with db-events) |
| Errors | { code, message, details, hint } as in supabase-js; code is an FBRST... code or Firebird's SQLSTATE |
Not available, because Firebird has no such types: contains, containedBy, overlaps,
textSearch, range and JSON operators. See
Differences from PostgREST.
Firebird's operators
CONTAINING (case-insensitive substring), STARTING WITH and SIMILAR TO are available as
functions, and as methods for JavaScript callers:
import { createClient, containing, starting, similar } from 'firebirdrest-js'
const q = client.from('books').select('title')
await containing(q, 'title', 'gods') // title CONTAINING 'gods'
await starting(q, 'title', 'The') // title STARTING WITH 'The'
await similar(q, 'title', '(A|The) %') // title SIMILAR TO '(A|The) %'
await client.from('books').select('title').containing('title', 'gods') // JavaScript
await client.from('books').select('title').filter('title', 'containing', 'gods') // eitherTyped queries
gen types writes the TypeScript types of your API, the way supabase gen types does, so
from(), select(), insert() and rpc() are checked by the compiler and their results typed:
npx firebirdrest-js gen types --url http://localhost:3000 --token "$JWT" --output src/database.types.tsimport { createClient } from 'firebirdrest-js'
import type { Database, Tables } from './database.types'
const client = createClient<Database>('http://localhost:3000', token)
const { data } = await client.from('books').select('title, authors(name), tags(name)')
// data: { title: string; authors: { name: string } | null; tags: { name: string }[] }[] | null
await client.from('books').insert({ title: 'Mort' }) // a missing NOT NULL column is a compile error
await client.rpc('add_vat', { amount: 100 }) // data: number | null
type Author = Tables<'authors'>What it reads from the server's OpenAPI document (FirebirdREST 0.7 or later for all of it):
| | |
|---|---|
| tables and views | Row, Insert (columns without a default required), Update; views typed apart |
| foreign keys | embedding in both directions, one-to-one, many-to-many through junction tables, composite keys |
| computed fields | typed when selected by name, left out of select('*') as the server does |
| functions and procedures | arguments (required unless they have a default) and results: a value, rows, or an object |
| Firebird 6 schemas | one entry per exposed schema, for client.schema('inventory') |
- The document lists what the token's role may use. Generate with a role that sees everything
the application uses, or run the server with
openapi-mode = "ignore-privileges"in development. - A foreign key into another schema is embeddable but not typed: postgrest-js types relationships
within one schema.
gen typeslists such keys on standard error. - Computed relationships (procedures embedded as resources) are not typed.
- Against a server before 0.7, views, composite keys and argument defaults are not known: views are typed as tables and every argument is optional.
--schema public,inventorylimits the schemas;FIREBIRDREST_URLandFIREBIRDREST_TOKENcan replace--urland--token. The generator is also a function:import { generateTypes } from 'firebirdrest-js/gen'.
Regenerate after changing the database.
Realtime events
FirebirdREST streams Firebird events (POST_EVENT 'order_placed' in a trigger or procedure) to
clients as Server-Sent Events, for the names listed in its db-events setting. A channel listens
for one or more of them:
const channel = client
.channel('order_placed', 'stock_changed')
.on('order_placed', (e) => refreshOrders()) // e.name, e.count (posts merged into this notification)
.on('*', (e) => console.log('event', e.name))
.onReset((reason) => reloadOrders()) // notifications may have been missed: read again
.subscribe((status, error) => console.log(status, error)) // 'open' | 'error' | 'closed'
channel.unsubscribe()An event says that something happened, not what: read the data through the API as usual. The
channel reconnects by itself while the server is unreachable (1 s to 30 s between attempts),
sending the last notification's id so FirebirdREST 0.8+ replays what was missed; when the server
can't know (it restarted, or the client was away too long) onReset runs instead. It
stops when the server refuses the subscription (an event that isn't exposed, a role that may not
listen). It uses fetch with a streaming body, so it works in browsers and in Node 18+ without
EventSource, and sends the client's Authorization header.
Files and single values
file() sends a query with the Accept type you choose and returns the one value as it is: a
Blob for binary types, a string for text/*. The query must select one column and match one
row.
const { data } = await client.file(
client.from('covers').select('image').eq('book_id', 1), 'image/png')
// data: Blob with the PNG bytes
const { data: title } = await client.file(
client.from('books').select('title').eq('id', 1), 'text/plain')In JSON responses, binary columns are base64 strings, as FirebirdREST sends them.
Options
const client = createClient(url, token, {
headers: { 'X-Request-Id': '...' }, // extra headers for every request
schema: 'inventory', // Firebird 6 SQL schema (Accept-Profile / Content-Profile)
timezone: 'Europe/Bucharest', // Prefer: timezone= for WITH TIME ZONE values (Firebird 4+)
fetch: myFetch, // custom fetch
})
client.auth(newToken) // switch the JWT laterTests
The suite runs against a FirebirdREST server with the bookstore example:
FIREBIRDREST_URL=http://localhost:3000 \
FIREBIRDREST_JWT_SECRET=reallyreallyreallyreallyverysafe1234 npm testWithout FIREBIRDREST_JWT_SECRET the write tests are skipped. CI starts Firebird 3, 5 and 6
(with db-schemas and the Firebird 6 schema example) and FirebirdREST in Docker, both the
released image and a build of its main branch (see .github/workflows/ci.yml).
Releasing
Publishing is done by GitHub Actions when a version tag is pushed, after the compatibility suite passes:
npm version 0.1.0 # updates package.json and creates the v0.1.0 tag
git push --follow-tagsOne-time setup, after the first manual npm publish: on npmjs.com open the package →
Settings → Trusted publisher → GitHub Actions, with repository mariuz/firebirdrest-js and
workflow release.yml. The workflow then publishes with a short-lived OIDC token, no secret
needed, and with provenance, so npm shows the source commit and workflow.
License
MIT.
