@incorta/sdk
v1.10.0
Published
Read Incorta schemas, tables, views, and columns on behalf of the signed-in user — OAuth 2.0 sessions from @incorta/auth, no personal access tokens.
Readme
@incorta/sdk
Read Incorta schemas, tables, views, and columns from Node.js, as the
signed-in user. Sessions come from
@incorta/auth — OAuth 2.0
against the authorization server built into Incorta — so this client has no
identity of its own and no personal access token to store.
The Python twin is incorta-sdk; the two speak the
same API, model the same distinctions, and share the same INCORTA_*
configuration.
npm install @incorta/sdk@incorta/auth comes with it as a dependency, pinned to the same version — the
two are always released together, so @incorta/[email protected] runs on
@incorta/[email protected].
Why it is scoped to a user
Every call carries a user's own Incorta access token, so Incorta filters the results: two people hitting the same endpoint of your app see two different catalogs. That is a property of the design, not a setting — there is no app-level identity to over-share from, and nothing to revoke separately when someone leaves.
It also means the scoped client, not the top-level one, is what you hold:
IncortaClient configuration + OAuth (build once, at startup)
└── forRequest() → IncortaUserClient (build per request)
├── schemas
└── tablesConfiguration
createIncortaClient() takes every @incorta/auth option, each falling back to
its environment variable:
| Option | Environment variable | Meaning |
| --- | --- | --- |
| incortaUrl | INCORTA_URL | Environment root including any context path (often /incorta), without /api/v2 |
| tenant | INCORTA_TENANT | Tenant name, e.g. default |
| clientId | INCORTA_CLIENT_ID | OAuth client id (see incorta-auth register) |
| clientSecret | INCORTA_CLIENT_SECRET | OAuth client secret |
| secret | INCORTA_AUTH_SECRET | Session-cookie encryption key (≥ 32 chars) |
| internalIncortaUrl | INCORTA_INTERNAL_URL | Optional split-horizon address for server-to-server calls |
Plus two of its own: timeoutMs (default 30000) and maxRetries (default
3, for 429/5xx and network errors only — client errors are never retried).
When the app already builds an IncortaAuth — the usual case, since it needs
one to serve logins — pass it in rather than letting this package create a
second:
import { createIncortaAuth } from "@incorta/auth";
import { createIncortaClient } from "@incorta/sdk";
const auth = createIncortaAuth({ appAccess: "catalog" });
const client = createIncortaClient({ auth });Usage
Express
import express from "express";
import { incortaAuth, requireAuth } from "@incorta/auth/express";
import { createIncortaClient } from "@incorta/sdk";
const client = createIncortaClient(); // once, at startup
const app = express();
// The client owns an auth instance — this serves /auth/* and attaches the
// session to req.incortaAuth.
app.use(incortaAuth(client.auth));
app.get("/api/schemas", requireAuth(client.auth), async (req, res) => {
const incorta = client.forSession(req.incortaAuth!); // per request, as this user
res.json(await incorta.schemas.physical());
});
app.get("/api/tables/:schema/:table", requireAuth(client.auth), async (req, res) => {
const incorta = client.forSession(req.incortaAuth!);
const object = await incorta.tables.get(req.params.schema, req.params.table);
res.json({
name: object.qualifiedName,
columns: object.columns.map((c) => ({ name: c.name, type: c.dataType })),
});
});TanStack Start / anything with web-standard Request
export const ServerRoute = createServerFileRoute().methods({
GET: async ({ request }) => {
const incorta = await client.forRequest(request);
return Response.json(await incorta.schemas.list());
},
});Given a session you already read (from a gate or a middleware), use
client.forSession(session) instead.
Surface
createIncortaClient(config) → IncortaClient
| Member | Purpose |
| --- | --- |
| auth | The underlying IncortaAuth — mount auth.handler / auth.gate |
| forRequest(request, opts?) | Scoped client for the user behind a Request |
| forSession(session, opts?) | Scoped client from a session you already read |
| forAccessToken(token, opts?) | Scoped client from a bare access token |
| info | baseUrl, tenant, timeoutMs, maxRetries — no secrets |
IncortaUserClient.schemas
| Method | Returns |
| --- | --- |
| list({ type, limit, offset, sortBy }) | SchemaInfo[] |
| listPage(...) | Page<SchemaInfo> — adds the server-side total |
| iterAll({ type, pageSize }) | AsyncGenerator, one page fetched at a time |
| physical() / business() | Shorthands for the type filter |
| get(name) | PhysicalSchema \| BusinessSchema, with contents |
| exists(name) | boolean |
IncortaUserClient.tables
| Method | Returns |
| --- | --- |
| get(schema, name) | Table \| View, columns populated |
| list(schema) / names(schema) | Every object, or just their names |
| columns(schema, name) | Column[] |
| tablesOnly(schema) / viewsOnly(schema) | Filtered by kind |
| exists(schema, name) | boolean |
Names are matched case-insensitively.
IncortaUserClient.data
Reads rows out of business views. Fields are addressed by their fully qualified
name, SCHEMA.VIEW.COLUMN.
| Method | Returns |
| --- | --- |
| query(measures, options?) | Promise<QueryResult> |
| iterRows(measures, options?) | Async generator of rows, one page at a time |
| csv(measures, options?) | Promise<string> — the CSV Incorta rendered |
| raw(body) | The decoded response for a body sent verbatim |
| buildBody(measures, options?) | The request body, without sending it |
const result = await incorta.data.query(
[{ field: "HR_BS.Employee_BS.SALARY", aggregation: "sum", label: "payroll" }],
{
rows: ["HR_BS.Employee_BS.JOB_TITLE"],
aggregate: true,
filters: [
{
type: "fieldKey",
fieldKey: "HR_BS.Employee_BS.JOB_TITLE",
op: "IN_LIST",
values: ["Accountant"],
},
],
sorting: [{ field: "HR_BS.Employee_BS.JOB_TITLE", dir: "desc" }],
},
);
result.headers; // ["JOB_TITLE", "payroll"]
result.records(); // [{ JOB_TITLE: "Accountant", payroll: "39600.0" }]
result.totalRows; // rows matching beyond this pageA bare string is shorthand for { field } in both measures and the dimension
lists. Cells always come back as strings — Incorta renders every value as text.
aggregate: false gives a flat extract; aggregate: true folds each measure
with its aggregation and groups by rows and columns.
Models
Plain data — no classes — so results survive structuredClone,
JSON.stringify, and a trip through a worker or an HTTP response. Where Python
uses isinstance, these carry a kind discriminant that narrows in TypeScript
and survives serialisation:
const schema = await incorta.schemas.get("OnlineStore");
if (schema.kind === "physical") {
for (const table of schema.tables) console.log(table.rowsCount);
} else {
for (const view of schema.views) console.log(view.sources);
}Every model keeps the untouched API record in .raw, so a field this package
does not model is still reachable.
Errors
Everything derives from IncortaError:
IncortaError
├── IncortaConfigError a setting is missing or malformed
├── IncortaAuthRequiredError no signed-in user on this request
├── IncortaSessionExpiredError the captured token aged out (thrown locally)
├── IncortaConnectionError environment unreachable
│ └── IncortaTimeoutError
├── IncortaApiError non-2xx, carrying .statusCode and .code
│ ├── AuthenticationError 401 — Incorta refused the token
│ ├── PermissionDeniedError 403 — this user lacks access
│ ├── NotFoundError 404
│ │ └── SchemaNotFoundError
│ └── IncortaServerError 5xx
└── TableNotFoundError detected client-side, lists what does existToken lifetime
forRequest refreshes the access token as it reads the session, so a client
built per request always starts fresh. A scoped client held past its token's
expiry throws IncortaSessionExpiredError before making a request, rather
than letting Incorta answer 401 — build one per request and the case never
arises.
Behaviour both SDKs share
These are the API quirks the SDKs exist to absorb, handled identically in TypeScript and Python.
schemaTypefails silently.?schemaType=TYPOreturns HTTP 200 with business schemas, and so does omitting the parameter. A typo would hand you plausible but wrong data, so both clients validate the value locally and always send it explicitly.- Physical and business schemas return disjoint keys. A physical schema
carries
tablesDetails; a business schema carriesviewsDetails. The other key is absent entirely, not empty. Both clients return a different type for each rather than one half-null shape. - The API misspells its own value as
BUSSINESS_VIEW(three S's). Both clients round-trip that spelling and accept the corrected one, so nothing breaks whichever way Incorta resolves it. - There is no per-table endpoint. Fetching one table means fetching its
whole schema, so prefer
schemas.get(name)once over Ntables.getcalls. aggregatedefaults to true when omitted. A flat extract written without it returns zero rows with HTTP 200, reporting string columns asdouble. Both clients always send the flag explicitly.- Aggregate queries ignore the top-level
sortinglist. Sorting is read only from inside a dimension. Both clients route each sort onto the dimension it names, and reject a sort matching none rather than letting it vanish. format: "csv"cannot be unstringified. Asking for both returns the header line alone, with HTTP 200. Both clients pick the encoding themselves.nullValueAs: "DASH"is documented but rejected with HTTP 400. Both clients omit it from the accepted values and say why.- The query endpoint uses a different error envelope,
{"errorMessages": [{"message": "INC_..."}]}, and answers some 400s in plain text rather than JSON. Both clients parse all three shapes onto the same error object. - Errors carry a stable
INC_code inside{"message": "INC_09030108: ..."}. Both clients parse it onto the error object separately from the prose. - Tokens never appear in logs, serialised output, or a client's public surface.
Development
From the repository root (a pnpm workspace — this package resolves
@incorta/auth from packages/auth via workspace:^, so it always builds
against the auth code in this commit):
pnpm install
pnpm build && pnpm typecheck && pnpm testReleased off the same v{version} tag as @incorta/auth, at the same version;
the publish pipeline rewrites the workspace:^ dependency to that exact
version. See the root README.
