@duckcodeailabs/dql-cli 1.0.2 → 1.0.4

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,9 @@
1
+ import { execSync } from 'node:child_process';
1
2
  import { createServer } from 'node:http';
2
3
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, watch, writeFileSync } from 'node:fs';
3
4
  import { dirname, extname, join, normalize, relative, resolve } from 'node:path';
4
5
  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, canonicalize, } from '@duckcodeailabs/dql-core';
6
+ import { loadSemanticLayerFromDir, resolveSemanticLayerAsync, Parser, buildLineageGraph, buildManifest, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryCompleteLineagePaths, LineageGraph, canonicalize, canonicalizeNotebook, diffDQL, diffNotebook, } from '@duckcodeailabs/dql-core';
6
7
  import { listBlockTemplates } from './block-templates.js';
7
8
  import { buildSemanticObjectDetail, buildSemanticTree, computeSyncDiff, loadSemanticImportManifest, performSemanticImport, previewSemanticImport, syncSemanticImport, } from './semantic-import.js';
8
9
  export async function startLocalServer(opts) {
@@ -219,7 +220,11 @@ export async function startLocalServer(opts) {
219
220
  return;
220
221
  }
221
222
  mkdirSync(dirname(absPath), { recursive: true });
222
- const toWrite = absPath.endsWith('.dql') ? canonicalizeSafe(content) : content;
223
+ const toWrite = absPath.endsWith('.dql')
224
+ ? canonicalizeSafe(content)
225
+ : absPath.endsWith('.dqlnb')
226
+ ? canonicalizeNotebookSafe(content)
227
+ : content;
223
228
  writeFileSync(absPath, toWrite, 'utf-8');
224
229
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
225
230
  res.end(serializeJSON({ ok: true }));
@@ -339,20 +344,37 @@ export async function startLocalServer(opts) {
339
344
  if (req.method === 'POST' && path === '/api/blocks/save-from-cell') {
340
345
  try {
341
346
  const body = await readJSON(req);
342
- const { name, domain, content, description, tags, metricRefs, template, } = body;
347
+ const { name, domain, owner, content, description, tags, metricRefs, template, } = body;
343
348
  if (!name || typeof name !== 'string' || !content || typeof content !== 'string') {
344
349
  res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
345
350
  res.end(serializeJSON({ error: 'name and content are required' }));
346
351
  return;
347
352
  }
353
+ const missing = [];
354
+ if (!owner || !owner.trim())
355
+ missing.push('owner');
356
+ if (!domain || !domain.trim())
357
+ missing.push('domain');
358
+ if (!description || !description.trim())
359
+ missing.push('description');
360
+ if (missing.length > 0) {
361
+ res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
362
+ res.end(serializeJSON({
363
+ error: `Block is missing required governance fields: ${missing.join(', ')}`,
364
+ missing,
365
+ }));
366
+ return;
367
+ }
348
368
  const created = createBlockArtifacts(projectRoot, {
349
369
  name,
350
370
  domain,
371
+ owner,
351
372
  content,
352
373
  description,
353
374
  tags,
354
375
  metricRefs,
355
376
  template,
377
+ gitMetadata: readGitMetadata(projectRoot),
356
378
  });
357
379
  res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8' });
358
380
  res.end(serializeJSON(created));
@@ -1279,9 +1301,21 @@ export async function startLocalServer(opts) {
1279
1301
  res.end(serializeJSON({ columns: [], rows: [], error: 'Missing SQL in request body.' }));
1280
1302
  return;
1281
1303
  }
1282
- const prepared = prepareLocalExecution(typeof body.sql === 'string' ? body.sql : '', isConnectionConfig(body.connection) ? body.connection : connection, projectRoot, projectConfig);
1304
+ const semantic = prepareSemanticSql(body.sql, semanticLayer);
1305
+ if (semantic.unresolvedRefs.length > 0) {
1306
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
1307
+ res.end(serializeJSON({
1308
+ columns: [],
1309
+ rows: [],
1310
+ error: `Unknown semantic reference${semantic.unresolvedRefs.length > 1 ? 's' : ''}: ${semantic.unresolvedRefs.join(', ')}`,
1311
+ code: 'semantic_ref',
1312
+ unresolvedRefs: semantic.unresolvedRefs,
1313
+ }));
1314
+ return;
1315
+ }
1316
+ const prepared = prepareLocalExecution(semantic.sql, isConnectionConfig(body.connection) ? body.connection : connection, projectRoot, projectConfig);
1283
1317
  const result = await executor.executeQuery(prepared.sql, Array.isArray(body.sqlParams) ? body.sqlParams : [], body.variables && typeof body.variables === 'object' ? body.variables : {}, prepared.connection);
1284
- const payload = serializeJSON(normalizeQueryResult(result));
1318
+ const payload = serializeJSON(normalizeQueryResult(result, semantic.semanticRefs));
1285
1319
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
1286
1320
  res.end(payload);
1287
1321
  }
@@ -1848,9 +1882,10 @@ export function formatLocalQueryRuntimeError(connection, error) {
1848
1882
  * Connector returns columns as ColumnMeta[] ({name,type,driverType}).
1849
1883
  * The notebook SPA expects columns as string[] (just names).
1850
1884
  */
1851
- function normalizeQueryResult(result) {
1885
+ function normalizeQueryResult(result, semanticRefs) {
1852
1886
  const rawCols = Array.isArray(result?.columns) ? result.columns : [];
1853
1887
  const columns = rawCols.map((c) => typeof c === 'string' ? c : typeof c?.name === 'string' ? c.name : String(c));
1888
+ const hasRefs = semanticRefs && (semanticRefs.metrics.length > 0 || semanticRefs.dimensions.length > 0);
1854
1889
  return {
1855
1890
  columns,
1856
1891
  rows: Array.isArray(result?.rows) ? result.rows : [],
@@ -1860,6 +1895,7 @@ function normalizeQueryResult(result) {
1860
1895
  : typeof result?.executionTime === 'number'
1861
1896
  ? result.executionTime
1862
1897
  : 0,
1898
+ ...(hasRefs ? { semanticRefs } : {}),
1863
1899
  };
1864
1900
  }
1865
1901
  export function serializeJSON(value) {
@@ -1941,6 +1977,25 @@ export function prepareLocalExecution(sql, connection, projectRoot, projectConfi
1941
1977
  connection: normalizedConnection,
1942
1978
  };
1943
1979
  }
1980
+ /**
1981
+ * Shared resolver for `@metric(name)` / `@dim(name)` refs in raw SQL.
1982
+ * Used by notebook SQL execution and Block Studio validation so both paths
1983
+ * behave identically. If the SQL has no refs, returns it unchanged.
1984
+ */
1985
+ export function prepareSemanticSql(sql, semanticLayer) {
1986
+ if (!hasSemanticRefs(sql)) {
1987
+ return { sql, semanticRefs: { metrics: [], dimensions: [] }, unresolvedRefs: [] };
1988
+ }
1989
+ const resolution = resolveSemanticRefs(sql, semanticLayer);
1990
+ return {
1991
+ sql: resolution.resolvedSql,
1992
+ semanticRefs: {
1993
+ metrics: resolution.resolvedMetrics,
1994
+ dimensions: resolution.resolvedDimensions,
1995
+ },
1996
+ unresolvedRefs: resolution.unresolvedRefs,
1997
+ };
1998
+ }
1944
1999
  export function normalizeProjectConnection(connection, projectRoot) {
1945
2000
  const normalized = { ...connection };
1946
2001
  if ((normalized.driver === 'file' || normalized.driver === 'duckdb') && normalized.filepath && normalized.filepath !== ':memory:' && !isAbsoluteLikePath(normalized.filepath)) {
@@ -2720,6 +2775,34 @@ function canonicalizeSafe(source) {
2720
2775
  return source;
2721
2776
  }
2722
2777
  }
2778
+ function canonicalizeNotebookSafe(source) {
2779
+ try {
2780
+ return canonicalizeNotebook(source);
2781
+ }
2782
+ catch {
2783
+ return source;
2784
+ }
2785
+ }
2786
+ export function readGitMetadata(projectRoot) {
2787
+ const run = (cmd) => execSync(cmd, { cwd: projectRoot, encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
2788
+ try {
2789
+ const commitSha = run('git rev-parse HEAD');
2790
+ let repo = null;
2791
+ let branch = null;
2792
+ try {
2793
+ repo = run('git config --get remote.origin.url') || null;
2794
+ }
2795
+ catch { /* no remote */ }
2796
+ try {
2797
+ branch = run('git rev-parse --abbrev-ref HEAD') || null;
2798
+ }
2799
+ catch { /* detached */ }
2800
+ return { commitSha, repo, branch };
2801
+ }
2802
+ catch {
2803
+ return null;
2804
+ }
2805
+ }
2723
2806
  export function createBlockArtifacts(projectRoot, options) {
2724
2807
  const slug = options.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'block';
2725
2808
  const safeDomain = (options.domain ?? '')
@@ -2736,23 +2819,27 @@ export function createBlockArtifacts(projectRoot, options) {
2736
2819
  const templateContent = options.template
2737
2820
  ? listBlockTemplates().find((template) => template.id === options.template)?.content
2738
2821
  : undefined;
2822
+ const relativePath = safeDomain ? `blocks/${safeDomain}/${slug}.dql` : `blocks/${slug}.dql`;
2739
2823
  const fileContent = canonicalizeSafe(normalizeBlockStudioContent({
2740
2824
  name: options.name,
2741
2825
  domain: safeDomain || 'uncategorized',
2826
+ owner: options.owner,
2742
2827
  description: options.description,
2743
2828
  tags: options.tags,
2744
2829
  content: options.content?.trim() || templateContent,
2745
2830
  }));
2746
2831
  writeFileSync(blockPath, fileContent, 'utf-8');
2747
- const relativePath = safeDomain ? `blocks/${safeDomain}/${slug}.dql` : `blocks/${slug}.dql`;
2748
2832
  const companionPath = writeBlockCompanionFile(projectRoot, {
2749
2833
  slug,
2750
2834
  name: options.name,
2751
2835
  domain: safeDomain || 'uncategorized',
2836
+ owner: options.owner,
2752
2837
  description: options.description,
2753
2838
  tags: options.tags,
2754
2839
  provider: 'dql',
2755
2840
  content: fileContent,
2841
+ gitMetadata: options.gitMetadata,
2842
+ gitPath: relativePath,
2756
2843
  });
2757
2844
  return {
2758
2845
  path: relativePath,
@@ -2928,6 +3015,17 @@ function writeBlockCompanionFile(projectRoot, options) {
2928
3015
  for (const table of options.lineage)
2929
3016
  lines.push(` - ${yamlScalar(table)}`);
2930
3017
  }
3018
+ if (options.gitMetadata || options.gitPath) {
3019
+ lines.push('git:');
3020
+ if (options.gitMetadata?.commitSha)
3021
+ lines.push(` commitSha: ${yamlScalar(options.gitMetadata.commitSha)}`);
3022
+ if (options.gitMetadata?.repo)
3023
+ lines.push(` repo: ${yamlScalar(options.gitMetadata.repo)}`);
3024
+ if (options.gitMetadata?.branch)
3025
+ lines.push(` branch: ${yamlScalar(options.gitMetadata.branch)}`);
3026
+ if (options.gitPath)
3027
+ lines.push(` path: ${yamlScalar(options.gitPath)}`);
3028
+ }
2931
3029
  lines.push('reviewStatus: draft');
2932
3030
  writeFileSync(companionPath, lines.join('\n') + '\n', 'utf-8');
2933
3031
  return relative(projectRoot, companionPath).replaceAll('\\', '/');
@@ -2968,6 +3066,7 @@ function normalizeBlockStudioContent(options) {
2968
3066
  return buildBlankBlockContent({
2969
3067
  name: options.name,
2970
3068
  domain: options.domain,
3069
+ owner: options.owner,
2971
3070
  description: options.description,
2972
3071
  tags: options.tags,
2973
3072
  sql: content || 'SELECT 1 AS value',
@@ -2979,7 +3078,7 @@ function buildBlankBlockContent(options) {
2979
3078
  ` domain = "${escapeDqlString(options.domain)}"`,
2980
3079
  ' type = "custom"',
2981
3080
  ` description = "${escapeDqlString(options.description?.trim() || options.name)}"`,
2982
- ' owner = ""',
3081
+ ` owner = "${escapeDqlString(options.owner?.trim() ?? '')}"`,
2983
3082
  ];
2984
3083
  lines.push(` tags = [${(options.tags ?? []).map((tag) => `"${escapeDqlString(tag)}"`).join(', ')}]`);
2985
3084
  lines.push('');
@@ -3255,13 +3354,49 @@ function ensureGitignoreEntry(projectRoot, pattern) {
3255
3354
  }
3256
3355
  async function readGitDiff(cwd, filePath) {
3257
3356
  const isRepo = await execGit(cwd, ['rev-parse', '--is-inside-work-tree']);
3258
- if (isRepo.code !== 0)
3259
- return { inRepo: false, diff: '' };
3357
+ if (isRepo.code !== 0) {
3358
+ return { inRepo: false, diff: '', before: null, after: null, diffReport: null };
3359
+ }
3260
3360
  if (!filePath) {
3261
3361
  const res = await execGit(cwd, ['diff', '--no-color']);
3262
- return { inRepo: true, diff: res.stdout };
3362
+ return { inRepo: true, diff: res.stdout, before: null, after: null, diffReport: null };
3363
+ }
3364
+ const isSemantic = filePath.endsWith('.dql') || filePath.endsWith('.dqlnb');
3365
+ const [diffRes, before, after] = await Promise.all([
3366
+ execGit(cwd, ['diff', '--no-color', '--', filePath]),
3367
+ isSemantic ? readHeadBlob(cwd, filePath) : Promise.resolve(null),
3368
+ isSemantic ? readWorkingCopy(join(cwd, filePath)) : Promise.resolve(null),
3369
+ ]);
3370
+ const diffReport = isSemantic ? computeSemanticDiff(filePath, before, after) : null;
3371
+ return { inRepo: true, diff: diffRes.stdout, before, after, diffReport };
3372
+ }
3373
+ async function readHeadBlob(cwd, filePath) {
3374
+ try {
3375
+ const res = await execGit(cwd, ['show', `HEAD:${filePath}`]);
3376
+ return res.code === 0 ? res.stdout : null;
3377
+ }
3378
+ catch {
3379
+ return null;
3380
+ }
3381
+ }
3382
+ async function readWorkingCopy(absPath) {
3383
+ try {
3384
+ return readFileSync(absPath, 'utf-8');
3385
+ }
3386
+ catch {
3387
+ return null;
3388
+ }
3389
+ }
3390
+ function computeSemanticDiff(filePath, before, after) {
3391
+ if (before === after)
3392
+ return null;
3393
+ try {
3394
+ return filePath.endsWith('.dqlnb')
3395
+ ? diffNotebook(before, after)
3396
+ : diffDQL(before ?? '', after ?? '');
3397
+ }
3398
+ catch {
3399
+ return null;
3263
3400
  }
3264
- const res = await execGit(cwd, ['diff', '--no-color', '--', filePath]);
3265
- return { inRepo: true, diff: res.stdout };
3266
3401
  }
3267
3402
  //# sourceMappingURL=local-runtime.js.map