fql-toolkit
v4.2.5
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 });.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();.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