npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@quadrokit/client

v0.3.23

Published

Typed 4D REST client and catalog code generator for QuadroKit

Downloads

5,252

Readme

@quadrokit/client

Typed 4D REST client: a small runtime (fetch with credentials: 'include', lists, entity sets, $attributes) and a code generator that turns a catalog JSON into a typed createClient and entity types.


Install

npm add @quadrokit/client
# or
bun add @quadrokit/client

The package exposes:

  • @quadrokit/client — runtime + types (and anything your generated code needs).
  • @quadrokit/client/runtime — low-level helpers if you build custom integrations.
  • @quadrokit/client/rx — optional RxJS bridge for QuadroEventBus (install rxjs in your app).

Generate the client from your 4D catalog

Install adds the quadrokit-client binary (alias: quadrokit).

bunx quadrokit-client generate \
  --url 'http://localhost:7080/rest/$catalog/$all' \
  --out .quadrokit/generated

Why $catalog/$all?

Use a URL that returns the full catalog (dataclasses with attributes). Plain /rest/$catalog often lists tables only; without attribute metadata, generated select paths and typings are incomplete. The generator warns if attributes are missing.

CLI options

| Option | Description | |--------|-------------| | --url | Catalog URL, or file:///absolute/path/to/catalog.json. If omitted: ${VITE_4D_ORIGIN}/rest/$catalog/$all (from env / .env), else http://127.0.0.1:7080/rest/$catalog/$all. | | --out | Output directory (default: .quadrokit/generated). | | --token | Authorization: Bearer … for protected catalog endpoints (generator HTTP only). | | --access-key | Multipart accessKey to 4D’s login API; generator obtains 4DAdminSID and uses it for the catalog request. Requires an http(s) --url unless you pass --login-url. | | --login-url | Full login URL (default: {catalog origin}/api/login). | | -v / --verbose | Step-by-step logs on stderr. | | --insecure-tls | Skip TLS verification (dev / self-signed HTTPS only). | | --no-split-type-files | Emit a single types.gen.ts. Default: split typings — entities/<Class>.gen.ts, optional datastore.gen.ts, and types.gen.ts as the barrel. |

Environment variables (often via .env in the project root): VITE_4D_ORIGIN, QUADROKIT_ACCESS_KEY, QUADROKIT_LOGIN_URL, QUADROKIT_CATALOG_TOKEN, QUADROKIT_GENERATE_VERBOSE, QUADROKIT_INSECURE_TLS.

Generated files

| File | Purpose | |------|---------| | types.gen.ts | Barrel: QuadroClient typing; re-exports entity types and *Path aliases. | | entities/<ClassName>.gen.ts | One file per exposed dataclass (default layout): export interface + export type <ClassName>Path (QuadroAttributePaths<…>). Omit with --no-split-type-files (single types.gen.ts). | | datastore.gen.ts | When the catalog lists datastore REST methods: per-method Datastore_* aliases and QuadroDatastoreMethodFn. Omitted when there are no such exposed methods. Runtime catalog.gen.json stores path as ["$catalog", "<methodName>"]/rest/$catalog/<methodName> when baseURL is /rest. | | catalog.gen.json | Catalog runtime spec (dataclass layouts, methods, relations) consumed by @quadrokit/client/runtime — keeps client.gen.ts tiny. | | client.gen.ts | Thin createClient(config) that wires QuadroHttp + buildQuadroClientFromCatalogSpec + catalog.gen.json. Config supports optional events (see Request lifecycle events). | | _quadroFns.gen.ts | Shared helpers: ResolveOverride, QuadroDataClassFn, QuadroEntityFn, QuadroEntityCollectionFn, QuadroDatastoreFn (used by *.gen.ts and optional override files). | | entities/<Class>.overrides.ts, datastore.overrides.ts | Optional stubs you edit to narrow method signatures (see Narrowing function signatures). Created if missing; preserved across regenerate when possible. | | meta.json | __NAME, sessionCookieName hint for 4D session cookies. |

Point your app imports at the generated folder, for example:

import { createClient } from './.quadrokit/generated/client.gen.js'
import type { Agency } from './.quadrokit/generated/types.gen.js'

