@flxbl-dev/client
v0.6.0
Published
Lightweight runtime SDK for FLXBL — zero dependencies, uses native fetch
Downloads
768
Maintainers
Readme
@flxbl-dev/client
Lightweight, type-safe runtime SDK for FLXBL. Zero dependencies — uses native fetch.
This package is the production dependency your app ships with. The generated typed client (from @flxbl-dev/cli) extends FlxblClient with schema-specific collections and full autocomplete.
Install
npm install @flxbl-dev/clientQuick Start
Use the generated client from @flxbl-dev/cli for full type safety:
import { createFlxblClient } from './flxbl/_generated';
const db = createFlxblClient({
instanceUrl: 'https://api.flxbl.dev',
apiKey: process.env.FLXBL_API_KEY,
});
// Fully typed — autocomplete on fields, type-checked inputs
const users = await db.users.query({ where: { role: 'admin' }, limit: 10 });
const post = await db.posts.create({
title: 'Hello',
content: '...',
published: true,
});
const user = await db.users.findById('abc123');
await db.users.update('abc123', { role: 'user' });
await db.users.delete('abc123');Or use the base client directly for untyped access:
import { FlxblClient } from '@flxbl-dev/client';
const client = new FlxblClient({
instanceUrl: 'https://api.flxbl.dev',
apiKey: process.env.FLXBL_API_KEY,
});
// Typed escape hatch without generated collections
const users = await client.query<User>('User', {
where: { role: 'admin' },
limit: 10,
});Configuration
import { FlxblClient } from '@flxbl-dev/client';
const client = new FlxblClient({
// Required
instanceUrl: 'https://api.flxbl.dev',
// Auth — provide one of:
apiKey: 'flxbl_abc123...', // API key (recommended for server-side)
accessToken: 'eyJhbG...', // JWT token (for browser/short-lived)
// Optional
fetch: customFetch, // Custom fetch implementation
});
// You can also set the token dynamically
client.setAccessToken('eyJhbG...');Collection Methods
Each generated collection (e.g., db.users) provides:
| Method | Description |
| ----------------------------- | --------------------------------------------------------------- |
| query(options?) | Explicit helper for complex filters, search, traversals, paging |
| findMany(options?) | Query with filters, sorting, pagination, search, and traversals |
| findFirst(options?) | Get the first matching record or null |
| count(where?) | Count records matching a where clause |
| exists(where?) | Check if any record matches a where clause |
| findById(id) | Get a single record by ID |
| create(data) | Create a new record |
| batchCreate(data[]) | Create multiple records via the Dynamic REST batch endpoint |
| update(id, data) | Partial update by ID |
| batchUpdate([{ id, data }]) | Update multiple records via the Dynamic REST batch endpoint |
| delete(id) | Delete by ID |
| batchDelete(ids[]) | Delete multiple records via the Dynamic REST batch endpoint |
| vectorSearch(options) | Search VECTOR fields; generated only for entities with vectors |
Query Options
Use `query()` for the explicit Dynamic REST graph query endpoint. `findMany()`
remains available as the CRUD-style alias and accepts the same options.
```typescript
const result = await db.users.query({
where: { isActive: true, role: { $in: ['admin', 'editor'] } },
select: ['id', 'email', 'role'],
orderBy: 'createdAt',
orderDirection: 'DESC',
offset: 0,
limit: 25,
includeCount: true,
traverse: [{ relationship: 'AUTHORED', direction: 'out', select: ['title'] }],
});
console.log(result.items); // User[]
console.log(result.count); // total count (if includeCount: true)
const sameResult = await db.users.findMany({ where: { isActive: true } });
const baseResult = await client.query<User>('User', {
search: 'admin',
limit: 10,
});Vector Search
Generated collections expose vectorSearch() only when the entity schema has
one or more VECTOR fields.
const matches = await db.documents.vectorSearch({
field: 'embedding',
vector: embedding,
topK: 10,
where: { status: { $eq: 'published' } },
select: ['title', 'summary'],
});
const baseMatches = await client.vectorSearch<Document>('Document', {
field: 'embedding',
vector: embedding,
topK: 10,
});
console.log(matches[0].data);
console.log(matches[0].score);Batch Operations
const created = await db.hours.batchCreate([
{ date: '2026-04-01', hours: 8 },
{ date: '2026-04-02', hours: 7.5 },
]);
await db.hours.batchUpdate([
{ id: 'hour-1', data: { hours: 8.5 } },
{ id: 'hour-2', data: { hours: 6 } },
]);
await db.hours.batchDelete(['hour-1', 'hour-2']);
console.log(created.successCount);Filter Operators
| Operator | Description |
| -------------------------- | ----------------------- |
| $eq | Equals |
| $neq | Not equals |
| $gt, $gte | Greater than (or equal) |
| $lt, $lte | Less than (or equal) |
| $in, $notIn | Value in / not in array |
| $isNull | Is null check |
| $contains | String contains |
| $startsWith, $endsWith | String prefix/suffix |
| $and, $or | Logical operators |
GraphQL
const { data, errors } = await client.graphql(
'tenant-id',
`query { users { id email } }`,
{
/* variables */
},
);Relationships
const rels = client.relationships('User', 'user-123');
// Create a relationship
await rels.create('AUTHORED', 'post-456', { role: 'primary' });
// List related entities
const { items, count } = await rels.list('AUTHORED', {
direction: 'out',
limit: 10,
offset: 0,
});
console.log(items[0]._relationship.properties);
console.log(items[0]._relationship.id);
// Update relationship properties
await rels.update('AUTHORED', 'post-456', { role: 'secondary' });
// Update one edge by durable relationship id
await rels.updateById('AUTHORED', 'rel_789', { role: 'reviewer' });
// Delete a relationship
await rels.delete('AUTHORED', 'post-456');
// Delete one edge by durable relationship id
await rels.deleteById('AUTHORED', 'rel_789');Endpoint-based update and delete address the edge by source, relationship
name, and target id. If your model allows parallel same-type edges between the
same two records, use _relationship.id with updateById and deleteById.
Typed Relationship Helpers
Generated clients expose relationship helpers on source collections:
const assignments = await db.workers.relationships(workerId).assignedTo.list({
where: { status: { equals: 'active' } },
edgeWhere: { role: { equals: 'PRIMARY' } },
limit: 10,
orderBy: 'name',
orderDirection: 'ASC',
});
await db.workers.relationships(workerId).assignedTo.update(projectId, {
role: 'BACKUP',
});
await db.workers.relationships(workerId).assignedTo.updateById('rel_789', {
role: 'PRIMARY',
});The helper delegates to client.relationships(entityName, id), so advanced
code can still use the base API directly.
Error Handling
import {
FlxblApiError,
FlxblAuthError,
FlxblValidationError,
} from '@flxbl-dev/client';
try {
await db.users.create({ email: 'bad' });
} catch (error) {
if (error instanceof FlxblAuthError) {
// 401 — re-authenticate
} else if (error instanceof FlxblValidationError) {
// 400 — check error.validationErrors
} else if (error instanceof FlxblApiError) {
// Generic API error — check error.statusCode
}
}How It Works
Under the hood, the client maps collection methods to FLXBL's Dynamic REST API:
| Method | HTTP Request |
| ---------------------------- | ---------------------------------------- |
| db.users.query(opts) | POST /api/v1/dynamic/User/query |
| db.users.findMany(opts) | POST /api/v1/dynamic/User/query |
| db.users.findById(id) | GET /api/v1/dynamic/User/:id |
| db.users.create(data) | POST /api/v1/dynamic/User |
| db.users.batchCreate(data) | POST /api/v1/dynamic/User/batch |
| db.users.update(id, data) | PATCH /api/v1/dynamic/User/:id |
| db.users.batchUpdate(data) | PATCH /api/v1/dynamic/User/batch |
| db.users.delete(id) | DELETE /api/v1/dynamic/User/:id |
| db.users.batchDelete(ids) | DELETE /api/v1/dynamic/User/batch |
| db.docs.vectorSearch(opts) | POST /api/v1/dynamic/Doc/vector-search |
Requirements
- Node.js >= 18 (for native
fetch) - A FLXBL instance with an active schema
License
MIT
