@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.
@@ -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 refValidation = semanticLayer.validateReferences([
2339
- ...semanticRefs.metrics,
2340
- ...semanticRefs.dimensions,
2341
- ...semanticRefs.segments,
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
- const blocks = [];
2861
- const metrics = [];
2862
- const dimensions = [];
2863
- // Scan .dql files
2864
- const dirs = ['blocks', 'dashboards', 'workbooks'];
2865
- for (const dir of dirs) {
2866
- const dirPath = join(projectRoot, dir);
2867
- if (!existsSync(dirPath))
2868
- continue;
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
- }
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
- // Load from semantic layer
2896
- if (semanticLayer) {
2897
- for (const m of semanticLayer.listMetrics()) {
2898
- metrics.push({ name: m.name, table: m.table, domain: m.domain, type: m.type });
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
- for (const d of semanticLayer.listDimensions()) {
2901
- dimensions.push({ name: d.name, table: d.table });
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
- return buildLineageGraph(blocks, metrics, dimensions);
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)