Creating the client

import { createClient } from './.quadrokit/generated/client.gen.js'

const quadro = createClient({
  /** Base URL of the REST root, e.g. `https://host:port/rest` or `/rest` behind a dev proxy */
  baseURL: import.meta.env.VITE_4D_REST_URL ?? '/rest',
  /** Optional: swap `fetch` (e.g. tests, Node with undici) */
  fetchImpl: globalThis.fetch,
  /** Optional: default headers on every request */
  defaultHeaders: { Authorization: `Bearer ${token}` },
})

Optional events: new QuadroEventBus() (from @quadrokit/client/runtime) enables request lifecycle events on every call through the client.

At runtime, requests use credentials: 'include' so session cookies (e.g. 4DSID_<datastore>) are sent when your app and 4D share the same site or proxy. See meta.jsonsessionCookieName after generate.

JSON: 4D date strings

QuadroHttp.json() parses response bodies with a JSON reviver so common 4D date encodings become JavaScript Date values or null:

| String | Result | |--------|--------| | dd!mm!yyyy (e.g. 22!2!2026) | Local calendar Date | | !!yyyy-mm-dd!! (e.g. !!2026-02-22!!) | Local calendar Date | | 0!0!0 | null | | !!0000-00-00!! | null |

Other strings are unchanged. If you parse JSON yourself, use parseQuadroJson or quadroJsonReviver from @quadrokit/client/runtime.


Request lifecycle events

Pass a QuadroEventBus from @quadrokit/client/runtime into createClient({ …, events }). The runtime emits a loading → success or loading → error sequence per logical operation (lists, get, delete, class functions, datastore paths, login, etc.).

import { createClient } from './.quadrokit/generated/client.gen.js'
import { QuadroEventBus } from '@quadrokit/client/runtime'

const events = new QuadroEventBus()

const quadro = createClient({
  baseURL: '/rest',
  events,
})

const unsub = events.subscribe((e) => {
  if (e.type === 'loading') {
    console.debug(e.context.operation, e.path, e.method)
  }
  if (e.type === 'success') {
    console.debug(e.durationMs, e.body)
  }
  if (e.type === 'error') {
    console.warn(e.status, e.message, e.error)
  }
})

// later: unsub()

You can also subscribe on the HTTP instance: quadro._http.subscribe(listener) returns an unsubscribe function.

Event shapes (QuadroClientEvent)

| Field | loading | success | error | |-------|-----------|-----------|---------| | type | 'loading' | 'success' | 'error' | | operationId | Correlates one start → one outcome | same | same | | context | QuadroRequestContext: operation (QuadroOperation string), optional className, methodName, entityKey, attributes | same | same | | path | Request path | same | same | | method | HTTP method | — | — | | startedAt | Timestamp | — | — | | durationMs | — | Elapsed | Elapsed | | status | — | If HTTP response is raw Response | If thrown value has HTTP status (e.g. QuadroHttpError) | | body | — | Result: parsed JSON, rows, void, or a small summary for raw Response | — | | message / error | — | — | User-facing message + original error |

Operations (QuadroOperation)

Examples include: http.json, http.void, http.request, collection.list, collection.release, dataclass.get, dataclass.delete, function.dataclass, function.entity, function.entityCollection, datastore (catalog datastore methods emit context.methodName set to the catalog method name).

QuadroHttp helpers (advanced)

When events is set, json, void, and request accept an optional trailing QuadroRequestContext so emissions can be tagged. QuadroHttp also exposes:

  • rawRequest(path, init) — fetch without emitting (used inside multi-step flows).
  • runWithEvents(context, path, method, fn) — run an async function with the same loading/success/error envelope as the built-in methods.
  • events getter — the bus instance, if any.

RxJS (@quadrokit/client/rx)

Install rxjs in your app, then bridge the bus to an observable:

import { quadroEventsObservable } from '@quadrokit/client/rx'
import { filter } from 'rxjs/operators'

const sub = quadroEventsObservable(events)
  .pipe(filter((e) => e.type === 'error'))
  .subscribe((e) => console.warn(e.message))

