@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/dist/index.js
ADDED
|
@@ -0,0 +1,1012 @@
|
|
|
1
|
+
// src/core/config.ts
|
|
2
|
+
import { createIncortaAuth } from "@incorta/auth";
|
|
3
|
+
|
|
4
|
+
// src/core/errors.ts
|
|
5
|
+
var CODE_PATTERN = /^\s*(INC_\d+)\s*:\s*([\s\S]*)$/;
|
|
6
|
+
var BODY_LIMIT = 2e3;
|
|
7
|
+
var IncortaError = class extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(`[incorta-sdk] ${message}`);
|
|
10
|
+
this.name = "IncortaError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var IncortaConfigError = class extends IncortaError {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "IncortaConfigError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var IncortaAuthRequiredError = class extends IncortaError {
|
|
20
|
+
constructor(message = "No Incorta session on this request. Sign the user in first \u2014 mount `client.auth.handler` and redirect to its /login route, or wrap the app in `client.auth.gate`.") {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "IncortaAuthRequiredError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var IncortaSessionExpiredError = class extends IncortaError {
|
|
26
|
+
constructor(expiredAt) {
|
|
27
|
+
super(
|
|
28
|
+
`The Incorta access token on this client expired at ${new Date(expiredAt).toISOString()}. Re-read the session (client.forRequest(request)) to get a refreshed token.`
|
|
29
|
+
);
|
|
30
|
+
this.expiredAt = expiredAt;
|
|
31
|
+
this.name = "IncortaSessionExpiredError";
|
|
32
|
+
}
|
|
33
|
+
expiredAt;
|
|
34
|
+
};
|
|
35
|
+
var IncortaConnectionError = class extends IncortaError {
|
|
36
|
+
constructor(message, cause) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.cause = cause;
|
|
39
|
+
this.name = "IncortaConnectionError";
|
|
40
|
+
}
|
|
41
|
+
cause;
|
|
42
|
+
};
|
|
43
|
+
var IncortaTimeoutError = class extends IncortaConnectionError {
|
|
44
|
+
constructor(message) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = "IncortaTimeoutError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var IncortaApiError = class extends IncortaError {
|
|
50
|
+
/** HTTP status code of the response. */
|
|
51
|
+
statusCode;
|
|
52
|
+
/** Incorta error code such as `"INC_09030108"`, when the response carried one. */
|
|
53
|
+
code;
|
|
54
|
+
/** Human-readable message with the `INC_` prefix stripped. */
|
|
55
|
+
detail;
|
|
56
|
+
/** Raw response text, truncated for readability. */
|
|
57
|
+
responseBody;
|
|
58
|
+
/** The request URL. Never carries credentials — the token travels in a header. */
|
|
59
|
+
url;
|
|
60
|
+
constructor(statusCode, detail, options = {}) {
|
|
61
|
+
const prefix = options.code ? `[${options.code}] ` : "";
|
|
62
|
+
super(`HTTP ${statusCode}: ${prefix}${detail}`);
|
|
63
|
+
this.name = "IncortaApiError";
|
|
64
|
+
this.statusCode = statusCode;
|
|
65
|
+
this.code = options.code;
|
|
66
|
+
this.detail = detail;
|
|
67
|
+
this.responseBody = (options.responseBody ?? "").slice(0, BODY_LIMIT);
|
|
68
|
+
this.url = options.url;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var AuthenticationError = class extends IncortaApiError {
|
|
72
|
+
constructor(...args) {
|
|
73
|
+
super(...args);
|
|
74
|
+
this.name = "AuthenticationError";
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
var PermissionDeniedError = class extends IncortaApiError {
|
|
78
|
+
constructor(...args) {
|
|
79
|
+
super(...args);
|
|
80
|
+
this.name = "PermissionDeniedError";
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var NotFoundError = class extends IncortaApiError {
|
|
84
|
+
constructor(...args) {
|
|
85
|
+
super(...args);
|
|
86
|
+
this.name = "NotFoundError";
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
var SchemaNotFoundError = class extends NotFoundError {
|
|
90
|
+
constructor(...args) {
|
|
91
|
+
super(...args);
|
|
92
|
+
this.name = "SchemaNotFoundError";
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
var IncortaServerError = class extends IncortaApiError {
|
|
96
|
+
constructor(...args) {
|
|
97
|
+
super(...args);
|
|
98
|
+
this.name = "IncortaServerError";
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
var TableNotFoundError = class extends IncortaError {
|
|
102
|
+
constructor(schemaName, tableName, available = []) {
|
|
103
|
+
const preview = [...available].sort().slice(0, 10).join(", ");
|
|
104
|
+
const suffix = available.length > 10 ? ", ..." : "";
|
|
105
|
+
super(
|
|
106
|
+
`Table or view "${tableName}" not found in schema "${schemaName}".` + (preview ? ` Available: ${preview}${suffix}` : "")
|
|
107
|
+
);
|
|
108
|
+
this.schemaName = schemaName;
|
|
109
|
+
this.tableName = tableName;
|
|
110
|
+
this.available = available;
|
|
111
|
+
this.name = "TableNotFoundError";
|
|
112
|
+
}
|
|
113
|
+
schemaName;
|
|
114
|
+
tableName;
|
|
115
|
+
available;
|
|
116
|
+
};
|
|
117
|
+
var STATUS_MAP = {
|
|
118
|
+
401: AuthenticationError,
|
|
119
|
+
403: PermissionDeniedError,
|
|
120
|
+
404: NotFoundError
|
|
121
|
+
};
|
|
122
|
+
function apiErrorFromResponse(statusCode, body, options = {}) {
|
|
123
|
+
let detail = "";
|
|
124
|
+
const payload = options.payload;
|
|
125
|
+
if (payload && typeof payload === "object") {
|
|
126
|
+
const record = payload;
|
|
127
|
+
const candidate = record.message ?? record.error;
|
|
128
|
+
if (typeof candidate === "string") {
|
|
129
|
+
detail = candidate;
|
|
130
|
+
} else if (Array.isArray(record.errorMessages)) {
|
|
131
|
+
detail = record.errorMessages.map(
|
|
132
|
+
(item) => item && typeof item === "object" ? item.message : void 0
|
|
133
|
+
).filter((message) => typeof message === "string").join("; ");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (!detail) {
|
|
137
|
+
detail = body.trim() || `Request failed with status ${statusCode}`;
|
|
138
|
+
}
|
|
139
|
+
let code;
|
|
140
|
+
const match = CODE_PATTERN.exec(detail);
|
|
141
|
+
if (match?.[1]) {
|
|
142
|
+
code = match[1];
|
|
143
|
+
detail = (match[2] ?? "").trim();
|
|
144
|
+
}
|
|
145
|
+
const ErrorClass = STATUS_MAP[statusCode] ?? (statusCode >= 500 ? IncortaServerError : IncortaApiError);
|
|
146
|
+
return new ErrorClass(statusCode, detail, { code, responseBody: body, url: options.url });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/core/config.ts
|
|
150
|
+
function stripTrailingSlash(url) {
|
|
151
|
+
return url.replace(/\/+$/, "");
|
|
152
|
+
}
|
|
153
|
+
function resolveConfig(input = {}) {
|
|
154
|
+
const { auth: provided, timeoutMs, maxRetries, ...authOptions } = input;
|
|
155
|
+
const timeout = timeoutMs ?? 3e4;
|
|
156
|
+
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
157
|
+
throw new IncortaConfigError("timeoutMs must be a positive number of milliseconds.");
|
|
158
|
+
}
|
|
159
|
+
const retries = maxRetries ?? 3;
|
|
160
|
+
if (!Number.isInteger(retries) || retries < 0) {
|
|
161
|
+
throw new IncortaConfigError("maxRetries must be an integer >= 0.");
|
|
162
|
+
}
|
|
163
|
+
let auth;
|
|
164
|
+
try {
|
|
165
|
+
auth = provided ?? createIncortaAuth(authOptions);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
throw new IncortaConfigError(
|
|
168
|
+
`Could not configure OAuth: ${error instanceof Error ? error.message : String(error)}`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const baseUrl = stripTrailingSlash(auth.config.internalIncortaUrl ?? auth.config.incortaUrl);
|
|
172
|
+
const tenant = auth.config.tenant;
|
|
173
|
+
return {
|
|
174
|
+
auth,
|
|
175
|
+
baseUrl,
|
|
176
|
+
tenant,
|
|
177
|
+
apiRoot: `${baseUrl}/api/v2/${tenant}`,
|
|
178
|
+
timeoutMs: timeout,
|
|
179
|
+
maxRetries: retries,
|
|
180
|
+
// The same fetch the auth SDK uses for its own server-to-server calls, so a
|
|
181
|
+
// proxy agent or test double installed there covers metadata calls too.
|
|
182
|
+
fetch: auth.config.fetch
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/core/enums.ts
|
|
187
|
+
var SCHEMA_TYPES = ["ALL", "PHYSICAL", "BUSINESS"];
|
|
188
|
+
var SORT_ORDERS = ["NAME_ASC", "NAME_DESC"];
|
|
189
|
+
function parseObjectType(raw) {
|
|
190
|
+
if (!raw) return "UNKNOWN";
|
|
191
|
+
const normalised = raw.trim().toUpperCase();
|
|
192
|
+
if (normalised === "TABLE") return "table";
|
|
193
|
+
if (normalised === "BUSSINESS_VIEW" || normalised === "BUSINESS_VIEW") {
|
|
194
|
+
return "BUSSINESS_VIEW";
|
|
195
|
+
}
|
|
196
|
+
return "UNKNOWN";
|
|
197
|
+
}
|
|
198
|
+
var QUERY_FORMATS = ["json", "csv"];
|
|
199
|
+
var FILTER_OPS = ["=", "!=", ">", ">=", "<", "<=", "BETWEEN", "IN_LIST"];
|
|
200
|
+
var AGGREGATIONS = [
|
|
201
|
+
"sum",
|
|
202
|
+
"count",
|
|
203
|
+
"distinct",
|
|
204
|
+
"median",
|
|
205
|
+
"average",
|
|
206
|
+
"min",
|
|
207
|
+
"max"
|
|
208
|
+
];
|
|
209
|
+
var NULL_VALUE_AS = ["NULL", "EMPTY"];
|
|
210
|
+
var SORT_DIRECTIONS = ["asc", "desc"];
|
|
211
|
+
var COLUMN_FUNCTIONS = ["key", "dimension", "measure", "attribute", "unknown"];
|
|
212
|
+
function parseColumnFunction(raw) {
|
|
213
|
+
if (!raw) return "unknown";
|
|
214
|
+
const normalised = raw.trim().toLowerCase();
|
|
215
|
+
return COLUMN_FUNCTIONS.includes(normalised) ? normalised : "unknown";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/core/http.ts
|
|
219
|
+
var RETRY_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
220
|
+
var BACKOFF_BASE_MS = 500;
|
|
221
|
+
var USER_AGENT = "incorta-sdk-ts";
|
|
222
|
+
function urlFor(config, ...segments) {
|
|
223
|
+
const encoded = segments.map((segment) => encodeURIComponent(segment)).join("/");
|
|
224
|
+
return `${config.apiRoot}/${encoded}`;
|
|
225
|
+
}
|
|
226
|
+
function sleep(ms) {
|
|
227
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
228
|
+
}
|
|
229
|
+
function retryAfterMs(response) {
|
|
230
|
+
const header = response.headers.get("retry-after");
|
|
231
|
+
if (!header) return null;
|
|
232
|
+
const seconds = Number(header);
|
|
233
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
234
|
+
const date = Date.parse(header);
|
|
235
|
+
return Number.isNaN(date) ? null : Math.max(0, date - Date.now());
|
|
236
|
+
}
|
|
237
|
+
function isTimeout(error) {
|
|
238
|
+
return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
239
|
+
}
|
|
240
|
+
async function postJson(context, url, options = {}) {
|
|
241
|
+
const { config } = context;
|
|
242
|
+
const target = options.params ? `${url}?${new URLSearchParams(options.params).toString()}` : url;
|
|
243
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
244
|
+
const token = await context.getToken();
|
|
245
|
+
let response;
|
|
246
|
+
try {
|
|
247
|
+
response = await config.fetch(target, {
|
|
248
|
+
method: "POST",
|
|
249
|
+
headers: {
|
|
250
|
+
authorization: `Bearer ${token}`,
|
|
251
|
+
accept: "application/json",
|
|
252
|
+
"content-type": "application/json",
|
|
253
|
+
"user-agent": USER_AGENT
|
|
254
|
+
},
|
|
255
|
+
body: JSON.stringify(options.body ?? {}),
|
|
256
|
+
signal: AbortSignal.timeout(config.timeoutMs)
|
|
257
|
+
});
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (isTimeout(error)) {
|
|
260
|
+
throw new IncortaTimeoutError(
|
|
261
|
+
`Request to ${target} timed out after ${config.timeoutMs}ms.`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (attempt < config.maxRetries) {
|
|
265
|
+
await sleep(BACKOFF_BASE_MS * 2 ** attempt);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
throw new IncortaConnectionError(
|
|
269
|
+
`Could not reach ${target}: ${error instanceof Error ? error.message : String(error)}`,
|
|
270
|
+
error
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (RETRY_STATUSES.has(response.status) && attempt < config.maxRetries) {
|
|
274
|
+
await sleep(retryAfterMs(response) ?? BACKOFF_BASE_MS * 2 ** attempt);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
return decode(response, target);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function decode(response, url) {
|
|
281
|
+
const body = await response.text();
|
|
282
|
+
let payload;
|
|
283
|
+
if (body.trim()) {
|
|
284
|
+
try {
|
|
285
|
+
payload = JSON.parse(body);
|
|
286
|
+
} catch {
|
|
287
|
+
payload = void 0;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (!response.ok) {
|
|
291
|
+
throw apiErrorFromResponse(response.status, body, { url, payload });
|
|
292
|
+
}
|
|
293
|
+
if (payload === void 0) {
|
|
294
|
+
throw new IncortaApiError(
|
|
295
|
+
response.status,
|
|
296
|
+
"Expected a JSON response from Incorta but the body was not valid JSON.",
|
|
297
|
+
{ responseBody: body, url }
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return payload;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/core/query.ts
|
|
304
|
+
var DEFAULT_QUERY_PAGE_SIZE = 1e3;
|
|
305
|
+
function oneOf(value, allowed, what, normalise = (raw) => raw) {
|
|
306
|
+
const candidate = normalise(String(value).trim());
|
|
307
|
+
if (allowed.includes(candidate)) return candidate;
|
|
308
|
+
throw new IncortaConfigError(
|
|
309
|
+
`Invalid ${what} "${value}". Expected one of: ${allowed.join(", ")}.`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
function validateNullValueAs(value) {
|
|
313
|
+
if (String(value).trim().toUpperCase() === "DASH") {
|
|
314
|
+
throw new IncortaConfigError(
|
|
315
|
+
`nullValueAs "DASH" appears in Incorta's API documentation but the server rejects it with HTTP 400. Use NULL or EMPTY.`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
return oneOf(value, NULL_VALUE_AS, "null representation", (raw) => raw.toUpperCase());
|
|
319
|
+
}
|
|
320
|
+
function clean(payload) {
|
|
321
|
+
const out = {};
|
|
322
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
323
|
+
if (value === void 0) continue;
|
|
324
|
+
if (Array.isArray(value) && value.length === 0) continue;
|
|
325
|
+
out[key] = value;
|
|
326
|
+
}
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
function requireFieldOrFormula(field, formula, what) {
|
|
330
|
+
if (!field && !formula) {
|
|
331
|
+
throw new IncortaConfigError(`${what} needs either a field or a formula.`);
|
|
332
|
+
}
|
|
333
|
+
if (field && formula) {
|
|
334
|
+
throw new IncortaConfigError(
|
|
335
|
+
`${what} takes a field or a formula, not both \u2014 Incorta would ignore one silently.`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function toMeasure(value) {
|
|
340
|
+
return typeof value === "string" ? { field: value } : value;
|
|
341
|
+
}
|
|
342
|
+
function toDimension(value) {
|
|
343
|
+
return typeof value === "string" ? { field: value } : value;
|
|
344
|
+
}
|
|
345
|
+
function sortToApi(sort) {
|
|
346
|
+
return clean({
|
|
347
|
+
field: sort.field,
|
|
348
|
+
formula: sort.formula,
|
|
349
|
+
label: sort.label,
|
|
350
|
+
dir: oneOf(sort.dir ?? "asc", SORT_DIRECTIONS, "sort direction", (raw) => raw.toLowerCase()),
|
|
351
|
+
measureIndex: sort.measureIndex
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
function filterToApi(filter) {
|
|
355
|
+
if (filter.type !== "fieldKey" && filter.type !== "formulaKey") {
|
|
356
|
+
throw new IncortaConfigError(
|
|
357
|
+
`Invalid filter type "${filter.type}". Expected "fieldKey" or "formulaKey".`
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
if (filter.type === "fieldKey" && !filter.fieldKey) {
|
|
361
|
+
throw new IncortaConfigError("A fieldKey filter needs fieldKey.");
|
|
362
|
+
}
|
|
363
|
+
if (filter.type === "formulaKey" && !filter.formulaKey) {
|
|
364
|
+
throw new IncortaConfigError("A formulaKey filter needs formulaKey.");
|
|
365
|
+
}
|
|
366
|
+
const op = filter.op === void 0 ? void 0 : oneOf(filter.op, FILTER_OPS, "filter operator");
|
|
367
|
+
const values = (filter.values ?? []).map(String);
|
|
368
|
+
if (op === "BETWEEN" && values.length !== 2) {
|
|
369
|
+
throw new IncortaConfigError("BETWEEN needs exactly two values.");
|
|
370
|
+
}
|
|
371
|
+
const value = clean({
|
|
372
|
+
values,
|
|
373
|
+
op,
|
|
374
|
+
options: filter.caseSensitive === void 0 ? void 0 : { caseSensitive: filter.caseSensitive }
|
|
375
|
+
});
|
|
376
|
+
return clean({
|
|
377
|
+
type: filter.type,
|
|
378
|
+
label: filter.label,
|
|
379
|
+
fieldKey: filter.fieldKey,
|
|
380
|
+
formulaKey: filter.formulaKey,
|
|
381
|
+
value: Object.keys(value).length > 0 ? value : void 0,
|
|
382
|
+
prompt: filter.prompt || void 0
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
function aggregateFilterToApi(filter) {
|
|
386
|
+
requireFieldOrFormula(filter.field, filter.formula, "An aggregate filter");
|
|
387
|
+
const op = filter.op === void 0 ? void 0 : oneOf(filter.op, FILTER_OPS, "filter operator");
|
|
388
|
+
if (op === "IN_LIST") {
|
|
389
|
+
throw new IncortaConfigError("IN_LIST is not accepted by aggregate filters.");
|
|
390
|
+
}
|
|
391
|
+
const values = (filter.values ?? []).map(String);
|
|
392
|
+
if (op === "BETWEEN" && values.length !== 2) {
|
|
393
|
+
throw new IncortaConfigError("BETWEEN needs exactly two values.");
|
|
394
|
+
}
|
|
395
|
+
return clean({
|
|
396
|
+
field: filter.field,
|
|
397
|
+
formula: filter.formula,
|
|
398
|
+
op,
|
|
399
|
+
values,
|
|
400
|
+
aggregation: filter.aggregation === void 0 ? void 0 : oneOf(filter.aggregation, AGGREGATIONS, "aggregation", (raw) => raw.toLowerCase())
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
function measureToApi(measure) {
|
|
404
|
+
requireFieldOrFormula(measure.field, measure.formula, "A measure");
|
|
405
|
+
return clean({
|
|
406
|
+
field: measure.field,
|
|
407
|
+
formula: measure.formula,
|
|
408
|
+
label: measure.label,
|
|
409
|
+
aggregation: measure.aggregation === void 0 ? void 0 : oneOf(measure.aggregation, AGGREGATIONS, "aggregation", (raw) => raw.toLowerCase()),
|
|
410
|
+
scale: measure.scale,
|
|
411
|
+
sourceField: measure.sourceField,
|
|
412
|
+
filters: (measure.filters ?? []).map(filterToApi)
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
function dimensionToApi(dimension, asRow) {
|
|
416
|
+
requireFieldOrFormula(dimension.field, dimension.formula, "A dimension");
|
|
417
|
+
if (!asRow && (dimension.subTotal || dimension.showEmptyGroups)) {
|
|
418
|
+
throw new IncortaConfigError("subTotal and showEmptyGroups apply to row dimensions only.");
|
|
419
|
+
}
|
|
420
|
+
return clean({
|
|
421
|
+
field: dimension.field,
|
|
422
|
+
formula: dimension.formula,
|
|
423
|
+
label: dimension.label,
|
|
424
|
+
sorting: (dimension.sorting ?? []).map(sortToApi),
|
|
425
|
+
datePart: dimension.datePart || void 0,
|
|
426
|
+
subTotal: asRow ? dimension.subTotal || void 0 : void 0,
|
|
427
|
+
showEmptyGroups: asRow ? dimension.showEmptyGroups || void 0 : void 0
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
function attachSorts(sorting, dims, payloads) {
|
|
431
|
+
for (const sort of sorting) {
|
|
432
|
+
const target = sort.field ?? sort.formula;
|
|
433
|
+
const index = dims.findIndex(
|
|
434
|
+
(dim) => target !== void 0 && (dim.field === target || dim.formula === target)
|
|
435
|
+
);
|
|
436
|
+
if (index === -1) {
|
|
437
|
+
const available = dims.map((dim) => dim.field ?? dim.formula ?? "?");
|
|
438
|
+
throw new IncortaConfigError(
|
|
439
|
+
`Cannot sort an aggregate query by "${target}": it is not one of its dimensions (${available.join(", ") || "none given"}). Incorta ignores sorting on anything else in an aggregate query and returns HTTP 200, so this is rejected rather than silently dropped.`
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
const payload = payloads[index];
|
|
443
|
+
const existing = payload.sorting ?? [];
|
|
444
|
+
payload.sorting = [...existing, sortToApi(sort)];
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function uniqueLabels(labels) {
|
|
448
|
+
const seen = /* @__PURE__ */ new Map();
|
|
449
|
+
return labels.map((label) => {
|
|
450
|
+
const count = seen.get(label) ?? 0;
|
|
451
|
+
seen.set(label, count + 1);
|
|
452
|
+
return count === 0 ? label : `${label}_${count + 1}`;
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
function headersOf(raw, kind) {
|
|
456
|
+
if (!Array.isArray(raw)) return [];
|
|
457
|
+
return raw.filter((item) => typeof item === "object" && item !== null).map((item) => ({
|
|
458
|
+
label: String(item.label ?? ""),
|
|
459
|
+
dataType: String(item.dataType ?? ""),
|
|
460
|
+
kind,
|
|
461
|
+
raw: item
|
|
462
|
+
}));
|
|
463
|
+
}
|
|
464
|
+
function intOf(payload, key) {
|
|
465
|
+
const value = Number(payload[key]);
|
|
466
|
+
return Number.isFinite(value) ? Math.trunc(value) : 0;
|
|
467
|
+
}
|
|
468
|
+
function parseQueryResult(payload) {
|
|
469
|
+
const columns = [
|
|
470
|
+
...headersOf(payload.rowHeaders, "row"),
|
|
471
|
+
...headersOf(payload.colHeaders, "column"),
|
|
472
|
+
...headersOf(payload.measureHeaders, "measure")
|
|
473
|
+
];
|
|
474
|
+
const rows = (Array.isArray(payload.data) ? payload.data : []).filter((row) => Array.isArray(row)).map((row) => row.map((cell) => cell === null || cell === void 0 ? "" : String(cell)));
|
|
475
|
+
const headers = columns.map((column) => column.label);
|
|
476
|
+
const totalRows = intOf(payload, "totalRows");
|
|
477
|
+
const endRow = intOf(payload, "endRow");
|
|
478
|
+
const complete = payload.complete === void 0 ? true : Boolean(payload.complete);
|
|
479
|
+
return {
|
|
480
|
+
columns,
|
|
481
|
+
headers,
|
|
482
|
+
rows,
|
|
483
|
+
totalRows,
|
|
484
|
+
startRow: intOf(payload, "startRow"),
|
|
485
|
+
endRow,
|
|
486
|
+
complete,
|
|
487
|
+
hasMore: !complete && endRow < totalRows,
|
|
488
|
+
isAggregated: Boolean(payload.isAggregated),
|
|
489
|
+
isSampled: Boolean(payload.isSampled),
|
|
490
|
+
raw: payload,
|
|
491
|
+
records() {
|
|
492
|
+
const keys = uniqueLabels(headers);
|
|
493
|
+
return rows.map((row) => {
|
|
494
|
+
const record = {};
|
|
495
|
+
keys.forEach((key, index) => {
|
|
496
|
+
record[key] = row[index] ?? "";
|
|
497
|
+
});
|
|
498
|
+
return record;
|
|
499
|
+
});
|
|
500
|
+
},
|
|
501
|
+
column(label) {
|
|
502
|
+
const target = label.toLowerCase();
|
|
503
|
+
const index = headers.findIndex((header) => header.toLowerCase() === target);
|
|
504
|
+
if (index === -1) {
|
|
505
|
+
throw new IncortaConfigError(
|
|
506
|
+
`No column "${label}" in this result. Available: ${headers.join(", ")}.`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
return rows.map((row) => row[index] ?? "");
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function createDataResource(context) {
|
|
514
|
+
function buildBody(measures, options = {}) {
|
|
515
|
+
if (!measures || measures.length === 0) {
|
|
516
|
+
throw new IncortaConfigError(
|
|
517
|
+
"A query needs at least one measure \u2014 the field or fields to return."
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
const pageSize = options.pageSize ?? 0;
|
|
521
|
+
const startRow = options.startRow ?? 0;
|
|
522
|
+
if (!Number.isInteger(pageSize) || pageSize < 0) {
|
|
523
|
+
throw new IncortaConfigError("pageSize must be an integer >= 0 (0 means the server default).");
|
|
524
|
+
}
|
|
525
|
+
if (!Number.isInteger(startRow) || startRow < 0) {
|
|
526
|
+
throw new IncortaConfigError("startRow must be an integer >= 0.");
|
|
527
|
+
}
|
|
528
|
+
const format = oneOf(
|
|
529
|
+
options.format ?? "json",
|
|
530
|
+
QUERY_FORMATS,
|
|
531
|
+
"result format",
|
|
532
|
+
(raw) => raw.toLowerCase()
|
|
533
|
+
);
|
|
534
|
+
const aggregate = options.aggregate ?? false;
|
|
535
|
+
const rowDims = (options.rows ?? []).map(toDimension);
|
|
536
|
+
const colDims = (options.columns ?? []).map(toDimension);
|
|
537
|
+
const rowPayloads = rowDims.map((dim) => dimensionToApi(dim, true));
|
|
538
|
+
const colPayloads = colDims.map((dim) => dimensionToApi(dim, false));
|
|
539
|
+
const query2 = {
|
|
540
|
+
// Always explicit. Omitting it means "aggregate", and a non-aggregate
|
|
541
|
+
// extract then silently returns zero rows.
|
|
542
|
+
aggregate,
|
|
543
|
+
format,
|
|
544
|
+
measures: measures.map((measure) => measureToApi(toMeasure(measure)))
|
|
545
|
+
};
|
|
546
|
+
const sorting = options.sorting ?? [];
|
|
547
|
+
if (aggregate) {
|
|
548
|
+
attachSorts(sorting, [...rowDims, ...colDims], [...rowPayloads, ...colPayloads]);
|
|
549
|
+
} else if (sorting.length > 0) {
|
|
550
|
+
query2.sorting = sorting.map(sortToApi);
|
|
551
|
+
}
|
|
552
|
+
if (rowPayloads.length > 0) query2.rows = rowPayloads;
|
|
553
|
+
if (colPayloads.length > 0) query2.columns = colPayloads;
|
|
554
|
+
if (options.filters?.length) query2.filters = options.filters.map(filterToApi);
|
|
555
|
+
if (options.aggregateFilters?.length) {
|
|
556
|
+
query2.aggregateFilters = options.aggregateFilters.map(aggregateFilterToApi);
|
|
557
|
+
}
|
|
558
|
+
if (pageSize) query2.pageSize = pageSize;
|
|
559
|
+
if (startRow) query2.startRow = startRow;
|
|
560
|
+
if (options.formatted) query2.formatted = true;
|
|
561
|
+
if (options.sampled) query2.sampled = true;
|
|
562
|
+
if (options.nullValueAs !== void 0) {
|
|
563
|
+
query2.nullValueAs = validateNullValueAs(options.nullValueAs);
|
|
564
|
+
}
|
|
565
|
+
Object.assign(query2, options.advanced ?? {});
|
|
566
|
+
const body = { query: query2 };
|
|
567
|
+
if (format === "json") body.stringify = false;
|
|
568
|
+
if (options.asUser !== void 0) {
|
|
569
|
+
if (!options.asUser.trim()) {
|
|
570
|
+
throw new IncortaConfigError("asUser must be a non-empty login name.");
|
|
571
|
+
}
|
|
572
|
+
body.username = Buffer.from(options.asUser, "utf8").toString("base64");
|
|
573
|
+
}
|
|
574
|
+
return body;
|
|
575
|
+
}
|
|
576
|
+
function send(body) {
|
|
577
|
+
return postJson(context, urlFor(context.config, "query"), { body });
|
|
578
|
+
}
|
|
579
|
+
async function query(measures, options = {}) {
|
|
580
|
+
const payload = await send(buildBody(measures, { ...options, format: "json" }));
|
|
581
|
+
return parseQueryResult(payload ?? {});
|
|
582
|
+
}
|
|
583
|
+
return {
|
|
584
|
+
query,
|
|
585
|
+
buildBody,
|
|
586
|
+
raw: send,
|
|
587
|
+
async *iterRows(measures, options = {}) {
|
|
588
|
+
const pageSize = options.pageSize ?? DEFAULT_QUERY_PAGE_SIZE;
|
|
589
|
+
if (!Number.isInteger(pageSize) || pageSize <= 0) {
|
|
590
|
+
throw new IncortaConfigError("pageSize must be a positive integer.");
|
|
591
|
+
}
|
|
592
|
+
let startRow = 0;
|
|
593
|
+
for (; ; ) {
|
|
594
|
+
const page = await query(measures, { ...options, pageSize, startRow });
|
|
595
|
+
yield* page.rows;
|
|
596
|
+
if (page.rows.length === 0 || !page.hasMore) return;
|
|
597
|
+
startRow += page.rows.length;
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
async csv(measures, options = {}) {
|
|
601
|
+
const payload = await send(buildBody(measures, { ...options, format: "csv" }));
|
|
602
|
+
const text = payload && typeof payload === "object" ? payload.data : void 0;
|
|
603
|
+
if (typeof text !== "string") {
|
|
604
|
+
throw new IncortaConfigError(
|
|
605
|
+
"Expected a CSV payload under 'data' but Incorta returned something else."
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
return text;
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// src/core/models.ts
|
|
614
|
+
function str(payload, key, fallback = "") {
|
|
615
|
+
const value = payload[key];
|
|
616
|
+
if (value === null || value === void 0) return fallback;
|
|
617
|
+
return typeof value === "string" ? value : String(value);
|
|
618
|
+
}
|
|
619
|
+
function bool(payload, key) {
|
|
620
|
+
return Boolean(payload[key]);
|
|
621
|
+
}
|
|
622
|
+
function int(payload, key) {
|
|
623
|
+
const value = Number(payload[key]);
|
|
624
|
+
return Number.isFinite(value) ? Math.trunc(value) : 0;
|
|
625
|
+
}
|
|
626
|
+
function epochMillisToDate(value) {
|
|
627
|
+
const millis = Number(value);
|
|
628
|
+
if (!Number.isFinite(millis) || millis <= 0) return null;
|
|
629
|
+
return new Date(millis);
|
|
630
|
+
}
|
|
631
|
+
function records(value) {
|
|
632
|
+
if (!Array.isArray(value)) return [];
|
|
633
|
+
return value.filter((item) => typeof item === "object" && item !== null);
|
|
634
|
+
}
|
|
635
|
+
function parseTableColumn(payload) {
|
|
636
|
+
const fn = parseColumnFunction(str(payload, "function") || null);
|
|
637
|
+
return {
|
|
638
|
+
kind: "tableColumn",
|
|
639
|
+
name: str(payload, "name"),
|
|
640
|
+
label: str(payload, "label"),
|
|
641
|
+
description: str(payload, "description"),
|
|
642
|
+
dataType: str(payload, "dataType"),
|
|
643
|
+
function: fn,
|
|
644
|
+
formula: str(payload, "formula"),
|
|
645
|
+
isEncrypted: bool(payload, "isEncrypt"),
|
|
646
|
+
isKey: fn === "key",
|
|
647
|
+
raw: payload
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function parseViewColumn(payload) {
|
|
651
|
+
return {
|
|
652
|
+
kind: "viewColumn",
|
|
653
|
+
name: str(payload, "name"),
|
|
654
|
+
label: str(payload, "label"),
|
|
655
|
+
description: str(payload, "description"),
|
|
656
|
+
dataType: str(payload, "dataType"),
|
|
657
|
+
function: parseColumnFunction(str(payload, "function") || null),
|
|
658
|
+
source: str(payload, "source"),
|
|
659
|
+
isFormula: bool(payload, "isFormula"),
|
|
660
|
+
raw: payload
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function qualify(schemaName, name) {
|
|
664
|
+
return schemaName ? `${schemaName}.${name}` : name;
|
|
665
|
+
}
|
|
666
|
+
function parseTable(payload, schemaName) {
|
|
667
|
+
const columns = records(payload.columns).map(parseTableColumn);
|
|
668
|
+
const name = str(payload, "name");
|
|
669
|
+
return {
|
|
670
|
+
kind: "table",
|
|
671
|
+
name,
|
|
672
|
+
type: parseObjectType(str(payload, "type") || null),
|
|
673
|
+
owner: str(payload, "owner"),
|
|
674
|
+
description: str(payload, "description"),
|
|
675
|
+
schemaName,
|
|
676
|
+
qualifiedName: qualify(schemaName, name),
|
|
677
|
+
columns,
|
|
678
|
+
columnNames: columns.map((column) => column.name),
|
|
679
|
+
keys: columns.filter((column) => column.isKey),
|
|
680
|
+
rowsCount: int(payload, "rowsCount"),
|
|
681
|
+
multiDataSource: bool(payload, "multiDataSource"),
|
|
682
|
+
hierarchy: bool(payload, "hierarchy"),
|
|
683
|
+
lastVersion: epochMillisToDate(payload.lastVersionTimestamp),
|
|
684
|
+
raw: payload
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function parseView(payload, schemaName) {
|
|
688
|
+
const columns = records(payload.columns).map(parseViewColumn);
|
|
689
|
+
const name = str(payload, "name");
|
|
690
|
+
return {
|
|
691
|
+
kind: "view",
|
|
692
|
+
name,
|
|
693
|
+
type: parseObjectType(str(payload, "type") || null),
|
|
694
|
+
owner: str(payload, "owner"),
|
|
695
|
+
description: str(payload, "description"),
|
|
696
|
+
schemaName,
|
|
697
|
+
qualifiedName: qualify(schemaName, name),
|
|
698
|
+
columns,
|
|
699
|
+
columnNames: columns.map((column) => column.name),
|
|
700
|
+
baseTable: str(payload, "baseTable"),
|
|
701
|
+
sources: [...new Set(columns.map((column) => column.source).filter(Boolean))],
|
|
702
|
+
raw: payload
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
function parseSchemaInfo(payload) {
|
|
706
|
+
const type = str(payload, "schemaType");
|
|
707
|
+
return {
|
|
708
|
+
id: int(payload, "schemaID"),
|
|
709
|
+
name: str(payload, "schemaName"),
|
|
710
|
+
description: str(payload, "schemaDescription"),
|
|
711
|
+
type,
|
|
712
|
+
owner: str(payload, "owner"),
|
|
713
|
+
lastModified: epochMillisToDate(payload.lastModified),
|
|
714
|
+
isEmpty: bool(payload, "isEmpty"),
|
|
715
|
+
isPhysical: type.toUpperCase() === "PHYSICAL",
|
|
716
|
+
isBusiness: type.toUpperCase() === "BUSINESS",
|
|
717
|
+
raw: payload
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
function parseSchema(payload, name) {
|
|
721
|
+
const total2 = int(payload, "total");
|
|
722
|
+
const viewsPayload = records(payload.viewsDetails);
|
|
723
|
+
if (viewsPayload.length > 0) {
|
|
724
|
+
const views = viewsPayload.map((item) => parseView(item, name));
|
|
725
|
+
return {
|
|
726
|
+
kind: "business",
|
|
727
|
+
name,
|
|
728
|
+
total: total2,
|
|
729
|
+
objects: views,
|
|
730
|
+
objectNames: views.map((view) => view.name),
|
|
731
|
+
views,
|
|
732
|
+
raw: payload
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
const tables = records(payload.tablesDetails).map((item) => parseTable(item, name));
|
|
736
|
+
return {
|
|
737
|
+
kind: "physical",
|
|
738
|
+
name,
|
|
739
|
+
total: total2,
|
|
740
|
+
objects: tables,
|
|
741
|
+
objectNames: tables.map((table) => table.name),
|
|
742
|
+
tables,
|
|
743
|
+
raw: payload
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function findObject(schema, name) {
|
|
747
|
+
const target = name.toLowerCase();
|
|
748
|
+
return schema.objects.find((object) => object.name.toLowerCase() === target);
|
|
749
|
+
}
|
|
750
|
+
function findColumn(object, name) {
|
|
751
|
+
const target = name.toLowerCase();
|
|
752
|
+
return object.columns.find((column) => column.name.toLowerCase() === target);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/core/schemas.ts
|
|
756
|
+
var DEFAULT_PAGE_SIZE = 100;
|
|
757
|
+
function validateSchemaType(value) {
|
|
758
|
+
if (value === void 0) return "ALL";
|
|
759
|
+
const normalised = String(value).trim().toUpperCase();
|
|
760
|
+
if (SCHEMA_TYPES.includes(normalised)) return normalised;
|
|
761
|
+
throw new IncortaConfigError(
|
|
762
|
+
`Invalid schema type "${value}". Expected one of: ${SCHEMA_TYPES.join(", ")}. Note the Incorta API accepts unknown values silently and returns BUSINESS schemas, so this is validated client-side.`
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
function validateSortBy(value) {
|
|
766
|
+
if (value === void 0) return "NAME_ASC";
|
|
767
|
+
const normalised = String(value).trim().toUpperCase();
|
|
768
|
+
if (SORT_ORDERS.includes(normalised)) return normalised;
|
|
769
|
+
throw new IncortaConfigError(
|
|
770
|
+
`Invalid sort order "${value}". Expected one of: ${SORT_ORDERS.join(", ")}.`
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
function validateWindow(limit, offset) {
|
|
774
|
+
if (!Number.isInteger(limit) || limit < 0) {
|
|
775
|
+
throw new IncortaConfigError("limit must be an integer >= 0 (0 means no limit).");
|
|
776
|
+
}
|
|
777
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
778
|
+
throw new IncortaConfigError("offset must be an integer >= 0.");
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
function details(payload, key) {
|
|
782
|
+
if (!payload || typeof payload !== "object") return [];
|
|
783
|
+
const value = payload[key];
|
|
784
|
+
if (!Array.isArray(value)) return [];
|
|
785
|
+
return value.filter(
|
|
786
|
+
(item) => typeof item === "object" && item !== null
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
function total(payload, fallback) {
|
|
790
|
+
if (!payload || typeof payload !== "object") return fallback;
|
|
791
|
+
const value = Number(payload.total);
|
|
792
|
+
return Number.isFinite(value) ? Math.trunc(value) : fallback;
|
|
793
|
+
}
|
|
794
|
+
function createSchemasResource(context) {
|
|
795
|
+
async function listPage(options = {}) {
|
|
796
|
+
const schemaType = validateSchemaType(options.type);
|
|
797
|
+
const ordering = validateSortBy(options.sortBy);
|
|
798
|
+
const limit = options.limit ?? 0;
|
|
799
|
+
const offset = options.offset ?? 0;
|
|
800
|
+
validateWindow(limit, offset);
|
|
801
|
+
const payload = await postJson(context, urlFor(context.config, "schema", "list"), {
|
|
802
|
+
params: { schemaType },
|
|
803
|
+
body: { limit, offset, sortBy: ordering }
|
|
804
|
+
});
|
|
805
|
+
const items = details(payload, "schemasDetails").map(parseSchemaInfo);
|
|
806
|
+
const serverTotal = total(payload, items.length);
|
|
807
|
+
return {
|
|
808
|
+
items,
|
|
809
|
+
total: serverTotal,
|
|
810
|
+
limit,
|
|
811
|
+
offset,
|
|
812
|
+
hasMore: limit === 0 ? false : offset + items.length < serverTotal
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
async function list(options = {}) {
|
|
816
|
+
const page = await listPage(options);
|
|
817
|
+
return [...page.items];
|
|
818
|
+
}
|
|
819
|
+
async function get(name) {
|
|
820
|
+
if (!name || !name.trim()) {
|
|
821
|
+
throw new IncortaConfigError("Schema name must be a non-empty string.");
|
|
822
|
+
}
|
|
823
|
+
let payload;
|
|
824
|
+
try {
|
|
825
|
+
payload = await postJson(context, urlFor(context.config, "schema", name, "list"), {
|
|
826
|
+
body: { limit: 0, offset: 0 }
|
|
827
|
+
});
|
|
828
|
+
} catch (error) {
|
|
829
|
+
if (error instanceof NotFoundError) {
|
|
830
|
+
throw new SchemaNotFoundError(error.statusCode, error.detail, {
|
|
831
|
+
code: error.code,
|
|
832
|
+
responseBody: error.responseBody,
|
|
833
|
+
url: error.url
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
throw error;
|
|
837
|
+
}
|
|
838
|
+
return parseSchema(payload, name);
|
|
839
|
+
}
|
|
840
|
+
return {
|
|
841
|
+
list,
|
|
842
|
+
listPage,
|
|
843
|
+
async *iterAll(options = {}) {
|
|
844
|
+
const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
845
|
+
if (!Number.isInteger(pageSize) || pageSize <= 0) {
|
|
846
|
+
throw new IncortaConfigError("pageSize must be a positive integer.");
|
|
847
|
+
}
|
|
848
|
+
let offset = 0;
|
|
849
|
+
for (; ; ) {
|
|
850
|
+
const page = await listPage({
|
|
851
|
+
type: options.type,
|
|
852
|
+
sortBy: options.sortBy,
|
|
853
|
+
limit: pageSize,
|
|
854
|
+
offset
|
|
855
|
+
});
|
|
856
|
+
yield* page.items;
|
|
857
|
+
if (page.items.length === 0 || !page.hasMore) return;
|
|
858
|
+
offset += page.items.length;
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
physical: (options = {}) => list({ ...options, type: "PHYSICAL" }),
|
|
862
|
+
business: (options = {}) => list({ ...options, type: "BUSINESS" }),
|
|
863
|
+
get,
|
|
864
|
+
async exists(name) {
|
|
865
|
+
try {
|
|
866
|
+
await get(name);
|
|
867
|
+
return true;
|
|
868
|
+
} catch (error) {
|
|
869
|
+
if (error instanceof SchemaNotFoundError) return false;
|
|
870
|
+
throw error;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/core/tables.ts
|
|
877
|
+
function createTablesResource(schemas) {
|
|
878
|
+
async function get(schemaName, tableName) {
|
|
879
|
+
if (!tableName || !tableName.trim()) {
|
|
880
|
+
throw new IncortaConfigError("Table name must be a non-empty string.");
|
|
881
|
+
}
|
|
882
|
+
const schema = await schemas.get(schemaName);
|
|
883
|
+
const found = findObject(schema, tableName);
|
|
884
|
+
if (!found) {
|
|
885
|
+
throw new TableNotFoundError(schemaName, tableName, [...schema.objectNames]);
|
|
886
|
+
}
|
|
887
|
+
return found;
|
|
888
|
+
}
|
|
889
|
+
return {
|
|
890
|
+
get,
|
|
891
|
+
async list(schemaName) {
|
|
892
|
+
const schema = await schemas.get(schemaName);
|
|
893
|
+
return [...schema.objects];
|
|
894
|
+
},
|
|
895
|
+
async names(schemaName) {
|
|
896
|
+
const schema = await schemas.get(schemaName);
|
|
897
|
+
return [...schema.objectNames];
|
|
898
|
+
},
|
|
899
|
+
async columns(schemaName, tableName) {
|
|
900
|
+
const object = await get(schemaName, tableName);
|
|
901
|
+
return [...object.columns];
|
|
902
|
+
},
|
|
903
|
+
async exists(schemaName, tableName) {
|
|
904
|
+
try {
|
|
905
|
+
await get(schemaName, tableName);
|
|
906
|
+
return true;
|
|
907
|
+
} catch (error) {
|
|
908
|
+
if (error instanceof TableNotFoundError) return false;
|
|
909
|
+
throw error;
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
async tablesOnly(schemaName) {
|
|
913
|
+
const schema = await schemas.get(schemaName);
|
|
914
|
+
return schema.kind === "physical" ? [...schema.tables] : [];
|
|
915
|
+
},
|
|
916
|
+
async viewsOnly(schemaName) {
|
|
917
|
+
const schema = await schemas.get(schemaName);
|
|
918
|
+
return schema.kind === "business" ? [...schema.views] : [];
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// src/core/user-client.ts
|
|
924
|
+
function createUserClient(config, options) {
|
|
925
|
+
const context = { config, getToken: options.getToken };
|
|
926
|
+
const schemas = createSchemasResource(context);
|
|
927
|
+
const user = options.user ?? null;
|
|
928
|
+
return {
|
|
929
|
+
user,
|
|
930
|
+
schemas,
|
|
931
|
+
tables: createTablesResource(schemas),
|
|
932
|
+
data: createDataResource(context),
|
|
933
|
+
info: {
|
|
934
|
+
baseUrl: config.baseUrl,
|
|
935
|
+
tenant: config.tenant,
|
|
936
|
+
user: user?.sub,
|
|
937
|
+
accessTokenExpiresAt: options.accessTokenExpiresAt ?? 0
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// src/index.ts
|
|
943
|
+
function createIncortaClient(config = {}) {
|
|
944
|
+
const resolved = resolveConfig(config);
|
|
945
|
+
function forSession(session) {
|
|
946
|
+
return createUserClient(resolved, {
|
|
947
|
+
// Checked at call time, not now: a client built at the top of a request
|
|
948
|
+
// should not fail for a token that is still valid when it is used.
|
|
949
|
+
getToken: () => {
|
|
950
|
+
if (session.accessTokenExpiresAt <= Date.now()) {
|
|
951
|
+
throw new IncortaSessionExpiredError(session.accessTokenExpiresAt);
|
|
952
|
+
}
|
|
953
|
+
return session.accessToken;
|
|
954
|
+
},
|
|
955
|
+
user: session.user,
|
|
956
|
+
accessTokenExpiresAt: session.accessTokenExpiresAt
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
return {
|
|
960
|
+
auth: resolved.auth,
|
|
961
|
+
forSession,
|
|
962
|
+
async forRequest(request) {
|
|
963
|
+
const session = await resolved.auth.getSession(request);
|
|
964
|
+
if (!session) throw new IncortaAuthRequiredError();
|
|
965
|
+
return forSession(session);
|
|
966
|
+
},
|
|
967
|
+
forAccessToken(accessToken, options = {}) {
|
|
968
|
+
return createUserClient(resolved, {
|
|
969
|
+
getToken: () => accessToken,
|
|
970
|
+
user: options.user
|
|
971
|
+
});
|
|
972
|
+
},
|
|
973
|
+
info: {
|
|
974
|
+
baseUrl: resolved.baseUrl,
|
|
975
|
+
tenant: resolved.tenant,
|
|
976
|
+
timeoutMs: resolved.timeoutMs,
|
|
977
|
+
maxRetries: resolved.maxRetries
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
export {
|
|
982
|
+
AGGREGATIONS,
|
|
983
|
+
AuthenticationError,
|
|
984
|
+
COLUMN_FUNCTIONS,
|
|
985
|
+
DEFAULT_PAGE_SIZE,
|
|
986
|
+
DEFAULT_QUERY_PAGE_SIZE,
|
|
987
|
+
FILTER_OPS,
|
|
988
|
+
IncortaApiError,
|
|
989
|
+
IncortaAuthRequiredError,
|
|
990
|
+
IncortaConfigError,
|
|
991
|
+
IncortaConnectionError,
|
|
992
|
+
IncortaError,
|
|
993
|
+
IncortaServerError,
|
|
994
|
+
IncortaSessionExpiredError,
|
|
995
|
+
IncortaTimeoutError,
|
|
996
|
+
NULL_VALUE_AS,
|
|
997
|
+
NotFoundError,
|
|
998
|
+
PermissionDeniedError,
|
|
999
|
+
QUERY_FORMATS,
|
|
1000
|
+
SCHEMA_TYPES,
|
|
1001
|
+
SORT_DIRECTIONS,
|
|
1002
|
+
SORT_ORDERS,
|
|
1003
|
+
SchemaNotFoundError,
|
|
1004
|
+
TableNotFoundError,
|
|
1005
|
+
createIncortaClient,
|
|
1006
|
+
findColumn,
|
|
1007
|
+
findObject,
|
|
1008
|
+
parseColumnFunction,
|
|
1009
|
+
parseObjectType,
|
|
1010
|
+
parseQueryResult
|
|
1011
|
+
};
|
|
1012
|
+
//# sourceMappingURL=index.js.map
|