@duckcodeailabs/dql-cli 0.8.10 → 0.8.12
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/dist/assets/dql-notebook/assets/index-8LrBWmPy.js +564 -0
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/commands/lineage.d.ts +4 -0
- package/dist/commands/lineage.d.ts.map +1 -1
- package/dist/commands/lineage.js +123 -56
- package/dist/commands/lineage.js.map +1 -1
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +216 -53
- package/dist/local-runtime.js.map +1 -1
- package/dist/local-runtime.test.js +38 -0
- package/dist/local-runtime.test.js.map +1 -1
- package/dist/package.json +45 -0
- package/package.json +8 -8
- package/dist/assets/dql-notebook/assets/index-DIVTsVNu.js +0 -564
package/dist/local-runtime.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, watch, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, extname, join, normalize, relative, resolve } from 'node:path';
|
|
4
|
-
import { buildExecutionPlan, createWelcomeNotebook, deserializeNotebook, getConnectorFormSchemas, } from '@duckcodeailabs/dql-notebook';
|
|
5
|
-
import { loadSemanticLayerFromDir, resolveSemanticLayerAsync, Parser, buildLineageGraph, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, } from '@duckcodeailabs/dql-core';
|
|
4
|
+
import { buildExecutionPlan, createWelcomeNotebook, deserializeNotebook, getConnectorFormSchemas, hasSemanticRefs, resolveSemanticRefs, } from '@duckcodeailabs/dql-notebook';
|
|
5
|
+
import { loadSemanticLayerFromDir, resolveSemanticLayerAsync, Parser, buildLineageGraph, buildManifest, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryCompleteLineagePaths, LineageGraph, } from '@duckcodeailabs/dql-core';
|
|
6
6
|
import { listBlockTemplates } from './block-templates.js';
|
|
7
7
|
import { buildSemanticObjectDetail, buildSemanticTree, computeSyncDiff, loadSemanticImportManifest, performSemanticImport, previewSemanticImport, syncSemanticImport, } from './semantic-import.js';
|
|
8
8
|
export async function startLocalServer(opts) {
|
|
@@ -1463,6 +1463,75 @@ export async function startLocalServer(opts) {
|
|
|
1463
1463
|
}
|
|
1464
1464
|
return;
|
|
1465
1465
|
}
|
|
1466
|
+
if (req.method === 'GET' && path === '/api/lineage/search') {
|
|
1467
|
+
const term = url.searchParams.get('q') ?? '';
|
|
1468
|
+
try {
|
|
1469
|
+
const graph = buildProjectLineageGraph(projectRoot, semanticLayer);
|
|
1470
|
+
const result = queryLineage(graph, { search: term });
|
|
1471
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1472
|
+
res.end(serializeJSON({ matches: result.matches ?? [] }));
|
|
1473
|
+
}
|
|
1474
|
+
catch (error) {
|
|
1475
|
+
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1476
|
+
res.end(serializeJSON({ error: error instanceof Error ? error.message : String(error) }));
|
|
1477
|
+
}
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
if (req.method === 'GET' && path === '/api/lineage/query') {
|
|
1481
|
+
try {
|
|
1482
|
+
const graph = buildProjectLineageGraph(projectRoot, semanticLayer);
|
|
1483
|
+
const types = url.searchParams.get('types')
|
|
1484
|
+
?.split(',')
|
|
1485
|
+
.map((value) => value.trim())
|
|
1486
|
+
.filter(Boolean);
|
|
1487
|
+
const upstreamDepthParam = url.searchParams.get('upstreamDepth');
|
|
1488
|
+
const downstreamDepthParam = url.searchParams.get('downstreamDepth');
|
|
1489
|
+
const result = queryLineage(graph, {
|
|
1490
|
+
focus: url.searchParams.get('focus') ?? undefined,
|
|
1491
|
+
search: url.searchParams.get('search') ?? undefined,
|
|
1492
|
+
types,
|
|
1493
|
+
domain: url.searchParams.get('domain') ?? undefined,
|
|
1494
|
+
upstreamDepth: upstreamDepthParam ? Number(upstreamDepthParam) : undefined,
|
|
1495
|
+
downstreamDepth: downstreamDepthParam ? Number(downstreamDepthParam) : undefined,
|
|
1496
|
+
});
|
|
1497
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1498
|
+
res.end(serializeJSON(result));
|
|
1499
|
+
}
|
|
1500
|
+
catch (error) {
|
|
1501
|
+
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1502
|
+
res.end(serializeJSON({ error: error instanceof Error ? error.message : String(error) }));
|
|
1503
|
+
}
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
if (req.method === 'GET' && path.startsWith('/api/lineage/node/')) {
|
|
1507
|
+
const rawNodeId = decodeURIComponent(path.slice('/api/lineage/node/'.length));
|
|
1508
|
+
try {
|
|
1509
|
+
const graph = buildProjectLineageGraph(projectRoot, semanticLayer);
|
|
1510
|
+
const node = resolveLineageNode(graph, rawNodeId);
|
|
1511
|
+
if (!node) {
|
|
1512
|
+
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1513
|
+
res.end(serializeJSON({ error: `Lineage node "${rawNodeId}" not found` }));
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1517
|
+
res.end(serializeJSON({
|
|
1518
|
+
node,
|
|
1519
|
+
incoming: graph.getIncomingEdges(node.id).map((edge) => ({
|
|
1520
|
+
edge,
|
|
1521
|
+
node: graph.getNode(edge.source),
|
|
1522
|
+
})),
|
|
1523
|
+
outgoing: graph.getOutgoingEdges(node.id).map((edge) => ({
|
|
1524
|
+
edge,
|
|
1525
|
+
node: graph.getNode(edge.target),
|
|
1526
|
+
})),
|
|
1527
|
+
}));
|
|
1528
|
+
}
|
|
1529
|
+
catch (error) {
|
|
1530
|
+
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1531
|
+
res.end(serializeJSON({ error: error instanceof Error ? error.message : String(error) }));
|
|
1532
|
+
}
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1466
1535
|
if (req.method === 'GET' && path.startsWith('/api/lineage/domain/')) {
|
|
1467
1536
|
const domain = decodeURIComponent(path.slice('/api/lineage/domain/'.length));
|
|
1468
1537
|
try {
|
|
@@ -1527,6 +1596,27 @@ export async function startLocalServer(opts) {
|
|
|
1527
1596
|
}
|
|
1528
1597
|
return;
|
|
1529
1598
|
}
|
|
1599
|
+
if (req.method === 'GET' && path.startsWith('/api/lineage/paths/')) {
|
|
1600
|
+
const rawNodeId = decodeURIComponent(path.slice('/api/lineage/paths/'.length));
|
|
1601
|
+
try {
|
|
1602
|
+
const graph = buildProjectLineageGraph(projectRoot, semanticLayer);
|
|
1603
|
+
const maxDepth = Number(url.searchParams.get('maxDepth') ?? '10') || 10;
|
|
1604
|
+
const maxPaths = Number(url.searchParams.get('maxPaths') ?? '20') || 20;
|
|
1605
|
+
const result = queryCompleteLineagePaths(graph, rawNodeId, { maxDepth, maxPaths });
|
|
1606
|
+
if (!result) {
|
|
1607
|
+
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1608
|
+
res.end(serializeJSON({ error: `Node "${rawNodeId}" not found` }));
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1612
|
+
res.end(serializeJSON(result));
|
|
1613
|
+
}
|
|
1614
|
+
catch (error) {
|
|
1615
|
+
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
1616
|
+
res.end(serializeJSON({ error: error instanceof Error ? error.message : String(error) }));
|
|
1617
|
+
}
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1530
1620
|
if (req.method === 'GET' && path === '/api/lineage/trust-chain') {
|
|
1531
1621
|
const from = url.searchParams.get('from');
|
|
1532
1622
|
const to = url.searchParams.get('to');
|
|
@@ -2277,6 +2367,40 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
|
|
|
2277
2367
|
semanticRefs,
|
|
2278
2368
|
};
|
|
2279
2369
|
}
|
|
2370
|
+
function resolveCustomBlockSql(sql, semanticLayer) {
|
|
2371
|
+
if (!sql) {
|
|
2372
|
+
return {
|
|
2373
|
+
sql: null,
|
|
2374
|
+
diagnostics: [],
|
|
2375
|
+
semanticRefs: { metrics: [], dimensions: [], segments: [] },
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
const semanticRefs = extractBlockStudioSemanticReferences(sql);
|
|
2379
|
+
if (!hasSemanticRefs(sql)) {
|
|
2380
|
+
return { sql, diagnostics: [], semanticRefs };
|
|
2381
|
+
}
|
|
2382
|
+
const resolution = resolveSemanticRefs(sql, semanticLayer);
|
|
2383
|
+
if (resolution.unresolvedRefs.length > 0) {
|
|
2384
|
+
return {
|
|
2385
|
+
sql: null,
|
|
2386
|
+
diagnostics: resolution.unresolvedRefs.map((unresolved) => ({
|
|
2387
|
+
severity: 'error',
|
|
2388
|
+
code: 'semantic_ref',
|
|
2389
|
+
message: `Unknown semantic reference: ${unresolved}`,
|
|
2390
|
+
})),
|
|
2391
|
+
semanticRefs,
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
return {
|
|
2395
|
+
sql: resolution.resolvedSql,
|
|
2396
|
+
diagnostics: [],
|
|
2397
|
+
semanticRefs: {
|
|
2398
|
+
metrics: resolution.resolvedMetrics,
|
|
2399
|
+
dimensions: resolution.resolvedDimensions,
|
|
2400
|
+
segments: semanticRefs.segments,
|
|
2401
|
+
},
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2280
2404
|
export function validateBlockStudioSource(source, semanticLayer) {
|
|
2281
2405
|
const diagnostics = [];
|
|
2282
2406
|
const semanticConfig = parseSemanticBlockConfig(source);
|
|
@@ -2335,18 +2459,10 @@ export function validateBlockStudioSource(source, semanticLayer) {
|
|
|
2335
2459
|
}
|
|
2336
2460
|
}
|
|
2337
2461
|
else if (semanticLayer) {
|
|
2338
|
-
const
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
]);
|
|
2343
|
-
for (const unknown of refValidation.unknown) {
|
|
2344
|
-
diagnostics.push({
|
|
2345
|
-
severity: 'error',
|
|
2346
|
-
code: 'semantic_ref',
|
|
2347
|
-
message: `Unknown semantic reference: ${unknown}`,
|
|
2348
|
-
});
|
|
2349
|
-
}
|
|
2462
|
+
const resolvedCustomSql = resolveCustomBlockSql(executableSql, semanticLayer);
|
|
2463
|
+
semanticRefs = resolvedCustomSql.semanticRefs;
|
|
2464
|
+
diagnostics.push(...resolvedCustomSql.diagnostics);
|
|
2465
|
+
executableSql = resolvedCustomSql.sql;
|
|
2350
2466
|
}
|
|
2351
2467
|
const chartConfig = extractBlockStudioChartConfig(source);
|
|
2352
2468
|
if (!chartConfig) {
|
|
@@ -2856,52 +2972,99 @@ function buildNotebookTemplate(title, template) {
|
|
|
2856
2972
|
return JSON.stringify({ version: 1, title, cells }, null, 2);
|
|
2857
2973
|
}
|
|
2858
2974
|
/** Build a lineage graph from the project's blocks and semantic layer. */
|
|
2975
|
+
// Simple lineage graph cache: rebuilds at most every 5 seconds
|
|
2976
|
+
let _lineageCache = null;
|
|
2977
|
+
const LINEAGE_CACHE_TTL_MS = 5000;
|
|
2859
2978
|
function buildProjectLineageGraph(projectRoot, semanticLayer) {
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
const block = stmt;
|
|
2878
|
-
if (block.kind !== 'BlockDecl')
|
|
2879
|
-
continue;
|
|
2880
|
-
blocks.push({
|
|
2881
|
-
name: block.name,
|
|
2882
|
-
sql: block.query?.rawSQL ?? '',
|
|
2883
|
-
domain: extractProp(block, 'domain'),
|
|
2884
|
-
owner: extractProp(block, 'owner'),
|
|
2885
|
-
status: extractProp(block, 'status'),
|
|
2886
|
-
blockType: block.blockType,
|
|
2887
|
-
metricRef: block.metricRef,
|
|
2888
|
-
chartType: extractVizChart(block),
|
|
2889
|
-
});
|
|
2890
|
-
}
|
|
2979
|
+
if (_lineageCache && Date.now() - _lineageCache.builtAt < LINEAGE_CACHE_TTL_MS) {
|
|
2980
|
+
return _lineageCache.graph;
|
|
2981
|
+
}
|
|
2982
|
+
const graph = buildProjectLineageGraphUncached(projectRoot, semanticLayer);
|
|
2983
|
+
_lineageCache = { graph, builtAt: Date.now() };
|
|
2984
|
+
return graph;
|
|
2985
|
+
}
|
|
2986
|
+
function buildProjectLineageGraphUncached(projectRoot, semanticLayer) {
|
|
2987
|
+
const manifestPath = join(projectRoot, 'dql-manifest.json');
|
|
2988
|
+
if (existsSync(manifestPath)) {
|
|
2989
|
+
try {
|
|
2990
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
2991
|
+
if (manifest.lineage?.nodes && manifest.lineage?.edges) {
|
|
2992
|
+
return LineageGraph.fromJSON({
|
|
2993
|
+
nodes: manifest.lineage.nodes,
|
|
2994
|
+
edges: manifest.lineage.edges,
|
|
2995
|
+
});
|
|
2891
2996
|
}
|
|
2892
|
-
catch { /* skip unparseable */ }
|
|
2893
2997
|
}
|
|
2998
|
+
catch {
|
|
2999
|
+
// Fall back to a live build.
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
const dbtManifestPath = resolveDbtManifestPath(projectRoot);
|
|
3003
|
+
try {
|
|
3004
|
+
const manifest = buildManifest({
|
|
3005
|
+
projectRoot,
|
|
3006
|
+
dbtManifestPath,
|
|
3007
|
+
});
|
|
3008
|
+
return LineageGraph.fromJSON({
|
|
3009
|
+
nodes: manifest.lineage.nodes,
|
|
3010
|
+
edges: manifest.lineage.edges,
|
|
3011
|
+
});
|
|
2894
3012
|
}
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
3013
|
+
catch {
|
|
3014
|
+
const blocks = [];
|
|
3015
|
+
const metrics = [];
|
|
3016
|
+
const dimensions = [];
|
|
3017
|
+
const dirs = ['blocks', 'dashboards', 'workbooks'];
|
|
3018
|
+
for (const dir of dirs) {
|
|
3019
|
+
const dirPath = join(projectRoot, dir);
|
|
3020
|
+
if (!existsSync(dirPath))
|
|
3021
|
+
continue;
|
|
3022
|
+
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
|
3023
|
+
if (!entry.isFile() || extname(entry.name) !== '.dql')
|
|
3024
|
+
continue;
|
|
3025
|
+
try {
|
|
3026
|
+
const source = readFileSync(join(dirPath, entry.name), 'utf-8');
|
|
3027
|
+
const parser = new Parser(source, `${dir}/${entry.name}`);
|
|
3028
|
+
const ast = parser.parse();
|
|
3029
|
+
for (const stmt of ast.statements) {
|
|
3030
|
+
const block = stmt;
|
|
3031
|
+
if (block.kind !== 'BlockDecl')
|
|
3032
|
+
continue;
|
|
3033
|
+
blocks.push({
|
|
3034
|
+
name: block.name,
|
|
3035
|
+
sql: block.query?.rawSQL ?? '',
|
|
3036
|
+
domain: extractProp(block, 'domain'),
|
|
3037
|
+
owner: extractProp(block, 'owner'),
|
|
3038
|
+
status: extractProp(block, 'status'),
|
|
3039
|
+
blockType: block.blockType,
|
|
3040
|
+
metricRef: block.metricRef,
|
|
3041
|
+
chartType: extractVizChart(block),
|
|
3042
|
+
});
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
catch { /* skip unparseable */ }
|
|
3046
|
+
}
|
|
2899
3047
|
}
|
|
2900
|
-
|
|
2901
|
-
|
|
3048
|
+
if (semanticLayer) {
|
|
3049
|
+
for (const m of semanticLayer.listMetrics()) {
|
|
3050
|
+
metrics.push({ name: m.name, table: m.table, domain: m.domain, type: m.type });
|
|
3051
|
+
}
|
|
3052
|
+
for (const d of semanticLayer.listDimensions()) {
|
|
3053
|
+
dimensions.push({ name: d.name, table: d.table });
|
|
3054
|
+
}
|
|
2902
3055
|
}
|
|
3056
|
+
return buildLineageGraph(blocks, metrics, dimensions);
|
|
2903
3057
|
}
|
|
2904
|
-
|
|
3058
|
+
}
|
|
3059
|
+
function resolveDbtManifestPath(projectRoot) {
|
|
3060
|
+
const candidate = join(projectRoot, 'target', 'manifest.json');
|
|
3061
|
+
return existsSync(candidate) ? candidate : undefined;
|
|
3062
|
+
}
|
|
3063
|
+
function resolveLineageNode(graph, rawNodeId) {
|
|
3064
|
+
if (graph.getNode(rawNodeId))
|
|
3065
|
+
return graph.getNode(rawNodeId);
|
|
3066
|
+
const result = queryLineage(graph, { focus: rawNodeId });
|
|
3067
|
+
return result.focalNode;
|
|
2905
3068
|
}
|
|
2906
3069
|
function extractProp(block, key) {
|
|
2907
3070
|
// Check direct AST fields first (parser puts domain, owner, type directly on the node)
|