rxjs is an optional peer dependency of @quadrokit/client; you only need it if you import @quadrokit/client/rx.


Dataclass API (generated)

For each exposed dataclass, the client exposes a namespace such as quadro.Agency, quadro.Reservation, etc.

all(options?) — full class as an async iterable “collection”

Walk entities with optional select, filter, orderby, paging, and 4D entity sets ($method=entityset) for efficient paging.

const col = quadro.Agency.all({
  select: ['ID', 'name', 'department.name'] as const,
  filter: 'name = "Paris"',
  page: 1,
  pageSize: 50,
  signal: ac.signal,
})

for await (const agency of col) {
  console.log(agency.name)
}

await col.release() // release server entity set when done (see 4D $method=release)

Important options (see CollectionOptions in @quadrokit/client/runtime):

| Option | Role | |--------|------| | select | $attributes: scalar or relation paths ('manager.name'). | | filter / orderby | OData-style list constraints. | | page / pageSize | Client-side paging over the list / entity set. | | maxItems | Stop iteration after N rows. | | signal | AbortController — abort in-flight requests when unmounting or changing filters. | | reuseEntitySet / onEntitySetReady | Reuse a cached entity-set URI across requests (see your app’s paging hooks). |

query(filter, options?)

Same shape as all, but with a filter string as the first argument (and optional params for :1, :2, … placeholders in the filter).

get(id, options?)

Load one entity by primary key; optional select for $attributes.

delete(id)

DELETE the entity.

Related entities on a row

Mapped rows get non-enumerable properties for related collections (e.g. agency.todayBookings) with a .list(options) that returns another collection handle for the child dataclass.


ORDA class functions (generated from catalog)

If your 4D project exposes methods in the REST catalog, the generator wires them by applyTo (see Calling class functions):

| Catalog applyTo | Where it appears in JS | REST shape (simplified) | |-------------------|-------------------------|-------------------------| | dataClass | quadro.Agency.agencyStats<R, A>(args, init?)R = result, A = args tuple (defaults: unknown, readonly unknown[]) | POST /rest/{Class}/{function} with JSON array body | | entityCollection | On all() / query() handles and on related { list, … } APIs | POST /rest/{Class}/{function} + optional selection, entitySet | | entity | On each mapped entity row | POST /rest/{Class}({key})/{function} |

  • args: tuple type A (second generic on dataclass functions; default readonly unknown[]) — 4D receives a JSON array of parameters (scalars, entities, entity selections per 4D rules).
  • init: optional { method: 'GET' \| 'POST', unwrapResult?, signal?, … } — GET uses ?$params=… for HTTP GET functions.
  • unwrapResult: when true (default), { "result": x } responses are unwrapped to x.

Entity-selection methods accept EntityCollectionMethodOptions:

  • selection: { filter?, orderby?, select?, page?, pageSize? }$filter, $orderby, $attributes, $skip, $top.
  • entitySet: UUID or path segment so the URL includes …/$entityset/{uuid}.

Example:

const stats = await quadro.Agency.agencyStats<MyStatsRow, [string, string]>([from, to])
// or: agencyStats<MyStatsRow>([from, to]) when you only need to fix the result type

const col = quadro.Reservation.all({ pageSize: 20 })
// After the handle exists, e.g. from a hook or manual create:
await col.getFirst?.([], { selection: { filter: 'ID > 0', pageSize: 1 } })

await reservation.cancel<{ ok: boolean }>([], { signal: ac.signal })

If the catalog does not list a method, it will not appear on the client — regenerate after changing 4D exposure.


Narrowing function signatures (override files)

Generated ORDA dataClass, entity, and entityCollection methods, plus datastore methods, default to loose generics (unknown / readonly unknown[] for results and argument tuples). To type return values and parameters in TypeScript, add entries to the optional override modules beside the codegen output. The generator wires each method type as ResolveOverride<YourOverrides, "methodName", DefaultFn> in *.gen.ts: if your interface defines that exact property name (same spelling as the catalog / JS API), your signature wins; otherwise the default Quadro*Fn stands.

Where the files live

Split typings (default): for each exposed dataclass, entities/<ClassName>.overrides.ts declares up to three interfaces:

