fql-toolkit
Version:
355 lines (252 loc) • 9.26 kB
Markdown
# fql-toolkit
TypeScript client library for [FQL (Form Query Language)](../fql-docs/docs/fql-language.md). 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
```bash
npm install fql-toolkit
```
---
## Quick Start
```ts
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('user@example.com', '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
```ts
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: '...' }`.
```ts
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('user@example.com', '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('user@example.com', { 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('user@example.com', { resetPasswordURL: 'https://myapp.com/reset?token=' });
await auth.resetPassword(token, 'newpassword');
```
### State access
```ts
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
```ts
type AuthUser =
| { id: number; token: string; role: 'GUEST' }
| { id: number; email: string; nickname: string; token: string; role: 'SUPERADMIN' | 'ADMIN' | 'USER' }
```
### `useAuth(manager)` — React hook
```ts
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)
```ts
const unsubscribe = auth.subscribe(() => {
console.log('auth state changed:', auth.getState());
});
unsubscribe(); // stop listening
```
### CL-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`:
```ts
// 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.
```ts
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?)`
```ts
await client.forms.show(); // all forms
await client.forms.show(['Orders', 'Customers']); // specific forms
```
### `client.forms.modify(formName, changes)`
```ts
await client.forms.modify('Orders', {
add: [{ name: 'discount', type: 'number' }],
remove: ['note'],
});
```
### `client.forms.remove(formNameOrNames)`
```ts
await client.forms.remove('Orders');
await client.forms.remove(['Orders', 'Customers']);
```
---
## `client.form(name)` — Record management (DML)
### `.create(data)`
```ts
await client.form('Orders').create({ amount: 150, approved: true });
```
### `.get()` / `.labels(...fields).get()`
```ts
// 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.
```ts
await client.form('Orders')
.with('amount', '>', 100)
.with('approved', '=', true)
.get();
```
Dot notation for reference fields:
```ts
.with('customer.name', '=', 'Acme Corp')
```
Supported operators: `=` `!=` `<>` `<` `>` `<=` `>=`
### `.modify(newValues)`
Updates matching records. At least one `.with()` required. Fetches `fql_token` automatically.
```ts
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.
```ts
await client.form('Orders')
.with('approved', '=', false)
.remove();
```
---
## `client.execute(fqlString, fqlToken?)`
Escape hatch for raw FQL commands not covered by the builder API.
```ts
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>>`:
```ts
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;
}
```
```ts
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.
```ts
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
```bash
# 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
```