UNPKG

fql-toolkit

Version:
355 lines (252 loc) 9.26 kB
# 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 ```