| Interface | Catalog applyTo | Example call site | |-----------|-------------------|-------------------| | <ClassName>DataclassOverrides | dataClass | quadro.Car.findACar(...) | | <ClassName>EntityOverrides | entity | (await quadro.Car.get(id)).isAvailable(...) | | <ClassName>EntityCollectionOverrides | entityCollection | On all() / query() handles and related list APIs |

datastore.overrides.ts (next to types.gen.ts) maps top-level datastore methods on quadro by catalog method name.

Single types.gen.ts (--no-split-type-files): same idea, with ./<ClassName>.overrides.ts and ./datastore.overrides.ts next to types.gen.ts.

Stub files may re-export QuadroDataClassFn, QuadroEntityFn, QuadroEntityCollectionFn, QuadroDatastoreFn from _quadroFns.gen.ts; you can import type those helpers from _quadroFns.gen.js (or .ts) when you add properties.

Fn type parameters (from _quadroFns.gen.ts)

  • QuadroDataClassFn<R, A>R: resolved Promise result type; A: tuple of positional arguments (sent as the JSON array body to 4D).
  • QuadroEntityFn<R, A> — same shape for per-entity calls.
  • QuadroEntityCollectionFn<R, A> — same R / A; real calls also accept init with EntityCollectionMethodOptions (selection, entitySet, …).
  • QuadroDatastoreFn<B, R>B: body type; R: result type for quadro.<datastoreMethod>(body?).

Examples

Per-class overrides (entities/Car.overrides.ts in the split layout):

import type { QuadroDataClassFn, QuadroEntityFn } from '../_quadroFns.gen.js'
import type { Car } from './Car.gen.js'

export interface CarDataclassOverrides {
  /** Catalog method name must match exactly (e.g. findACar). */
  findACar: QuadroDataClassFn<Car[], [string, string]>
}

export interface CarEntityOverrides {
  isAvailable: QuadroEntityFn<boolean, readonly []>
}

export interface CarEntityCollectionOverrides {}

Datastore overrides (datastore.overrides.ts):

import type { QuadroDatastoreFn } from './_quadroFns.gen.js'

export interface DatastoreOverrides {
  testFn: QuadroDatastoreFn<{ foo: string }, { ok: true }>
}

Regenerate behavior

Run quadrokit-client generate after catalog changes. The tool rewrites *.gen.ts (and related generated files) but keeps your existing *.overrides.ts content when refreshing entities/ (and only creates stub override files if missing). Do not rely on editing *.gen.ts — put signature narrowing in override modules only.


Low-level runtime (advanced)

Import from @quadrokit/client/runtime when you need primitives without codegen:

| Export | Use | |--------|-----| | QuadroHttp | Thin wrapper: json(), void(), request() with base URL + default headers; optional events; rawRequest, runWithEvents, subscribe. | | QuadroEventBus, QuadroClientEvent, QuadroLoadingEvent, QuadroSuccessEvent, QuadroErrorEvent, QuadroRequestContext, QuadroOperation | Lifecycle event types and bus (see Request lifecycle events). | | createCollection | Build a CollectionHandle from a CollectionContext + options + row mapper. | | callDataClassFunction, callEntityFunction, callEntityCollectionFunction | Call ORDA functions by name and path (same REST rules as above). | | callDatastorePath, createClient’s rpc | Arbitrary segments under the REST root, e.g. datastore or singleton paths. | | buildListSearchParams, buildMethodSelectionQuery, … | Query-string helpers aligned with 4D. | | QuadroHttpError | Thrown on non-OK HTTP with status and body text. |

The generated createClient is the recommended surface; these are for tooling, tests, or custom wrappers.


Errors

Failed HTTP responses throw QuadroHttpError (status + response body). Handle network errors separately (fetch failures). When events is configured, failures also emit a QuadroErrorEvent with message, error, and optional status before the error propagates.


Monorepo development

From packages/client:

bun run typecheck
bun run build

Example generate from the fixture catalog (adjust file:// to an absolute path on your machine):

bun run src/cli.ts generate --url file:///ABS/path/to/assets/catalog.json --out ../../.quadrokit/generated-demo

Further reading