@kaelio/ktx 0.14.0 → 0.16.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/assets/python/kaelio_ktx-0.16.0-py3-none-any.whl +0 -0
- package/assets/python/manifest.json +4 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/commands/setup-commands.js +2 -0
- package/dist/connection-drivers.d.ts +1 -1
- package/dist/connection-drivers.js +2 -0
- package/dist/connection.js +1 -0
- package/dist/connectors/athena/connector.d.ts +160 -0
- package/dist/connectors/athena/connector.js +378 -0
- package/dist/connectors/athena/dialect.d.ts +31 -0
- package/dist/connectors/athena/dialect.js +146 -0
- package/dist/connectors/athena/live-database-introspection.d.ts +10 -0
- package/dist/connectors/athena/live-database-introspection.js +24 -0
- package/dist/connectors/duckdb/connector.d.ts +60 -0
- package/dist/connectors/duckdb/connector.js +282 -0
- package/dist/connectors/duckdb/dialect.d.ts +31 -0
- package/dist/connectors/duckdb/dialect.js +155 -0
- package/dist/connectors/duckdb/federated-attach.js +7 -0
- package/dist/connectors/duckdb/federated-executor.js +7 -13
- package/dist/connectors/duckdb/live-database-introspection.d.ts +8 -0
- package/dist/connectors/duckdb/live-database-introspection.js +24 -0
- package/dist/connectors/shared/duckdb-json-safe.d.ts +3 -0
- package/dist/connectors/shared/duckdb-json-safe.js +12 -0
- package/dist/context/connections/connection-type-dialect.d.ts +2 -0
- package/dist/context/connections/connection-type-dialect.js +20 -0
- package/dist/context/connections/connection-type.d.ts +6 -15
- package/dist/context/connections/connection-type.js +1 -10
- package/dist/context/connections/dialects.js +4 -0
- package/dist/context/connections/drivers.js +38 -0
- package/dist/context/connections/federation.d.ts +1 -1
- package/dist/context/connections/federation.js +6 -5
- package/dist/context/connections/local-warehouse-descriptor.d.ts +1 -0
- package/dist/context/connections/local-warehouse-descriptor.js +10 -0
- package/dist/context/connections/project-sql-executor.d.ts +24 -0
- package/dist/context/connections/project-sql-executor.js +56 -0
- package/dist/context/connections/query-policy.d.ts +12 -0
- package/dist/context/connections/query-policy.js +46 -0
- package/dist/context/core/config-reference.js +11 -1
- package/dist/context/daemon/semantic-layer-compute.d.ts +16 -0
- package/dist/context/daemon/semantic-layer-compute.js +44 -2
- package/dist/context/ingest/adapters/looker/mapping.d.ts +1 -0
- package/dist/context/ingest/adapters/looker/mapping.js +3 -10
- package/dist/context/ingest/adapters/looker/types.d.ts +6 -15
- package/dist/context/ingest/adapters/metabase/mapping.d.ts +1 -0
- package/dist/context/ingest/adapters/metabase/mapping.js +1 -0
- package/dist/context/ingest/adapters/sigma/chunk.d.ts +6 -0
- package/dist/context/ingest/adapters/sigma/chunk.js +119 -0
- package/dist/context/ingest/adapters/sigma/client-port.d.ts +45 -0
- package/dist/context/ingest/adapters/sigma/client-port.js +1 -0
- package/dist/context/ingest/adapters/sigma/client.d.ts +33 -0
- package/dist/context/ingest/adapters/sigma/client.js +176 -0
- package/dist/context/ingest/adapters/sigma/detect.d.ts +1 -0
- package/dist/context/ingest/adapters/sigma/detect.js +23 -0
- package/dist/context/ingest/adapters/sigma/fetch.d.ts +14 -0
- package/dist/context/ingest/adapters/sigma/fetch.js +191 -0
- package/dist/context/ingest/adapters/sigma/local-sigma.adapter.d.ts +17 -0
- package/dist/context/ingest/adapters/sigma/local-sigma.adapter.js +41 -0
- package/dist/context/ingest/adapters/sigma/project.d.ts +8 -0
- package/dist/context/ingest/adapters/sigma/project.js +186 -0
- package/dist/context/ingest/adapters/sigma/sigma.adapter.d.ts +18 -0
- package/dist/context/ingest/adapters/sigma/sigma.adapter.js +43 -0
- package/dist/context/ingest/adapters/sigma/types.d.ts +80 -0
- package/dist/context/ingest/adapters/sigma/types.js +82 -0
- package/dist/context/ingest/local-adapters.d.ts +2 -1
- package/dist/context/ingest/local-adapters.js +22 -0
- package/dist/context/mcp/context-tools.js +1 -0
- package/dist/context/mcp/local-project-ports.d.ts +1 -1
- package/dist/context/mcp/local-project-ports.js +53 -51
- package/dist/context/project/config.d.ts +122 -0
- package/dist/context/project/driver-schemas.d.ts +61 -0
- package/dist/context/project/driver-schemas.js +47 -0
- package/dist/context/scan/local-scan.js +3 -1
- package/dist/context/scan/types.d.ts +1 -1
- package/dist/context/sl/local-query.js +23 -6
- package/dist/context/sl/semantic-layer.service.d.ts +0 -4
- package/dist/context/sl/semantic-layer.service.js +3 -20
- package/dist/context/sl/tools/sl-warehouse-validation.d.ts +1 -1
- package/dist/context/sl/tools/sl-warehouse-validation.js +2 -2
- package/dist/context/sl/types.d.ts +1 -0
- package/dist/context/sql-analysis/dialect-notes.d.ts +1 -1
- package/dist/context/sql-analysis/dialect-notes.js +3 -2
- package/dist/context/sql-analysis/dialect.js +1 -0
- package/dist/context/sql-analysis/dialects/athena.md +12 -0
- package/dist/context/sql-analysis/dialects/duckdb.md +10 -0
- package/dist/local-adapters.js +17 -0
- package/dist/public-ingest.js +1 -0
- package/dist/setup-databases.d.ts +1 -1
- package/dist/setup-databases.js +52 -2
- package/dist/setup-sources.d.ts +2 -1
- package/dist/setup-sources.js +84 -0
- package/dist/skills/sigma_ingest/SKILL.md +189 -0
- package/dist/sql.js +9 -14
- package/dist/status-project.js +2 -1
- package/dist/telemetry/events.d.ts +1 -1
- package/package.json +3 -1
- package/assets/python/kaelio_ktx-0.14.0-py3-none-any.whl +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
|
+
import { KtxExpectedError } from '../../errors.js';
|
|
4
5
|
/** @internal */
|
|
5
6
|
export function resolveKtxHomePath(path) {
|
|
6
7
|
if (path === '~') {
|
|
@@ -22,7 +23,16 @@ export function resolveKtxConfigReference(value, env) {
|
|
|
22
23
|
}
|
|
23
24
|
if (value.startsWith('file:')) {
|
|
24
25
|
const filePath = resolveKtxHomePath(value.slice('file:'.length).trim());
|
|
25
|
-
|
|
26
|
+
let fileValue;
|
|
27
|
+
try {
|
|
28
|
+
fileValue = readFileSync(filePath, 'utf8').trim();
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
// A `file:` secret whose target is missing or unreadable is a config
|
|
32
|
+
// problem (secrets/ is gitignored, so absent after a clone), not a ktx
|
|
33
|
+
// fault — classify it so it stays out of Error Tracking.
|
|
34
|
+
throw new KtxExpectedError(`Could not read the secret file referenced in ktx.yaml: ${filePath}`, { cause: error });
|
|
35
|
+
}
|
|
26
36
|
return fileValue.length > 0 ? fileValue : undefined;
|
|
27
37
|
}
|
|
28
38
|
const trimmed = value.trim();
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import type { ResolvedSemanticLayerSource, SemanticLayerQueryInput } from '../sl/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A ktx-daemon compute call failed. `inputRejected` is true when the daemon
|
|
4
|
+
* refused the request as invalid (a caller-driven outcome) rather than faulting;
|
|
5
|
+
* callers can promote those to an expected KtxQueryError while letting genuine
|
|
6
|
+
* daemon crashes reach Error Tracking. `detail` is the daemon's own message,
|
|
7
|
+
* unwrapped for surfacing to the caller.
|
|
8
|
+
*/
|
|
9
|
+
export declare class KtxDaemonComputeError extends Error {
|
|
10
|
+
readonly inputRejected: boolean;
|
|
11
|
+
readonly detail: string;
|
|
12
|
+
constructor(message: string, options: {
|
|
13
|
+
inputRejected: boolean;
|
|
14
|
+
detail: string;
|
|
15
|
+
cause?: unknown;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
2
18
|
interface KtxSemanticLayerComputeQueryResult {
|
|
3
19
|
sql: string;
|
|
4
20
|
dialect: string;
|
|
@@ -2,6 +2,41 @@ import { request as httpRequest } from 'node:http';
|
|
|
2
2
|
import { request as httpsRequest } from 'node:https';
|
|
3
3
|
import { URL } from 'node:url';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
|
+
// The daemon signals "the caller sent a well-formed request I refused as
|
|
6
|
+
// invalid" with a distinct subprocess exit code (INPUT_REJECTED_EXIT_CODE in
|
|
7
|
+
// python/ktx-daemon/__main__.py) or HTTP 400. Kept in sync across the boundary.
|
|
8
|
+
const DAEMON_INPUT_REJECTED_EXIT_CODE = 3;
|
|
9
|
+
const DAEMON_INPUT_REJECTED_HTTP_STATUS = 400;
|
|
10
|
+
/**
|
|
11
|
+
* A ktx-daemon compute call failed. `inputRejected` is true when the daemon
|
|
12
|
+
* refused the request as invalid (a caller-driven outcome) rather than faulting;
|
|
13
|
+
* callers can promote those to an expected KtxQueryError while letting genuine
|
|
14
|
+
* daemon crashes reach Error Tracking. `detail` is the daemon's own message,
|
|
15
|
+
* unwrapped for surfacing to the caller.
|
|
16
|
+
*/
|
|
17
|
+
export class KtxDaemonComputeError extends Error {
|
|
18
|
+
inputRejected;
|
|
19
|
+
detail;
|
|
20
|
+
constructor(message, options) {
|
|
21
|
+
super(message, options.cause !== undefined ? { cause: options.cause } : undefined);
|
|
22
|
+
this.name = 'KtxDaemonComputeError';
|
|
23
|
+
this.inputRejected = options.inputRejected;
|
|
24
|
+
this.detail = options.detail;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** The daemon reports HTTP failures as `{ "detail": "…" }`; fall back to the raw body. */
|
|
28
|
+
function daemonHttpErrorDetail(raw) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(raw);
|
|
31
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.detail === 'string') {
|
|
32
|
+
return parsed.detail;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Non-JSON body (e.g. a proxy error page) — surface it verbatim.
|
|
37
|
+
}
|
|
38
|
+
return raw;
|
|
39
|
+
}
|
|
5
40
|
function parseJsonObject(raw, subcommand) {
|
|
6
41
|
const parsed = JSON.parse(raw);
|
|
7
42
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
@@ -25,7 +60,11 @@ function runProcessJson(options) {
|
|
|
25
60
|
const stdoutText = Buffer.concat(stdout).toString('utf8').trim();
|
|
26
61
|
const stderrText = Buffer.concat(stderr).toString('utf8').trim();
|
|
27
62
|
if (code !== 0) {
|
|
28
|
-
|
|
63
|
+
const detail = stderrText || `exit code ${code}`;
|
|
64
|
+
reject(new KtxDaemonComputeError(`ktx-daemon ${subcommand} failed: ${detail}`, {
|
|
65
|
+
inputRejected: code === DAEMON_INPUT_REJECTED_EXIT_CODE,
|
|
66
|
+
detail,
|
|
67
|
+
}));
|
|
29
68
|
return;
|
|
30
69
|
}
|
|
31
70
|
try {
|
|
@@ -60,7 +99,10 @@ function postJson(baseUrl) {
|
|
|
60
99
|
const text = Buffer.concat(chunks).toString('utf8');
|
|
61
100
|
const statusCode = response.statusCode ?? 0;
|
|
62
101
|
if (statusCode < 200 || statusCode >= 300) {
|
|
63
|
-
reject(new
|
|
102
|
+
reject(new KtxDaemonComputeError(`ktx-daemon HTTP ${path} failed with ${statusCode}: ${text}`, {
|
|
103
|
+
inputRejected: statusCode === DAEMON_INPUT_REJECTED_HTTP_STATUS,
|
|
104
|
+
detail: daemonHttpErrorDetail(text),
|
|
105
|
+
}));
|
|
64
106
|
return;
|
|
65
107
|
}
|
|
66
108
|
try {
|
|
@@ -10,6 +10,7 @@ declare const LOOKER_DIALECT_TO_CONNECTION_TYPE: {
|
|
|
10
10
|
readonly sqlite: "SQLITE";
|
|
11
11
|
readonly sqlserver: "SQLSERVER";
|
|
12
12
|
readonly clickhouse: "CLICKHOUSE";
|
|
13
|
+
readonly awsathena: "ATHENA";
|
|
13
14
|
};
|
|
14
15
|
/** @internal */
|
|
15
16
|
export type LookerWarehouseTargetConnectionType = (typeof LOOKER_DIALECT_TO_CONNECTION_TYPE)[keyof typeof LOOKER_DIALECT_TO_CONNECTION_TYPE];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { warehouseTargetDialect } from '../../../connections/connection-type-dialect.js';
|
|
1
2
|
const LOOKER_DIALECT_TO_CONNECTION_TYPE = {
|
|
2
3
|
bigquery: 'BIGQUERY',
|
|
3
4
|
bigquery_standard_sql: 'BIGQUERY',
|
|
@@ -7,15 +8,7 @@ const LOOKER_DIALECT_TO_CONNECTION_TYPE = {
|
|
|
7
8
|
sqlite: 'SQLITE',
|
|
8
9
|
sqlserver: 'SQLSERVER',
|
|
9
10
|
clickhouse: 'CLICKHOUSE',
|
|
10
|
-
|
|
11
|
-
const SQLGLOT_DIALECT_BY_CONNECTION_TYPE = {
|
|
12
|
-
BIGQUERY: 'bigquery',
|
|
13
|
-
SNOWFLAKE: 'snowflake',
|
|
14
|
-
POSTGRESQL: 'postgres',
|
|
15
|
-
MYSQL: 'mysql',
|
|
16
|
-
SQLITE: 'sqlite',
|
|
17
|
-
SQLSERVER: 'tsql',
|
|
18
|
-
CLICKHOUSE: 'clickhouse',
|
|
11
|
+
awsathena: 'ATHENA',
|
|
19
12
|
};
|
|
20
13
|
export async function discoverLookerConnections(client) {
|
|
21
14
|
return client.listLookerConnections();
|
|
@@ -29,7 +22,7 @@ export function lookerDialectToConnectionType(dialect) {
|
|
|
29
22
|
}
|
|
30
23
|
/** @internal */
|
|
31
24
|
export function sqlglotDialectForConnectionType(connectionType) {
|
|
32
|
-
return
|
|
25
|
+
return warehouseTargetDialect(connectionType);
|
|
33
26
|
}
|
|
34
27
|
/** @internal */
|
|
35
28
|
export function validateLookerWarehouseTarget(connectionType) {
|
|
@@ -12,27 +12,18 @@ export declare const lookerPullConfigSchema: z.ZodObject<{
|
|
|
12
12
|
lookUpdatedSince: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
|
|
13
13
|
connectionMappings: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
14
14
|
connectionTypes: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
15
|
-
|
|
15
|
+
POSTGRESQL: "POSTGRESQL";
|
|
16
|
+
SQLITE: "SQLITE";
|
|
17
|
+
DUCKDB: "DUCKDB";
|
|
18
|
+
SQLSERVER: "SQLSERVER";
|
|
16
19
|
BIGQUERY: "BIGQUERY";
|
|
17
20
|
SNOWFLAKE: "SNOWFLAKE";
|
|
18
|
-
MYSQL: "MYSQL";
|
|
19
|
-
SQLSERVER: "SQLSERVER";
|
|
20
|
-
SQLITE: "SQLITE";
|
|
21
|
-
CLICKHOUSE: "CLICKHOUSE";
|
|
22
|
-
POSTGRESQL: "POSTGRESQL";
|
|
23
|
-
CENTRALREACH: "CENTRALREACH";
|
|
24
|
-
EPIC: "EPIC";
|
|
25
|
-
CERNER: "CERNER";
|
|
26
21
|
ATHENA: "ATHENA";
|
|
27
|
-
QUICKBOOKS: "QUICKBOOKS";
|
|
28
|
-
WORKDAY: "WORKDAY";
|
|
29
|
-
REST: "REST";
|
|
30
|
-
S3: "S3";
|
|
31
|
-
SLACK: "SLACK";
|
|
32
22
|
METABASE: "METABASE";
|
|
33
23
|
LOOKER: "LOOKER";
|
|
34
24
|
NOTION: "NOTION";
|
|
35
|
-
|
|
25
|
+
MYSQL: "MYSQL";
|
|
26
|
+
CLICKHOUSE: "CLICKHOUSE";
|
|
36
27
|
}>>>;
|
|
37
28
|
parsedTargetTables: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
38
29
|
ok: z.ZodLiteral<true>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { sigmaManifestSchema, stagedDataModelFileSchema, stagedWorkbookFileSchema, STAGED_FILES, } from './types.js';
|
|
4
|
+
async function walkStagedDir(stagedDir) {
|
|
5
|
+
let entries;
|
|
6
|
+
try {
|
|
7
|
+
entries = await readdir(stagedDir, { withFileTypes: true, recursive: true });
|
|
8
|
+
}
|
|
9
|
+
catch (err) {
|
|
10
|
+
if (err.code === 'ENOENT')
|
|
11
|
+
return [];
|
|
12
|
+
throw err;
|
|
13
|
+
}
|
|
14
|
+
const paths = [];
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
if (!entry.isFile())
|
|
17
|
+
continue;
|
|
18
|
+
const abs = join(entry.parentPath, entry.name);
|
|
19
|
+
paths.push(relative(stagedDir, abs).replace(/\\/g, '/'));
|
|
20
|
+
}
|
|
21
|
+
paths.sort();
|
|
22
|
+
return paths;
|
|
23
|
+
}
|
|
24
|
+
async function loadBundle(stagedDir) {
|
|
25
|
+
const allPaths = await walkStagedDir(stagedDir);
|
|
26
|
+
let manifest = null;
|
|
27
|
+
try {
|
|
28
|
+
const body = await readFile(join(stagedDir, STAGED_FILES.manifest), 'utf-8');
|
|
29
|
+
manifest = sigmaManifestSchema.parse(JSON.parse(body));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
manifest = null;
|
|
33
|
+
}
|
|
34
|
+
const dataModelsByPath = new Map();
|
|
35
|
+
const dmPrefix = `${STAGED_FILES.dataModelsDir}/`;
|
|
36
|
+
for (const path of allPaths) {
|
|
37
|
+
if (!path.startsWith(dmPrefix) || !path.endsWith('.json'))
|
|
38
|
+
continue;
|
|
39
|
+
try {
|
|
40
|
+
const body = await readFile(join(stagedDir, path), 'utf-8');
|
|
41
|
+
const parsed = stagedDataModelFileSchema.parse(JSON.parse(body));
|
|
42
|
+
dataModelsByPath.set(path, parsed);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Malformed file — skip.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const workbooksByPath = new Map();
|
|
49
|
+
const wbPrefix = `${STAGED_FILES.workbooksDir}/`;
|
|
50
|
+
for (const path of allPaths) {
|
|
51
|
+
if (!path.startsWith(wbPrefix) || !path.endsWith('.json'))
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
const body = await readFile(join(stagedDir, path), 'utf-8');
|
|
55
|
+
const parsed = stagedWorkbookFileSchema.parse(JSON.parse(body));
|
|
56
|
+
workbooksByPath.set(path, parsed);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Malformed file — skip.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { manifest, dataModelsByPath, workbooksByPath, allPaths };
|
|
63
|
+
}
|
|
64
|
+
/** Max data models per LLM work unit. Controls parallel processing granularity. */
|
|
65
|
+
const DATA_MODELS_PER_UNIT = 50;
|
|
66
|
+
/** Max workbooks per LLM work unit. Controls incremental re-sync granularity. */
|
|
67
|
+
const WORKBOOKS_PER_UNIT = 2000;
|
|
68
|
+
function emitBatches(paths, perUnit, unitKeyBase, labelBase, noun, allPaths) {
|
|
69
|
+
const batches = Math.ceil(paths.length / perUnit) || 0;
|
|
70
|
+
const units = [];
|
|
71
|
+
for (let i = 0; i < batches; i++) {
|
|
72
|
+
const batch = paths.slice(i * perUnit, (i + 1) * perUnit);
|
|
73
|
+
const rawFiles = [...batch].sort();
|
|
74
|
+
const rawFilesSet = new Set(rawFiles);
|
|
75
|
+
const suffix = batches > 1 ? `-${i}` : '';
|
|
76
|
+
units.push({
|
|
77
|
+
unitKey: `${unitKeyBase}${suffix}`,
|
|
78
|
+
displayLabel: batches > 1 ? `${labelBase} (${i + 1}/${batches})` : labelBase,
|
|
79
|
+
rawFiles,
|
|
80
|
+
peerFileIndex: allPaths.filter((p) => !rawFilesSet.has(p)).sort(),
|
|
81
|
+
dependencyPaths: [],
|
|
82
|
+
notes: `${batch.length} ${noun}${batch.length === 1 ? '' : 's'}`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return units;
|
|
86
|
+
}
|
|
87
|
+
function emitWorkUnits(bundle) {
|
|
88
|
+
if (!bundle.manifest)
|
|
89
|
+
return [];
|
|
90
|
+
const dmPaths = [...bundle.dataModelsByPath.keys()].sort();
|
|
91
|
+
const wbPaths = [...bundle.workbooksByPath.keys()].sort();
|
|
92
|
+
return [
|
|
93
|
+
...emitBatches(dmPaths, DATA_MODELS_PER_UNIT, 'sigma-data-models', 'Sigma: data models', 'data model', bundle.allPaths),
|
|
94
|
+
...emitBatches(wbPaths, WORKBOOKS_PER_UNIT, 'sigma-workbooks', 'Sigma: workbooks', 'workbook', bundle.allPaths),
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
export async function chunkSigmaStagedDir(stagedDir, opts = {}) {
|
|
98
|
+
const bundle = await loadBundle(stagedDir);
|
|
99
|
+
if (!bundle.manifest) {
|
|
100
|
+
return { workUnits: [] };
|
|
101
|
+
}
|
|
102
|
+
const firstRunUnits = emitWorkUnits(bundle);
|
|
103
|
+
if (!opts.diffSet) {
|
|
104
|
+
return { workUnits: firstRunUnits };
|
|
105
|
+
}
|
|
106
|
+
const touched = new Set([...opts.diffSet.added, ...opts.diffSet.modified]);
|
|
107
|
+
const kept = [];
|
|
108
|
+
for (const wu of firstRunUnits) {
|
|
109
|
+
const anyTouched = wu.rawFiles.some((p) => touched.has(p));
|
|
110
|
+
if (!anyTouched)
|
|
111
|
+
continue;
|
|
112
|
+
const changedFiles = wu.rawFiles.filter((p) => touched.has(p));
|
|
113
|
+
const unchangedFiles = wu.rawFiles.filter((p) => !touched.has(p));
|
|
114
|
+
const deps = new Set([...wu.dependencyPaths, ...unchangedFiles]);
|
|
115
|
+
kept.push({ ...wu, rawFiles: changedFiles.sort(), dependencyPaths: [...deps].sort() });
|
|
116
|
+
}
|
|
117
|
+
const eviction = opts.diffSet.deleted.length > 0 ? { deletedRawPaths: [...opts.diffSet.deleted].sort() } : undefined;
|
|
118
|
+
return { workUnits: kept, eviction };
|
|
119
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { FetchContext } from '../../types.js';
|
|
2
|
+
import type { SigmaPullConfig, WorkbookFilterInput } from './types.js';
|
|
3
|
+
export interface SigmaTestConnectionResult {
|
|
4
|
+
success: boolean;
|
|
5
|
+
message?: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Data model summary shape from GET /v2/dataModels list response. */
|
|
9
|
+
export interface SigmaDataModelSummary {
|
|
10
|
+
dataModelId: string;
|
|
11
|
+
dataModelUrlId: string;
|
|
12
|
+
name: string;
|
|
13
|
+
path: string;
|
|
14
|
+
latestVersion: number;
|
|
15
|
+
ownerId: string;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
updatedAt: string;
|
|
18
|
+
isArchived?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** Workbook summary shape from GET /v2/workbooks list response. */
|
|
21
|
+
export interface SigmaWorkbookSummary {
|
|
22
|
+
workbookId: string;
|
|
23
|
+
workbookUrlId: string;
|
|
24
|
+
name: string;
|
|
25
|
+
path: string;
|
|
26
|
+
latestVersion: number;
|
|
27
|
+
ownerId: string;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
updatedAt: string;
|
|
30
|
+
isArchived?: boolean;
|
|
31
|
+
description?: string;
|
|
32
|
+
}
|
|
33
|
+
/** Re-exported so callers can reference the type without importing from types.ts directly. */
|
|
34
|
+
export type { WorkbookFilterInput as ListWorkbooksOptions } from './types.js';
|
|
35
|
+
export interface SigmaRuntimeClient {
|
|
36
|
+
testConnection(): Promise<SigmaTestConnectionResult>;
|
|
37
|
+
listDataModels(): Promise<SigmaDataModelSummary[]>;
|
|
38
|
+
listWorkbooks(opts?: WorkbookFilterInput): Promise<SigmaWorkbookSummary[]>;
|
|
39
|
+
/** Returns the raw spec object from GET /v2/dataModels/{id}/spec. */
|
|
40
|
+
getDataModelSpec(dataModelId: string): Promise<unknown>;
|
|
41
|
+
cleanup(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
export interface SigmaClientFactory {
|
|
44
|
+
createClient(config: SigmaPullConfig, ctx: FetchContext): Promise<SigmaRuntimeClient> | SigmaRuntimeClient;
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ListWorkbooksOptions, SigmaDataModelSummary, SigmaRuntimeClient, SigmaTestConnectionResult, SigmaWorkbookSummary } from './client-port.js';
|
|
2
|
+
export interface SigmaClientRuntimeConfig {
|
|
3
|
+
apiUrl: string;
|
|
4
|
+
clientId: string;
|
|
5
|
+
clientSecret: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SigmaClientConfig {
|
|
8
|
+
maxRetries: number;
|
|
9
|
+
baseDelayMs: number;
|
|
10
|
+
maxDelayMs: number;
|
|
11
|
+
timeoutMs: number;
|
|
12
|
+
}
|
|
13
|
+
export declare const DEFAULT_SIGMA_CLIENT_CONFIG: SigmaClientConfig;
|
|
14
|
+
export declare class DefaultSigmaClient implements SigmaRuntimeClient {
|
|
15
|
+
private readonly runtimeConfig;
|
|
16
|
+
private readonly clientConfig;
|
|
17
|
+
private accessToken;
|
|
18
|
+
private refreshToken;
|
|
19
|
+
private tokenExpiresAt;
|
|
20
|
+
private tokenInflight;
|
|
21
|
+
constructor(runtimeConfig: SigmaClientRuntimeConfig, clientConfig?: SigmaClientConfig);
|
|
22
|
+
private get apiUrl();
|
|
23
|
+
private basicAuthHeader;
|
|
24
|
+
private fetchToken;
|
|
25
|
+
private ensureToken;
|
|
26
|
+
private request;
|
|
27
|
+
private paginateAll;
|
|
28
|
+
testConnection(): Promise<SigmaTestConnectionResult>;
|
|
29
|
+
listDataModels(): Promise<SigmaDataModelSummary[]>;
|
|
30
|
+
listWorkbooks(opts?: ListWorkbooksOptions): Promise<SigmaWorkbookSummary[]>;
|
|
31
|
+
getDataModelSpec(dataModelId: string): Promise<unknown>;
|
|
32
|
+
cleanup(): Promise<void>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
export const DEFAULT_SIGMA_CLIENT_CONFIG = {
|
|
2
|
+
maxRetries: 3,
|
|
3
|
+
baseDelayMs: 500,
|
|
4
|
+
maxDelayMs: 10_000,
|
|
5
|
+
timeoutMs: 30_000,
|
|
6
|
+
};
|
|
7
|
+
function isNonRetryable500(text) {
|
|
8
|
+
try {
|
|
9
|
+
const body = JSON.parse(text);
|
|
10
|
+
// service_error indicates a deterministic Sigma rejection (e.g. unsupported data
|
|
11
|
+
// source subtype). Retrying will not help, so throw immediately.
|
|
12
|
+
return body['code'] === 'service_error';
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class DefaultSigmaClient {
|
|
19
|
+
runtimeConfig;
|
|
20
|
+
clientConfig;
|
|
21
|
+
accessToken = null;
|
|
22
|
+
refreshToken = null;
|
|
23
|
+
tokenExpiresAt = 0;
|
|
24
|
+
tokenInflight = null;
|
|
25
|
+
constructor(runtimeConfig, clientConfig = DEFAULT_SIGMA_CLIENT_CONFIG) {
|
|
26
|
+
this.runtimeConfig = runtimeConfig;
|
|
27
|
+
this.clientConfig = clientConfig;
|
|
28
|
+
}
|
|
29
|
+
get apiUrl() {
|
|
30
|
+
return this.runtimeConfig.apiUrl.replace(/\/$/, '');
|
|
31
|
+
}
|
|
32
|
+
basicAuthHeader() {
|
|
33
|
+
const credentials = Buffer.from(`${this.runtimeConfig.clientId}:${this.runtimeConfig.clientSecret}`).toString('base64');
|
|
34
|
+
return `Basic ${credentials}`;
|
|
35
|
+
}
|
|
36
|
+
async fetchToken(body) {
|
|
37
|
+
const res = await fetch(`${this.apiUrl}/v2/auth/token`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
41
|
+
Authorization: this.basicAuthHeader(),
|
|
42
|
+
},
|
|
43
|
+
body: body.toString(),
|
|
44
|
+
signal: AbortSignal.timeout(this.clientConfig.timeoutMs),
|
|
45
|
+
});
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
const text = await res.text().catch(() => '');
|
|
48
|
+
throw new Error(`Sigma auth failed (${res.status}): ${text}`);
|
|
49
|
+
}
|
|
50
|
+
return res.json();
|
|
51
|
+
}
|
|
52
|
+
async ensureToken() {
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
// Refresh 60 s before expiry so in-flight requests don't get 401.
|
|
55
|
+
if (this.accessToken && now < this.tokenExpiresAt - 60_000) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (this.tokenInflight)
|
|
59
|
+
return this.tokenInflight;
|
|
60
|
+
const body = new URLSearchParams();
|
|
61
|
+
if (this.refreshToken) {
|
|
62
|
+
body.set('grant_type', 'refresh_token');
|
|
63
|
+
body.set('refresh_token', this.refreshToken);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
body.set('grant_type', 'client_credentials');
|
|
67
|
+
}
|
|
68
|
+
this.tokenInflight = this.fetchToken(body)
|
|
69
|
+
.then((data) => {
|
|
70
|
+
this.accessToken = data.access_token;
|
|
71
|
+
this.refreshToken = data.refresh_token ?? null;
|
|
72
|
+
this.tokenExpiresAt = Date.now() + data.expires_in * 1000;
|
|
73
|
+
})
|
|
74
|
+
.finally(() => {
|
|
75
|
+
this.tokenInflight = null;
|
|
76
|
+
});
|
|
77
|
+
return this.tokenInflight;
|
|
78
|
+
}
|
|
79
|
+
async request(path, query) {
|
|
80
|
+
await this.ensureToken();
|
|
81
|
+
const url = new URL(`${this.apiUrl}${path}`);
|
|
82
|
+
if (query) {
|
|
83
|
+
for (const [k, v] of Object.entries(query)) {
|
|
84
|
+
url.searchParams.set(k, v);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
let lastError = null;
|
|
88
|
+
for (let attempt = 0; attempt <= this.clientConfig.maxRetries; attempt++) {
|
|
89
|
+
if (attempt > 0) {
|
|
90
|
+
const delay = Math.min(this.clientConfig.baseDelayMs * 2 ** (attempt - 1), this.clientConfig.maxDelayMs);
|
|
91
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
92
|
+
}
|
|
93
|
+
const res = await fetch(url.toString(), {
|
|
94
|
+
headers: { Authorization: `Bearer ${this.accessToken}` },
|
|
95
|
+
signal: AbortSignal.timeout(this.clientConfig.timeoutMs),
|
|
96
|
+
});
|
|
97
|
+
if (res.status === 401) {
|
|
98
|
+
// Token rejected — force full re-auth and retry once.
|
|
99
|
+
this.accessToken = null;
|
|
100
|
+
this.refreshToken = null;
|
|
101
|
+
this.tokenExpiresAt = 0;
|
|
102
|
+
await this.ensureToken();
|
|
103
|
+
const retried = await fetch(url.toString(), {
|
|
104
|
+
headers: { Authorization: `Bearer ${this.accessToken}` },
|
|
105
|
+
signal: AbortSignal.timeout(this.clientConfig.timeoutMs),
|
|
106
|
+
});
|
|
107
|
+
if (!retried.ok) {
|
|
108
|
+
const text = await retried.text().catch(() => '');
|
|
109
|
+
throw new Error(`Sigma API error after token refresh (${retried.status}): ${text}`);
|
|
110
|
+
}
|
|
111
|
+
return retried.json();
|
|
112
|
+
}
|
|
113
|
+
if (res.status === 429 || res.status >= 500) {
|
|
114
|
+
const text = await res.text().catch(() => '');
|
|
115
|
+
lastError = new Error(`Sigma API error (${res.status}): ${text}`);
|
|
116
|
+
if (isNonRetryable500(text))
|
|
117
|
+
throw lastError;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!res.ok) {
|
|
121
|
+
const text = await res.text().catch(() => '');
|
|
122
|
+
throw new Error(`Sigma API error (${res.status}): ${text}`);
|
|
123
|
+
}
|
|
124
|
+
return res.json();
|
|
125
|
+
}
|
|
126
|
+
throw lastError ?? new Error('Sigma API request failed after retries');
|
|
127
|
+
}
|
|
128
|
+
async paginateAll(path, query = {}) {
|
|
129
|
+
const all = [];
|
|
130
|
+
let page = null;
|
|
131
|
+
do {
|
|
132
|
+
const q = { ...query, limit: '1000' };
|
|
133
|
+
if (page) {
|
|
134
|
+
q['page'] = page;
|
|
135
|
+
}
|
|
136
|
+
const res = await this.request(path, q);
|
|
137
|
+
all.push(...res.entries);
|
|
138
|
+
page = res.nextPage ?? null;
|
|
139
|
+
} while (page !== null);
|
|
140
|
+
return all;
|
|
141
|
+
}
|
|
142
|
+
async testConnection() {
|
|
143
|
+
try {
|
|
144
|
+
await this.ensureToken();
|
|
145
|
+
return { success: true };
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async listDataModels() {
|
|
152
|
+
return this.paginateAll('/v2/dataModels');
|
|
153
|
+
}
|
|
154
|
+
async listWorkbooks(opts = {}) {
|
|
155
|
+
const query = {};
|
|
156
|
+
if (!opts.includeExplorations)
|
|
157
|
+
query['excludeExplorations'] = 'true';
|
|
158
|
+
let results = await this.paginateAll('/v2/workbooks', query);
|
|
159
|
+
if (!opts.includeArchived) {
|
|
160
|
+
results = results.filter((wb) => !wb.isArchived);
|
|
161
|
+
}
|
|
162
|
+
if (opts.updatedSince) {
|
|
163
|
+
const since = new Date(opts.updatedSince).getTime();
|
|
164
|
+
results = results.filter((wb) => new Date(wb.updatedAt).getTime() >= since);
|
|
165
|
+
}
|
|
166
|
+
return results;
|
|
167
|
+
}
|
|
168
|
+
async getDataModelSpec(dataModelId) {
|
|
169
|
+
return this.request(`/v2/dataModels/${encodeURIComponent(dataModelId)}/spec`);
|
|
170
|
+
}
|
|
171
|
+
async cleanup() {
|
|
172
|
+
this.accessToken = null;
|
|
173
|
+
this.refreshToken = null;
|
|
174
|
+
this.tokenExpiresAt = 0;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function detectSigmaStagedDir(stagedDir: string): Promise<boolean>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { STAGED_FILES } from './types.js';
|
|
4
|
+
export async function detectSigmaStagedDir(stagedDir) {
|
|
5
|
+
try {
|
|
6
|
+
await stat(join(stagedDir, STAGED_FILES.manifest));
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
for (const subdir of [STAGED_FILES.dataModelsDir, STAGED_FILES.workbooksDir]) {
|
|
12
|
+
let entries;
|
|
13
|
+
try {
|
|
14
|
+
entries = await readdir(join(stagedDir, subdir));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (entries.some((name) => name.endsWith('.json')))
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FetchContext } from '../../types.js';
|
|
2
|
+
import type { SigmaClientFactory } from './client-port.js';
|
|
3
|
+
export interface SigmaFetchLogger {
|
|
4
|
+
log(message: string): void;
|
|
5
|
+
warn(message: string): void;
|
|
6
|
+
}
|
|
7
|
+
export interface FetchSigmaBundleParams {
|
|
8
|
+
pullConfig: unknown;
|
|
9
|
+
stagedDir: string;
|
|
10
|
+
ctx: FetchContext;
|
|
11
|
+
clientFactory: SigmaClientFactory;
|
|
12
|
+
logger?: SigmaFetchLogger;
|
|
13
|
+
}
|
|
14
|
+
export declare function fetchSigmaBundle({ pullConfig, stagedDir, ctx, clientFactory, logger, }: FetchSigmaBundleParams): Promise<void>;
|