@incorta/sdk 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/dist/index.cjs +1067 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +842 -0
- package/dist/index.d.ts +842 -0
- package/dist/index.js +1012 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Incorta
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# @incorta/sdk
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@incorta/sdk)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
6
|
+
Read Incorta **schemas, tables, views, and columns** from Node.js, as the
|
|
7
|
+
**signed-in user**. Sessions come from
|
|
8
|
+
[`@incorta/auth`](https://www.npmjs.com/package/@incorta/auth) — OAuth 2.0
|
|
9
|
+
against the authorization server built into Incorta — so this client has no
|
|
10
|
+
identity of its own and no personal access token to store.
|
|
11
|
+
|
|
12
|
+
The Python twin is [`incorta-sdk`](../sdk-python/README.md); the two speak the
|
|
13
|
+
same API, model the same distinctions, and share the same `INCORTA_*`
|
|
14
|
+
configuration.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @incorta/sdk
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Why it is scoped to a user
|
|
21
|
+
|
|
22
|
+
Every call carries a user's own Incorta access token, so Incorta filters the
|
|
23
|
+
results: two people hitting the same endpoint of your app see two different
|
|
24
|
+
catalogs. That is a property of the design, not a setting — there is no
|
|
25
|
+
app-level identity to over-share from, and nothing to revoke separately when
|
|
26
|
+
someone leaves.
|
|
27
|
+
|
|
28
|
+
It also means the *scoped* client, not the top-level one, is what you hold:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
IncortaClient configuration + OAuth (build once, at startup)
|
|
32
|
+
└── forRequest() → IncortaUserClient (build per request)
|
|
33
|
+
├── schemas
|
|
34
|
+
└── tables
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
`createIncortaClient()` takes every `@incorta/auth` option, each falling back to
|
|
40
|
+
its environment variable:
|
|
41
|
+
|
|
42
|
+
| Option | Environment variable | Meaning |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `incortaUrl` | `INCORTA_URL` | Environment root **including** any context path (often `/incorta`), **without** `/api/v2` |
|
|
45
|
+
| `tenant` | `INCORTA_TENANT` | Tenant name, e.g. `default` |
|
|
46
|
+
| `clientId` | `INCORTA_CLIENT_ID` | OAuth client id (see `incorta-auth register`) |
|
|
47
|
+
| `clientSecret` | `INCORTA_CLIENT_SECRET` | OAuth client secret |
|
|
48
|
+
| `secret` | `INCORTA_AUTH_SECRET` | Session-cookie encryption key (≥ 32 chars) |
|
|
49
|
+
| `internalIncortaUrl` | `INCORTA_INTERNAL_URL` | Optional split-horizon address for server-to-server calls |
|
|
50
|
+
|
|
51
|
+
Plus two of its own: `timeoutMs` (default `30000`) and `maxRetries` (default
|
|
52
|
+
`3`, for 429/5xx and network errors only — client errors are never retried).
|
|
53
|
+
|
|
54
|
+
When the app already builds an `IncortaAuth` — the usual case, since it needs
|
|
55
|
+
one to serve logins — pass it in rather than letting this package create a
|
|
56
|
+
second:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { createIncortaAuth } from "@incorta/auth";
|
|
60
|
+
import { createIncortaClient } from "@incorta/sdk";
|
|
61
|
+
|
|
62
|
+
const auth = createIncortaAuth({ appAccess: "catalog" });
|
|
63
|
+
const client = createIncortaClient({ auth });
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
### Express
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import express from "express";
|
|
72
|
+
import { incortaAuth, requireAuth } from "@incorta/auth/express";
|
|
73
|
+
import { createIncortaClient } from "@incorta/sdk";
|
|
74
|
+
|
|
75
|
+
const client = createIncortaClient(); // once, at startup
|
|
76
|
+
const app = express();
|
|
77
|
+
|
|
78
|
+
// The client owns an auth instance — this serves /auth/* and attaches the
|
|
79
|
+
// session to req.incortaAuth.
|
|
80
|
+
app.use(incortaAuth(client.auth));
|
|
81
|
+
|
|
82
|
+
app.get("/api/schemas", requireAuth(client.auth), async (req, res) => {
|
|
83
|
+
const incorta = client.forSession(req.incortaAuth!); // per request, as this user
|
|
84
|
+
res.json(await incorta.schemas.physical());
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
app.get("/api/tables/:schema/:table", requireAuth(client.auth), async (req, res) => {
|
|
88
|
+
const incorta = client.forSession(req.incortaAuth!);
|
|
89
|
+
const object = await incorta.tables.get(req.params.schema, req.params.table);
|
|
90
|
+
res.json({
|
|
91
|
+
name: object.qualifiedName,
|
|
92
|
+
columns: object.columns.map((c) => ({ name: c.name, type: c.dataType })),
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### TanStack Start / anything with web-standard `Request`
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
export const ServerRoute = createServerFileRoute().methods({
|
|
101
|
+
GET: async ({ request }) => {
|
|
102
|
+
const incorta = await client.forRequest(request);
|
|
103
|
+
return Response.json(await incorta.schemas.list());
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Given a session you already read (from a gate or a middleware), use
|
|
109
|
+
`client.forSession(session)` instead.
|
|
110
|
+
|
|
111
|
+
## Surface
|
|
112
|
+
|
|
113
|
+
### `createIncortaClient(config)` → `IncortaClient`
|
|
114
|
+
|
|
115
|
+
| Member | Purpose |
|
|
116
|
+
| --- | --- |
|
|
117
|
+
| `auth` | The underlying `IncortaAuth` — mount `auth.handler` / `auth.gate` |
|
|
118
|
+
| `forRequest(request, opts?)` | Scoped client for the user behind a `Request` |
|
|
119
|
+
| `forSession(session, opts?)` | Scoped client from a session you already read |
|
|
120
|
+
| `forAccessToken(token, opts?)` | Scoped client from a bare access token |
|
|
121
|
+
| `info` | `baseUrl`, `tenant`, `timeoutMs`, `maxRetries` — no secrets |
|
|
122
|
+
|
|
123
|
+
### `IncortaUserClient.schemas`
|
|
124
|
+
|
|
125
|
+
| Method | Returns |
|
|
126
|
+
| --- | --- |
|
|
127
|
+
| `list({ type, limit, offset, sortBy })` | `SchemaInfo[]` |
|
|
128
|
+
| `listPage(...)` | `Page<SchemaInfo>` — adds the server-side `total` |
|
|
129
|
+
| `iterAll({ type, pageSize })` | `AsyncGenerator`, one page fetched at a time |
|
|
130
|
+
| `physical()` / `business()` | Shorthands for the type filter |
|
|
131
|
+
| `get(name)` | `PhysicalSchema \| BusinessSchema`, with contents |
|
|
132
|
+
| `exists(name)` | `boolean` |
|
|
133
|
+
|
|
134
|
+
### `IncortaUserClient.tables`
|
|
135
|
+
|
|
136
|
+
| Method | Returns |
|
|
137
|
+
| --- | --- |
|
|
138
|
+
| `get(schema, name)` | `Table \| View`, columns populated |
|
|
139
|
+
| `list(schema)` / `names(schema)` | Every object, or just their names |
|
|
140
|
+
| `columns(schema, name)` | `Column[]` |
|
|
141
|
+
| `tablesOnly(schema)` / `viewsOnly(schema)` | Filtered by kind |
|
|
142
|
+
| `exists(schema, name)` | `boolean` |
|
|
143
|
+
|
|
144
|
+
Names are matched case-insensitively.
|
|
145
|
+
|
|
146
|
+
### `IncortaUserClient.data`
|
|
147
|
+
|
|
148
|
+
Reads rows out of business views. Fields are addressed by their fully qualified
|
|
149
|
+
name, `SCHEMA.VIEW.COLUMN`.
|
|
150
|
+
|
|
151
|
+
| Method | Returns |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| `query(measures, options?)` | `Promise<QueryResult>` |
|
|
154
|
+
| `iterRows(measures, options?)` | Async generator of rows, one page at a time |
|
|
155
|
+
| `csv(measures, options?)` | `Promise<string>` — the CSV Incorta rendered |
|
|
156
|
+
| `raw(body)` | The decoded response for a body sent verbatim |
|
|
157
|
+
| `buildBody(measures, options?)` | The request body, without sending it |
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
const result = await incorta.data.query(
|
|
161
|
+
[{ field: "HR_BS.Employee_BS.SALARY", aggregation: "sum", label: "payroll" }],
|
|
162
|
+
{
|
|
163
|
+
rows: ["HR_BS.Employee_BS.JOB_TITLE"],
|
|
164
|
+
aggregate: true,
|
|
165
|
+
filters: [
|
|
166
|
+
{
|
|
167
|
+
type: "fieldKey",
|
|
168
|
+
fieldKey: "HR_BS.Employee_BS.JOB_TITLE",
|
|
169
|
+
op: "IN_LIST",
|
|
170
|
+
values: ["Accountant"],
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
sorting: [{ field: "HR_BS.Employee_BS.JOB_TITLE", dir: "desc" }],
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
result.headers; // ["JOB_TITLE", "payroll"]
|
|
177
|
+
result.records(); // [{ JOB_TITLE: "Accountant", payroll: "39600.0" }]
|
|
178
|
+
result.totalRows; // rows matching beyond this page
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
A bare string is shorthand for `{ field }` in both `measures` and the dimension
|
|
182
|
+
lists. Cells always come back as strings — Incorta renders every value as text.
|
|
183
|
+
|
|
184
|
+
`aggregate: false` gives a flat extract; `aggregate: true` folds each measure
|
|
185
|
+
with its `aggregation` and groups by `rows` and `columns`.
|
|
186
|
+
|
|
187
|
+
### Models
|
|
188
|
+
|
|
189
|
+
Plain data — no classes — so results survive `structuredClone`,
|
|
190
|
+
`JSON.stringify`, and a trip through a worker or an HTTP response. Where Python
|
|
191
|
+
uses `isinstance`, these carry a `kind` discriminant that narrows in TypeScript
|
|
192
|
+
**and** survives serialisation:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
const schema = await incorta.schemas.get("OnlineStore");
|
|
196
|
+
if (schema.kind === "physical") {
|
|
197
|
+
for (const table of schema.tables) console.log(table.rowsCount);
|
|
198
|
+
} else {
|
|
199
|
+
for (const view of schema.views) console.log(view.sources);
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Every model keeps the untouched API record in `.raw`, so a field this package
|
|
204
|
+
does not model is still reachable.
|
|
205
|
+
|
|
206
|
+
### Errors
|
|
207
|
+
|
|
208
|
+
Everything derives from `IncortaError`:
|
|
209
|
+
|
|
210
|
+
```text
|
|
211
|
+
IncortaError
|
|
212
|
+
├── IncortaConfigError a setting is missing or malformed
|
|
213
|
+
├── IncortaAuthRequiredError no signed-in user on this request
|
|
214
|
+
├── IncortaSessionExpiredError the captured token aged out (thrown locally)
|
|
215
|
+
├── IncortaConnectionError environment unreachable
|
|
216
|
+
│ └── IncortaTimeoutError
|
|
217
|
+
├── IncortaApiError non-2xx, carrying .statusCode and .code
|
|
218
|
+
│ ├── AuthenticationError 401 — Incorta refused the token
|
|
219
|
+
│ ├── PermissionDeniedError 403 — this user lacks access
|
|
220
|
+
│ ├── NotFoundError 404
|
|
221
|
+
│ │ └── SchemaNotFoundError
|
|
222
|
+
│ └── IncortaServerError 5xx
|
|
223
|
+
└── TableNotFoundError detected client-side, lists what does exist
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Token lifetime
|
|
227
|
+
|
|
228
|
+
`forRequest` refreshes the access token as it reads the session, so a client
|
|
229
|
+
built per request always starts fresh. A scoped client held past its token's
|
|
230
|
+
expiry throws `IncortaSessionExpiredError` **before** making a request, rather
|
|
231
|
+
than letting Incorta answer 401 — build one per request and the case never
|
|
232
|
+
arises.
|
|
233
|
+
|
|
234
|
+
## Behaviour both SDKs share
|
|
235
|
+
|
|
236
|
+
These are the API quirks the SDKs exist to absorb, handled identically in
|
|
237
|
+
TypeScript and Python.
|
|
238
|
+
|
|
239
|
+
- **`schemaType` fails silently.** `?schemaType=TYPO` returns HTTP 200 with
|
|
240
|
+
*business* schemas, and so does omitting the parameter. A typo would hand you
|
|
241
|
+
plausible but wrong data, so both clients validate the value locally and
|
|
242
|
+
always send it explicitly.
|
|
243
|
+
- **Physical and business schemas return disjoint keys.** A physical schema
|
|
244
|
+
carries `tablesDetails`; a business schema carries `viewsDetails`. The other
|
|
245
|
+
key is absent entirely, not empty. Both clients return a different type for
|
|
246
|
+
each rather than one half-null shape.
|
|
247
|
+
- **The API misspells its own value** as `BUSSINESS_VIEW` (three S's). Both
|
|
248
|
+
clients round-trip that spelling and accept the corrected one, so nothing
|
|
249
|
+
breaks whichever way Incorta resolves it.
|
|
250
|
+
- **There is no per-table endpoint.** Fetching one table means fetching its
|
|
251
|
+
whole schema, so prefer `schemas.get(name)` once over N `tables.get` calls.
|
|
252
|
+
- **`aggregate` defaults to *true* when omitted.** A flat extract written
|
|
253
|
+
without it returns **zero rows with HTTP 200**, reporting string columns as
|
|
254
|
+
`double`. Both clients always send the flag explicitly.
|
|
255
|
+
- **Aggregate queries ignore the top-level `sorting` list.** Sorting is read
|
|
256
|
+
only from inside a dimension. Both clients route each sort onto the dimension
|
|
257
|
+
it names, and reject a sort matching none rather than letting it vanish.
|
|
258
|
+
- **`format: "csv"` cannot be unstringified.** Asking for both returns the
|
|
259
|
+
header line alone, with HTTP 200. Both clients pick the encoding themselves.
|
|
260
|
+
- **`nullValueAs: "DASH"` is documented but rejected** with HTTP 400. Both
|
|
261
|
+
clients omit it from the accepted values and say why.
|
|
262
|
+
- **The query endpoint uses a different error envelope**, `{"errorMessages":
|
|
263
|
+
[{"message": "INC_..."}]}`, and answers some 400s in plain text rather than
|
|
264
|
+
JSON. Both clients parse all three shapes onto the same error object.
|
|
265
|
+
- **Errors carry a stable `INC_` code** inside `{"message": "INC_09030108: ..."}`.
|
|
266
|
+
Both clients parse it onto the error object separately from the prose.
|
|
267
|
+
- **Tokens never appear** in logs, serialised output, or a client's public
|
|
268
|
+
surface.
|
|
269
|
+
|
|
270
|
+
## Development
|
|
271
|
+
|
|
272
|
+
From the repository root (a pnpm workspace — this package resolves
|
|
273
|
+
`@incorta/auth` from `packages/auth` via `workspace:^`, so it always builds
|
|
274
|
+
against the auth code in this commit):
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
pnpm install
|
|
278
|
+
pnpm build && pnpm typecheck && pnpm test
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Released off the same `v{version}` tag as `@incorta/auth`, at the same version;
|
|
282
|
+
the publish pipeline rewrites the `workspace:^` dependency to that exact
|
|
283
|
+
version. See the [root README](../../README.md#releases).
|