fql-toolkit
v4.9.0
Published
Fql Toolkit
Readme
fql-toolkit
TypeScript client library for FQL (Form Query Language). Wraps the CL-FQL REST API with a typed, chainable interface so frontends don't have to hand-craft FQL strings or manage fql_token manually.
Installation
npm install fql-toolkitQuick Start
import FQLClient from 'fql-toolkit';
import { AuthManager } from 'fql-toolkit/auth';
// 1. Create an AuthManager (handles login, guest sessions, storage)
const auth = new AuthManager({
baseURL: 'http://localhost:7645',
storage: localStorage,
});
await auth.login('[email protected]', 'password');
// 2. Create a client that reads the token from auth automatically
const client = new FQLClient({
baseURL: 'http://localhost:7645',
auth,
});
// 3. Use it
const res = await client.form('Employees')
.labels('name', 'salary')
.with('salary', '>', 50000)
.get();
if (res.ok) {
console.log(res.data);
} else {
console.error(res.error);
}Auth module
fql-toolkit/auth exports AuthManager — a pure TypeScript class that handles all CL-FQL auth operations, persists state to configurable storage, and notifies subscribers on changes.
Import paths
import { AuthManager } from 'fql-toolkit/auth';
import { useAuth } from 'fql-toolkit/auth/react';
import type { AuthUser, AuthState, AuthResult } from 'fql-toolkit/auth';new AuthManager(options)
| Option | Type | Description |
|-----------|---------------------------|-------------|
| baseURL | string | Backend base URL, e.g. http://localhost:7645 |
| storage | AuthStorage \| null | Where to persist auth state. Pass localStorage, sessionStorage, or null for memory-only. |
AuthStorage is any object with getItem / setItem / removeItem — localStorage and sessionStorage both qualify.
Auth state is persisted under the key "fql-auth". On construction, AuthManager hydrates from storage automatically — no extra init() call needed.
Auth operations
All async methods return Promise<AuthResult> and never throw. On network or HTTP error they return { ok: false, error: '...' }.
const auth = new AuthManager({ baseURL: 'http://localhost:7645', storage: localStorage });
// Guest session (always available, even before login)
await auth.enterAsGuest();
// Login
const result = await auth.login('[email protected]', 'password');
if (!result.ok) console.error(result.error);
// Logout — clears state and automatically re-enters as guest
await auth.logout();
// Registration (sends activation email; result.user is absent)
await auth.register('[email protected]', { activationURL: 'https://myapp.com/activate?token=' });
// Account activation (does not set user state — call login() after)
await auth.activateAccount('nickname', activationToken, 'password');
// Password reset
await auth.requestPasswordReset('[email protected]', { resetPasswordURL: 'https://myapp.com/reset?token=' });
await auth.resetPassword(token, 'newpassword');State access
auth.getToken(); // string | null
auth.getState(); // { user: AuthUser | null, token: string | null, loggedIn: boolean }loggedIn is true only when the authenticated user has role !== 'GUEST'.
AuthUser type
type AuthUser =
| { id: number; token: string; role: 'GUEST' }
| { id: number; email: string; nickname: string; token: string; role: 'SUPERADMIN' | 'ADMIN' | 'USER' }useAuth(manager) — React hook
import { useAuth } from 'fql-toolkit/auth/react';
function MyComponent() {
const { user, token, loggedIn } = useAuth(auth);
// Re-renders on every auth state change
}Requires React 18+. Uses useSyncExternalStore internally — SSR-safe.
Observer (non-React)
const unsubscribe = auth.subscribe(() => {
console.log('auth state changed:', auth.getState());
});
unsubscribe(); // stop listeningCL-FQL endpoints called
| Operation | Endpoint |
|-----------|----------|
| enterAsGuest | POST /api/auth/register/guest |
| login | POST /api/auth/login |
| register | POST /api/auth/register |
| activateAccount | POST /api/auth/activate |
| requestPasswordReset | POST /api/auth/request-password-reset |
| resetPassword | POST /api/auth/reset-password |
new FQLClient(config)
Accepts either a static token or an AuthManager:
// Static token (simple scripts, server-side)
const client = new FQLClient({ baseURL: 'http://localhost:7645', token: 'jwt-token' });
// AuthManager (recommended for frontends — token is read dynamically on every request)
const client = new FQLClient({ baseURL: 'http://localhost:7645', auth: authManager });| Option | Type | Description |
|-----------|-----------------|-------------|
| baseURL | string | Backend base URL |
| token | string | Static JWT. Use when auth is not provided. |
| auth | AuthManager | Auth manager. Token is read on every request — stays current after login/logout. |
client.forms — Form management (DDL)
client.forms.create(input)
Creates a new form. dataSpecs defines scalar fields; dataRefs defines reference fields.
await client.forms.create({
name: 'Orders',
dataSpecs: [
{ name: 'amount', type: 'number', notNull: true },
{ name: 'note', type: 'text' },
{ name: 'approved', type: 'boolean' },
],
dataRefs: [
{ name: 'customer', cardinality: [1, 1], path: 'Customers.id' },
{ name: 'items', cardinality: [0, 'many'], path: 'Products.id' },
],
});Type aliases accepted for type: string → text, int / integer / float / double → number, bool → boolean.
client.forms.show(formNames?)
await client.forms.show(); // all forms
await client.forms.show(['Orders', 'Customers']); // specific formsclient.forms.modify(formName, changes)
await client.forms.modify('Orders', {
add: [{ name: 'discount', type: 'number' }],
remove: ['note'],
});client.forms.remove(formNameOrNames)
await client.forms.remove('Orders');
await client.forms.remove(['Orders', 'Customers']);client.form(name) — Record management (DML)
.create(data)
await client.form('Orders').create({ amount: 150, approved: true });Reference keys and including values
ref() works in both create and modify, for any reference field —
1..1 or 1..many. For a 1..1 field, pass a bare key (no including values):
import { ref } from 'fql-toolkit';
await client.form('Cities').create({
name: 'Montevideo',
country: ref(['Uruguay', 'UY']),
});
// create Cities('Montevideo', {'Uruguay', 'UY'})For a 1..many field, pass a list of ref() elements. Each element with
including values is serialised as ({key}, i1, …):
await client.form('Orders').create({
name: 'O1',
items: [ref(['MacBook', 'Laptop'], 7), ref(['iPhone'], 3)],
});
// create Orders('O1', (({'MacBook', 'Laptop'}, 7), ({'iPhone'}, 3)))ref(key, ...including): key lists the values of the reference's key fields
in path order (a prefix is allowed); it must match exactly one record. Pass
all including values or none.
If a single ref with including values is the field's whole value (not
wrapped in an array), it is auto-wrapped in an extra set of parentheses so
the engine parses it as one element rather than as a bare key followed by a
stray scalar:
await client.form('Orders').create({ name: 'O1', items: ref(['MacBook'], 7) });
// create Orders('O1', (({'MacBook'}, 7)))ref() also takes a named key, to match by any subset of the reference's
key fields in any order:
await client.form('People').create({ name: 'Ana', home: ref({ 'country.name': 'Uruguay' }) });
// create People('Ana', {country.name = 'Uruguay'})Nested is equivalent: ref({ country: { name: 'Uruguay' } }).
FQLRef (the class ref() returns) and isFQLRef (a brand check that also
recognises FQLRef instances from a different copy of this package) are
exported from fql-toolkit.
Requires a CL-FQL engine with reference-key support.
.get() / .labels(...fields).get()
// All records, all fields
await client.form('Orders').get();
// Specific fields
await client.form('Orders').labels('amount', 'approved').get();
// With filter
await client.form('Orders')
.labels('amount')
.with('approved', '=', true)
.get();Structured read output (responseFormat)
get case can return nested, typed JSON instead of the flat dotted-key shape.
Opt in per query, per call, or per client:
// per query (builder) — returns FQLResult<FQLStructuredOutput>
const res = await client.form('Persons').labels('name', 'home.name').getStructured();
// res.data === { schema: FQLFieldSchema[], data: FQLRecord[] }
// per query, explicit
await client.form('Persons').labels('name', 'home.name').get({ responseFormat: 'structured' });
// per call (raw escape hatch)
await client.execute('get Persons (name, home.name)', null, { responseFormat: 'structured' });
// per client (default for every read)
const client = new FQLClient({ baseURL, auth, responseFormat: 'structured' });Structured output is { schema, data }: data nests a 1..1 reference as an
object and a multiple reference as an array of objects, always including the
root fql_id; schema describes each field (show forms vocabulary,
snake_case keys). The envelope fql_token comes back in a clean
{ form_name, token } shape in structured mode and normalizes to the same
FQLToken ({ token, formName }) as flat, so it round-trips into modify case
unchanged. Omit responseFormat (or pass 'flat') for the flat output.
Requires a CL-FQL backend with the structured feature deployed.
.with(field, operator, value)
Adds a filter condition. Multiple calls are ANDed together.
await client.form('Orders')
.with('amount', '>', 100)
.with('approved', '=', true)
.get();Dot notation for reference fields:
.with('customer.name', '=', 'Acme Corp')Supported operators: = != <> < > <= >=
.modify(newValues)
Updates matching records. At least one .with() required. Fetches fql_token automatically.
await client.form('Orders')
.with('amount', '=', 150)
.modify({ amount: 200, approved: false });.remove()
Removes matching records. At least one .with() required. Fetches fql_token automatically.
await client.form('Orders')
.with('approved', '=', false)
.remove();client.execute(fqlString, fqlToken?)
Escape hatch for raw FQL commands not covered by the builder API.
await client.execute('show forms');
await client.execute('get Employees with salary > 50000');
// Pass fql_token manually for raw modify/remove:
const getRes = await client.execute('get Orders with amount = 150');
await client.execute('remove Orders with amount = 150', getRes.fqlToken);Response shape
All methods return Promise<FQLResult<T>>:
interface FQLResult<T = unknown[]> {
ok: boolean; // false on error
data: T | null; // null when ok === false
error: string | null; // non-null when ok === false
message: string; // backend message (always present)
fqlToken: FQLToken | null;
}const res = await client.form('Orders').get();
if (!res.ok) {
console.error(res.error);
} else {
const orders = res.data; // unknown[] by default, cast as needed
}Error handling
Validation errors (missing name, unknown type, missing .with() condition) throw FQLError synchronously before any network call.
import { FQLError } from 'fql-toolkit';
try {
await client.form('Orders').remove(); // no .with() → throws
} catch (err) {
if (err instanceof FQLError) console.error(err.message);
}Network and backend errors are returned as FQLResult with ok: false — they do not throw.
Testing
# Unit tests (no backend needed)
npm run test:unit
# Integration tests (requires a running CL-FQL backend)
# 1. Copy .env.example → .env and fill in FQL_URL and FQL_API_KEY
# 2. Run:
npm run test:integration