@duckcodeailabs/dql-cli 0.8.10 → 0.8.11
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-Cxj__xjY.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 +184 -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/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, 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 {
|
|
@@ -2277,6 +2346,40 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
|
|
|
2277
2346
|
semanticRefs,
|
|
2278
2347
|
};
|
|
2279
2348
|
}
|
|
2349
|
+
function resolveCustomBlockSql(sql, semanticLayer) {
|
|
2350
|
+
if (!sql) {
|
|
2351
|
+
return {
|
|
2352
|
+
sql: null,
|
|
2353
|
+
diagnostics: [],
|
|
2354
|
+
semanticRefs: { metrics: [], dimensions: [], segments: [] },
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
const semanticRefs = extractBlockStudioSemanticReferences(sql);
|
|
2358
|
+
if (!hasSemanticRefs(sql)) {
|
|
2359
|
+
return { sql, diagnostics: [], semanticRefs };
|
|
2360
|
+
}
|
|
2361
|
+
const resolution = resolveSemanticRefs(sql, semanticLayer);
|
|
2362
|
+
if (resolution.unresolvedRefs.length > 0) {
|
|
2363
|
+
return {
|
|
2364
|
+
sql: null,
|
|
2365
|
+
diagnostics: resolution.unresolvedRefs.map((unresolved) => ({
|
|
2366
|
+
severity: 'error',
|
|
2367
|
+
code: 'semantic_ref',
|
|
2368
|
+
message: `Unknown semantic reference: ${unresolved}`,
|
|
2369
|
+
})),
|
|
2370
|
+
semanticRefs,
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
return {
|
|
2374
|
+
sql: resolution.resolvedSql,
|
|
2375
|
+
diagnostics: [],
|
|
2376
|
+
semanticRefs: {
|
|
2377
|
+
metrics: resolution.resolvedMetrics,
|
|
2378
|
+
dimensions: resolution.resolvedDimensions,
|
|
2379
|
+
segments: semanticRefs.segments,
|
|
2380
|
+
},
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2280
2383
|
export function validateBlockStudioSource(source, semanticLayer) {
|
|
2281
2384
|
const diagnostics = [];
|
|
2282
2385
|
const semanticConfig = parseSemanticBlockConfig(source);
|
|
@@ -2335,18 +2438,10 @@ export function validateBlockStudioSource(source, semanticLayer) {
|
|
|
2335
2438
|
}
|
|
2336
2439
|
}
|
|
2337
2440
|
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
|
-
}
|
|
2441
|
+
const resolvedCustomSql = resolveCustomBlockSql(executableSql, semanticLayer);
|
|
2442
|
+
semanticRefs = resolvedCustomSql.semanticRefs;
|
|
2443
|
+
diagnostics.push(...resolvedCustomSql.diagnostics);
|
|
2444
|
+
executableSql = resolvedCustomSql.sql;
|
|
2350
2445
|
}
|
|
2351
2446
|
const chartConfig = extractBlockStudioChartConfig(source);
|
|
2352
2447
|
if (!chartConfig) {
|
|
@@ -2857,51 +2952,87 @@ function buildNotebookTemplate(title, template) {
|
|
|
2857
2952
|
}
|
|
2858
2953
|
/** Build a lineage graph from the project's blocks and semantic layer. */
|
|
2859
2954
|
function buildProjectLineageGraph(projectRoot, semanticLayer) {
|
|
2860
|
-
const
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
|
2870
|
-
if (!entry.isFile() || extname(entry.name) !== '.dql')
|
|
2871
|
-
continue;
|
|
2872
|
-
try {
|
|
2873
|
-
const source = readFileSync(join(dirPath, entry.name), 'utf-8');
|
|
2874
|
-
const parser = new Parser(source, `${dir}/${entry.name}`);
|
|
2875
|
-
const ast = parser.parse();
|
|
2876
|
-
for (const stmt of ast.statements) {
|
|
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
|
-
}
|
|
2955
|
+
const manifestPath = join(projectRoot, 'dql-manifest.json');
|
|
2956
|
+
if (existsSync(manifestPath)) {
|
|
2957
|
+
try {
|
|
2958
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
2959
|
+
if (manifest.lineage?.nodes && manifest.lineage?.edges) {
|
|
2960
|
+
return LineageGraph.fromJSON({
|
|
2961
|
+
nodes: manifest.lineage.nodes,
|
|
2962
|
+
edges: manifest.lineage.edges,
|
|
2963
|
+
});
|
|
2891
2964
|
}
|
|
2892
|
-
catch { /* skip unparseable */ }
|
|
2893
2965
|
}
|
|
2966
|
+
catch {
|
|
2967
|
+
// Fall back to a live build.
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
const dbtManifestPath = resolveDbtManifestPath(projectRoot);
|
|
2971
|
+
try {
|
|
2972
|
+
const manifest = buildManifest({
|
|
2973
|
+
projectRoot,
|
|
2974
|
+
dbtManifestPath,
|
|
2975
|
+
});
|
|
2976
|
+
return LineageGraph.fromJSON({
|
|
2977
|
+
nodes: manifest.lineage.nodes,
|
|
2978
|
+
edges: manifest.lineage.edges,
|
|
2979
|
+
});
|
|
2894
2980
|
}
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2981
|
+
catch {
|
|
2982
|
+
const blocks = [];
|
|
2983
|
+
const metrics = [];
|
|
2984
|
+
const dimensions = [];
|
|
2985
|
+
const dirs = ['blocks', 'dashboards', 'workbooks'];
|
|
2986
|
+
for (const dir of dirs) {
|
|
2987
|
+
const dirPath = join(projectRoot, dir);
|
|
2988
|
+
if (!existsSync(dirPath))
|
|
2989
|
+
continue;
|
|
2990
|
+
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
|
2991
|
+
if (!entry.isFile() || extname(entry.name) !== '.dql')
|
|
2992
|
+
continue;
|
|
2993
|
+
try {
|
|
2994
|
+
const source = readFileSync(join(dirPath, entry.name), 'utf-8');
|
|
2995
|
+
const parser = new Parser(source, `${dir}/${entry.name}`);
|
|
2996
|
+
const ast = parser.parse();
|
|
2997
|
+
for (const stmt of ast.statements) {
|
|
2998
|
+
const block = stmt;
|
|
2999
|
+
if (block.kind !== 'BlockDecl')
|
|
3000
|
+
continue;
|
|
3001
|
+
blocks.push({
|
|
3002
|
+
name: block.name,
|
|
3003
|
+
sql: block.query?.rawSQL ?? '',
|
|
3004
|
+
domain: extractProp(block, 'domain'),
|
|
3005
|
+
owner: extractProp(block, 'owner'),
|
|
3006
|
+
status: extractProp(block, 'status'),
|
|
3007
|
+
blockType: block.blockType,
|
|
3008
|
+
metricRef: block.metricRef,
|
|
3009
|
+
chartType: extractVizChart(block),
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
catch { /* skip unparseable */ }
|
|
3014
|
+
}
|
|
2899
3015
|
}
|
|
2900
|
-
|
|
2901
|
-
|
|
3016
|
+
if (semanticLayer) {
|
|
3017
|
+
for (const m of semanticLayer.listMetrics()) {
|
|
3018
|
+
metrics.push({ name: m.name, table: m.table, domain: m.domain, type: m.type });
|
|
3019
|
+
}
|
|
3020
|
+
for (const d of semanticLayer.listDimensions()) {
|
|
3021
|
+
dimensions.push({ name: d.name, table: d.table });
|
|
3022
|
+
}
|
|
2902
3023
|
}
|
|
3024
|
+
return buildLineageGraph(blocks, metrics, dimensions);
|
|
2903
3025
|
}
|
|
2904
|
-
|
|
3026
|
+
}
|
|
3027
|
+
function resolveDbtManifestPath(projectRoot) {
|
|
3028
|
+
const candidate = join(projectRoot, 'target', 'manifest.json');
|
|
3029
|
+
return existsSync(candidate) ? candidate : undefined;
|
|
3030
|
+
}
|
|
3031
|
+
function resolveLineageNode(graph, rawNodeId) {
|
|
3032
|
+
if (graph.getNode(rawNodeId))
|
|
3033
|
+
return graph.getNode(rawNodeId);
|
|
3034
|
+
const result = queryLineage(graph, { focus: rawNodeId });
|
|
3035
|
+
return result.focalNode;
|
|
2905
3036
|
}
|
|
2906
3037
|
function extractProp(block, key) {
|
|
2907
3038
|
// Check direct AST fields first (parser puts domain, owner, type directly on the node)
|