@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.
@@ -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 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
- }
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 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
- }
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
- // 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 });
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
- for (const d of semanticLayer.listDimensions()) {
2901
- dimensions.push({ name: d.name, table: d.table });
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
- return buildLineageGraph(blocks, metrics, dimensions);
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)