Skip to content

Repository files navigation

@truenas/api-client

Framework-agnostic TypeScript client for the TrueNAS JSON-RPC 2.0 WebSocket API.

Status: early extraction in progress. The client is being pulled out of the TrueNAS Connect UI into this standalone package.

Requirements

  • Node ≥ 22 (provides a global WebSocket) or a browser. On older Node, supply a WebSocket implementation (e.g. the ws package) via the socket config.
  • rxjs ^7.8 is a peer dependency — the consuming project provides it.

Usage

import { createTrueNasClient } from '@truenas/api-client';

const client = await createTrueNasClient({
  uuid: 'system-uuid',
  hostnames: ['truenas.local'],
  enabled: true,
});

createTrueNasClient does not take credentials, so log in before calling anything — middleware refuses an unauthenticated call, and authenticated$ only turns true once one of these resolves:

await firstValueFrom(
  client.authenticator.loginWithApiKey({ username, key })
);
// or client.authenticator.loginWithUserPass(username, password)

Everything below hangs off client.api, and every method name it accepts comes from types generated from middlewared --dump-api. A name the declared version does not have is a compile error, and params and responses come from the same source — there is no list of endpoint constants to import.

client.api.call('system.info');                        // SystemInfoResult
client.api.call('alert.dismiss', ['uuid-1']);          // params required
client.api.call('nope.nope');                          // ✗ compile error

Queries. Middleware's .query methods are polymorphic in their options — the same endpoint returns a list, one entry, or a count. Which you get is chosen by the verb, so there is nothing to narrow:

client.api.query('user.query', [['uid', '>', 1000]]);  // UserEntry[]
client.api.queryOne('user.query', [['id', '=', 1]]);   // UserEntry
client.api.queryCount('user.query');                   // number

client.api.query('user.query', [], { select: ['id', 'username'] });
                                     // Pick<UserEntry, 'id' | 'username'>[]

Use satisfies rather than an annotation when building options into a variable — an annotated QueryListOptions<E> widens select, and the result degrades to Partial<E>[].

Jobs. A separate key space from call: app.start runs as a job and does not appear in the call directory. job starts one and follows it to completion, typing the result from the job directory:

client.api.job('pool.dataset.export_key', ['tank/enc'])
  .subscribe(job => report(job.progress.percent));     // Job<string | null>

Events. Emits the change as a union discriminated on msg. Narrowing is load-bearing: a removal carries an id and no fields in almost every collection.

client.api.events('app.query').subscribe(event => {
  if (event.msg === 'removed') return drop(event.id);
  render(event.fields);
});

Naming a version

The version is discovered at runtime; the types are fixed at compile time. createTrueNasClient defaults to the oldest supported version, which understates a newer server rather than promising methods it lacks. Name a version to reach the rest:

const client = await createTrueNasClient<ApiDirectoryV26_0_0>(opts);
client.api.query('container.query');                   // v26-only, reachable

That is a claim about the server, not a guarantee — the client you get is whichever version discovery found. Operations that must work across versions belong on client.ops, which resolves them at runtime.

Documentation

The API reference is generated from the TSDoc comments in the source with TypeDoc and published to GitHub Pages with each npm release: https://truenas.github.io/api-client-ts/

yarn docs                # generate locally into docs/ (gitignored)
yarn docs:check          # validate doc comments without rendering (run in CI)

Development

corepack enable          # once, to enable Yarn 4
yarn install
yarn build               # bundle to dist/ (ESM + CJS + .d.ts) via tsup
yarn typecheck           # tsc --noEmit
yarn test                # vitest
yarn lint                # eslint

Layout

Sources live under src/, grouped by role:

src/
  connection/   api/   auth/   client/        # the WebSocket client, split by responsibility
  types/   enums/   utils/   config/   errors/
  logger.ts   factory.ts   version-discovery.ts   index.ts

Internal modules import each other through the @/* path alias (@/* → src/*). The alias is a build-time convenience only — it is inlined away during bundling and never reaches consumers; the public API is solely what src/index.ts (the barrel) re-exports.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages