@smartretraining/rex-effect
v0.2.0
Published
Effect-native SDK for the Rex Software real-estate API, generated from Rex's describe/describeModel introspection.
Readme
@smartretraining/rex-effect
Effect-native SDK for the Rex Software real-estate API.
Rex ships no static OpenAPI document — it self-describes through introspection
endpoints. This package scrapes that introspection, transforms it into a
standard OpenAPI 3.1 document, and generates Effect operations from it with the
same generator used by the other @distilled.cloud/* SDKs.
Installation
npm install @smartretraining/rex-effect effectQuick Start
import { Effect, Layer } from "effect";
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
import { CredentialsFromEnv } from "@smartretraining/rex-effect";
import { ListingsSearch } from "@smartretraining/rex-effect";
const RexLive = Layer.merge(FetchHttpClient.layer, CredentialsFromEnv);
const program = Effect.gen(function* () {
const page = yield* ListingsSearch({ limit: 10 });
return page.rows ?? [];
});
const rows = await Effect.runPromise(program.pipe(Effect.provide(RexLive)));Every operation is a function Op(input) => Effect<Output, Error, ...>. The
Rex { result, error, correlation } envelope is unwrapped automatically by the
client — generated output schemas describe just the inner result payload.
Single-import, tree-shakeable client
For one import that exposes every operation, use a namespace import. Bundlers statically analyse namespace member access, so only the operations you actually call are kept:
import * as rex from "@smartretraining/rex-effect/Operations";
rex.ListingsSearch({ limit: 10 });
rex.PropertiesCreate({ /* ... */ });import * as rex from "@smartretraining/rex-effect" works too — same operations, plus
Category, Retry, the error types, and credentials helpers.
Configuration
Rex uses session-token auth, not a static API key. Provide credentials one of three ways:
# Option A — you already hold a session token
REX_API_TOKEN=your-session-token
# Option B — email + password; the SDK performs the login exchange
[email protected]
REX_PASSWORD=your-password
# Optional — override the API origin (defaults to https://api.rexsoftware.com)
REX_API_BASE_URL=https://api.rexsoftware.comCredentialsFromEnv— reads the variables above (prefersREX_API_TOKEN).CredentialsFromToken(token, { apiBaseUrl? })— build a layer from a token.- Construct the
Credentialsservice directly.
Receiving webhooks
The generated AdminWebhooks* operations manage webhook subscriptions
(create, update, search, markAsHealthy, etc.). Decoding the inbound HTTP
POST body Rex sends to your callback URL is a separate concern — Rex's
introspection has no schema for it, so the delivery payload is hand-modelled
in @smartretraining/rex-effect/Webhooks.
import { Effect } from "effect";
import { decodeWebhookDelivery } from "@smartretraining/rex-effect";
// inside your HTTP handler:
const delivery = yield* decodeWebhookDelivery(await req.json());
for (const event of delivery.data) {
if (event.payload.format === "v1_full_change_detail") {
event.payload.data.post; // current record state
event.payload.data.pre; // previous state, or null on "create" events
} else {
// v1_context_only — fetch the record yourself
const { service, record_id } = event.payload.context;
}
}decodeWebhookDelivery fails with RexParseError if the body doesn't match.
The payload is a discriminated union on payload.format
(v1_full_change_detail vs v1_context_only), so narrowing on it gives you
fully-typed access to either shape.
See docs/admin-webhooks.md for a full
Effect-based sync handler on Cloudflare Workers — subscribe, a fast-ack
fetch delivery handler (ctx.waitUntil), a scheduled nightly e-tag
backstop (Cron Trigger), and unhealthy-webhook recovery — following Rex's
recommended approach.
Regenerating the SDK
Operations in src/operations/ are generated. To regenerate or extend them:
1. Scrape Rex introspection
bun run specs:scrapeRequires REX_API_TOKEN, or REX_EMAIL + REX_PASSWORD, in the environment.
Writes specs/rex/{Service}.describe.json, specs/rex/{Service}.describeModel.json,
and specs/rex/_catalog.json.
2. Generate
bun run generateThis chains three steps:
scripts/build-openapi.ts— transforms the scrapedspecs/rex/*.describe.jsonfiles into a single OpenAPI 3.1 document atspecs/openapi.generated.json.scripts/generate.ts— runs the shared@distilled.cloud/coreOpenAPI generator over that document, emitting one module per Rex method intosrc/operations/and rewriting theindex.tsbarrel.oxlint --fix+oxfmt— lint and format the generated code.
Re-running bun run generate with unchanged specs is idempotent.
Adding more Rex services
Rex has ~272 services. This package generates Listings, Properties, and AdminWebhooks. To add another:
- Append the service name to the
SERVICESarray inscripts/scrape-specs.ts. - Run
bun run specs:scrapethenbun run generate.
build-openapi.ts globs every specs/rex/*.describe.json file, so new services
flow through automatically. Note: removing a service does not delete its stale
src/operations/*.ts files — prune those by hand.
Known limitations
- Output schemas are inferred from the example records embedded in Rex's
describepayloads. Methods that ship no example get a permissive schema. Every inferred field is treated as nullable, since one example can't prove a field is nevernull. search.criteriais an opaqueobjectin Rex'sopenapiblock. The real query surface lives in{Service}.describeModel.json(searchable_fields) and is not yet folded into the generated input schemas.- Rex returns logical failures as HTTP 200 with a non-null envelope
error. These surface as aRexApiErrordefect rather than a typed error channel — see the note insrc/client.ts.
License
MIT
