@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.
- package/dist/assets/dql-notebook/assets/{index-DhWFlKju.js → index-dZVjj9xj.js} +91 -91
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/commands/diff.d.ts +1 -1
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +38 -9
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/fmt.d.ts.map +1 -1
- package/dist/commands/fmt.js +4 -2
- package/dist/commands/fmt.js.map +1 -1
- package/dist/commands/migrate.d.ts +1 -0
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +86 -0
- package/dist/commands/migrate.js.map +1 -1
- package/dist/git-service.d.ts +17 -0
- package/dist/git-service.d.ts.map +1 -0
- package/dist/git-service.js +54 -0
- package/dist/git-service.js.map +1 -0
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/local-runtime.d.ts +22 -0
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +148 -13
- package/dist/local-runtime.js.map +1 -1
- package/package.json +10 -9
- package/dist/package.json +0 -46
package/dist/local-runtime.js
CHANGED
|
@@ -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')
|
|
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
|
|
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
|
-
|
|
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
|