# 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
```
