@datashow/super-visual-cli 0.0.0-stage → 0.1.0-rc.0
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/bin/supervisual.mjs +9 -0
- package/package.json +45 -4
- package/src/fabric-client.mjs +101 -0
- package/src/main.mjs +663 -0
- package/src/project-target.mjs +80 -0
- package/src/project.mjs +289 -0
- package/src/semantic-preview.mjs +528 -0
- package/templates/blank/README.md +26 -0
- package/templates/blank/contracts/page-set.yaml +78 -0
- package/templates/blank/contracts/report-source.json +30 -0
- package/templates/blank/contracts/semantic-model.yaml +39 -0
- package/templates/blank/contracts/widget-broker.json +33 -0
- package/templates/blank/deployment/CERTIFICATION.md +18 -0
- package/templates/blank/package.json +15 -0
- package/templates/blank/source/report-design.less +20 -0
- package/templates/blank/source/reportApplication.tsx +29 -0
- package/templates/blank/supervisual.yaml +24 -0
- package/templates/hello-world/README.md +29 -0
- package/templates/hello-world/contracts/page-set.yaml +74 -0
- package/templates/hello-world/contracts/report-source.json +45 -0
- package/templates/hello-world/contracts/semantic-model.yaml +32 -0
- package/templates/hello-world/contracts/widget-broker.json +37 -0
- package/templates/hello-world/deployment/CERTIFICATION.md +17 -0
- package/templates/hello-world/package.json +15 -0
- package/templates/hello-world/source/report-design.less +35 -0
- package/templates/hello-world/source/reportApplication.tsx +69 -0
- package/templates/hello-world/supervisual.yaml +24 -0
- package/templates/semantic-preview/index.html +14 -0
- package/templates/semantic-preview/src/main.tsx +172 -0
- package/README.md +0 -3
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { main } from "../src/main.mjs";
|
|
3
|
+
|
|
4
|
+
main().catch((error) => {
|
|
5
|
+
const json = process.argv.includes("--json");
|
|
6
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7
|
+
process.stderr.write(`${json ? JSON.stringify({ ok: false, error: message }) : `supervisual: ${message}`}\n`);
|
|
8
|
+
process.exitCode = 1;
|
|
9
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datashow/super-visual-cli",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-rc.0",
|
|
4
|
+
"description": "CLI for validating, compiling, packaging, verifying, and deploying Super Visual projects.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"supervisual": "bin/supervisual.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"templates"
|
|
13
|
+
],
|
|
14
|
+
"license": "Apache-2.0",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22.12.0"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/Datashow-Pty-Ltd/super-visual.git",
|
|
21
|
+
"directory": "packages/cli"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/Datashow-Pty-Ltd/super-visual#readme",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@datashow/super-visual-adapter-claude-dashboard": "0.1.0-rc.0",
|
|
29
|
+
"@datashow/super-visual-compiler": "0.1.0-rc.0",
|
|
30
|
+
"@datashow/super-visual-contracts": "0.1.0-rc.0",
|
|
31
|
+
"@datashow/super-visual-fabric": "0.1.0-rc.0",
|
|
32
|
+
"@fontsource/source-serif-4": "5.3.1",
|
|
33
|
+
"@nivo/bar": "0.99.0",
|
|
34
|
+
"@nivo/line": "0.99.0",
|
|
35
|
+
"@nivo/pie": "0.99.0",
|
|
36
|
+
"@nivo/sankey": "0.99.0",
|
|
37
|
+
"@nivo/tree": "0.99.0",
|
|
38
|
+
"commander": "15.0.0",
|
|
39
|
+
"d3": "7.9.0",
|
|
40
|
+
"less": "4.9.1",
|
|
41
|
+
"playwright": "1.64.1",
|
|
42
|
+
"react": "19.3.1",
|
|
43
|
+
"react-dom": "19.3.1",
|
|
44
|
+
"react-router-dom": "7.18.4",
|
|
45
|
+
"vite": "8.3.4"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
const run = promisify(execFile);
|
|
5
|
+
const base = "https://api.fabric.microsoft.com/v1";
|
|
6
|
+
const powerBiBase = "https://api.powerbi.com/v1.0/myorg";
|
|
7
|
+
const fabricOrigin = new URL(base).origin;
|
|
8
|
+
const powerBiOrigin = new URL(powerBiBase).origin;
|
|
9
|
+
|
|
10
|
+
function trustedUrl(pathOrUrl, serviceBase, expectedOrigin) {
|
|
11
|
+
const value = String(pathOrUrl);
|
|
12
|
+
const candidate = /^https:\/\//i.test(value)
|
|
13
|
+
? new URL(value)
|
|
14
|
+
: new URL(`${serviceBase}${value.startsWith("/") ? value : `/${value}`}`);
|
|
15
|
+
const regionalFabricOperation = expectedOrigin === fabricOrigin && candidate.hostname.endsWith(".analysis.windows.net");
|
|
16
|
+
if (candidate.protocol !== "https:" || (candidate.origin !== expectedOrigin && !regionalFabricOperation) || candidate.username || candidate.password) {
|
|
17
|
+
throw new Error(`Refusing untrusted service URL: ${candidate.origin}`);
|
|
18
|
+
}
|
|
19
|
+
return candidate;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const segment = (value) => encodeURIComponent(String(value));
|
|
23
|
+
|
|
24
|
+
export class FabricHttpClient {
|
|
25
|
+
constructor({ timeoutSeconds = 900, token, powerBiToken } = {}) { this.timeoutSeconds = timeoutSeconds; this.providedToken = token; this.providedPowerBiToken = powerBiToken; }
|
|
26
|
+
|
|
27
|
+
async accessToken(resource = "fabric") {
|
|
28
|
+
if (resource === "fabric" && this.providedToken) return this.providedToken;
|
|
29
|
+
if (resource === "powerbi" && this.providedPowerBiToken) return this.providedPowerBiToken;
|
|
30
|
+
const audience = resource === "fabric" ? "https://api.fabric.microsoft.com" : "https://analysis.windows.net/powerbi/api";
|
|
31
|
+
const { stdout } = await run("az", ["account", "get-access-token", "--resource", audience, "--query", "accessToken", "--output", "tsv"]);
|
|
32
|
+
const token = stdout.trim();
|
|
33
|
+
if (!token) throw new Error("Azure CLI did not return a Fabric access token; run az login first");
|
|
34
|
+
return token;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async request(pathOrUrl, method, skill, body) {
|
|
38
|
+
const url = trustedUrl(pathOrUrl, base, fabricOrigin);
|
|
39
|
+
// Body data is deliberately sent only to allowlisted Microsoft Fabric service hosts after contract validation.
|
|
40
|
+
const response = await fetch(url, { method, redirect: "error", headers: { Authorization: `Bearer ${await this.accessToken("fabric")}`, "x-ms-fabric-skill": skill, ...(body === undefined ? {} : { "Content-Type": "application/json" }) }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
|
|
41
|
+
return this.complete(response, skill);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async body(response) { const text = await response.text(); if (!text) return {}; try { return JSON.parse(text); } catch { return { text }; } }
|
|
45
|
+
async assert(response) { const body = await this.body(response); if (!response.ok) throw new Error(`Fabric HTTP ${response.status}: ${JSON.stringify(body)}`); return body; }
|
|
46
|
+
|
|
47
|
+
async complete(response, skill) {
|
|
48
|
+
if (response.status !== 202) return this.assert(response);
|
|
49
|
+
const operationId = response.headers.get("x-ms-operation-id") ?? response.headers.get("operation-id");
|
|
50
|
+
const locationHeader = response.headers.get("Location");
|
|
51
|
+
if (!locationHeader && !operationId) throw new Error("Fabric returned 202 without an operation location");
|
|
52
|
+
const location = trustedUrl(locationHeader ?? `${base}/operations/${segment(operationId)}`, base, fabricOrigin);
|
|
53
|
+
const started = Date.now();
|
|
54
|
+
while ((Date.now() - started) / 1000 < this.timeoutSeconds) {
|
|
55
|
+
await new Promise((resolve) => setTimeout(resolve, Math.max(1000, Number(response.headers.get("Retry-After") ?? 3) * 1000)));
|
|
56
|
+
const poll = await fetch(location, { redirect: "error", headers: { Authorization: `Bearer ${await this.accessToken("fabric")}`, "x-ms-fabric-skill": skill } });
|
|
57
|
+
const state = await this.assert(poll);
|
|
58
|
+
if (state.status === "Failed" || state.status === "Cancelled") throw new Error(`Fabric operation ${state.status}: ${JSON.stringify(state.error ?? state)}`);
|
|
59
|
+
if (state.status === "Succeeded") {
|
|
60
|
+
const resultUrl = new URL(location);
|
|
61
|
+
resultUrl.pathname = `${resultUrl.pathname.replace(/\/$/, "")}/result`;
|
|
62
|
+
resultUrl.search = "";
|
|
63
|
+
resultUrl.hash = "";
|
|
64
|
+
const result = await fetch(resultUrl, { redirect: "error", headers: { Authorization: `Bearer ${await this.accessToken("fabric")}`, "x-ms-fabric-skill": skill } });
|
|
65
|
+
if (result.status === 404) return state;
|
|
66
|
+
if (result.status === 400) {
|
|
67
|
+
const resultBody = await this.body(result);
|
|
68
|
+
if (resultBody.errorCode === "OperationHasNoResult") return state;
|
|
69
|
+
throw new Error(`Fabric HTTP 400: ${JSON.stringify(resultBody)}`);
|
|
70
|
+
}
|
|
71
|
+
return this.assert(result);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`Fabric operation exceeded ${this.timeoutSeconds}s`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
skill(type) { return type === "Report" ? "powerbi-report-management" : "semantic-model-authoring"; }
|
|
78
|
+
collection(type) { return type === "Report" ? "reports" : "semanticModels"; }
|
|
79
|
+
async listWorkspaces() { const output = []; let next = "/workspaces"; while (next) { const page = await this.request(next, "GET", "semantic-model-authoring"); output.push(...(page.value ?? [])); next = page.continuationUri; } return output; }
|
|
80
|
+
async getWorkspace(id) { return this.request(`/workspaces/${segment(id)}`, "GET", "semantic-model-authoring"); }
|
|
81
|
+
async listItems(workspaceId, type) {
|
|
82
|
+
const output = []; let next = `/workspaces/${segment(workspaceId)}/items?type=${segment(type)}`;
|
|
83
|
+
while (next) { const page = await this.request(next, "GET", this.skill(type)); output.push(...(page.value ?? [])); next = page.continuationUri; }
|
|
84
|
+
return output;
|
|
85
|
+
}
|
|
86
|
+
async createItem(workspaceId, type, displayName, format, parts) { return this.request(`/workspaces/${segment(workspaceId)}/${this.collection(type)}`, "POST", this.skill(type), { displayName, definition: { format, parts } }); }
|
|
87
|
+
async updateItem(workspaceId, type, id, format, parts) { await this.request(`/workspaces/${segment(workspaceId)}/${this.collection(type)}/${segment(id)}/updateDefinition`, "POST", this.skill(type), { definition: { format, parts } }); return { id }; }
|
|
88
|
+
async getDefinition(workspaceId, type, id, format) { const response = await this.request(`/workspaces/${segment(workspaceId)}/items/${segment(id)}/getDefinition?format=${segment(format)}`, "POST", this.skill(type), {}); return { format: response.definition?.format ?? format, parts: response.definition?.parts ?? [] }; }
|
|
89
|
+
async listConnections() { const output = []; let next = "/connections"; while (next) { const page = await this.request(next, "GET", "semantic-model-authoring"); output.push(...(page.value ?? [])); next = page.continuationUri; } return output; }
|
|
90
|
+
async getSemanticConnections(workspaceId, semanticModelId) { const response = await this.request(`/workspaces/${segment(workspaceId)}/items/${segment(semanticModelId)}/connections`, "GET", "semantic-model-authoring"); return response.value ?? []; }
|
|
91
|
+
async bindSemanticConnection(workspaceId, semanticModelId, connectionBinding) { return this.request(`/workspaces/${segment(workspaceId)}/semanticModels/${segment(semanticModelId)}/bindConnection`, "POST", "semantic-model-authoring", { connectionBinding }); }
|
|
92
|
+
async powerBi(pathName, method, body) {
|
|
93
|
+
const url = trustedUrl(pathName, powerBiBase, powerBiOrigin);
|
|
94
|
+
// DAX and deployment identifiers are deliberately sent only to the fixed Microsoft Power BI origin.
|
|
95
|
+
const response = await fetch(url, { method, redirect: "error", headers: { Authorization: `Bearer ${await this.accessToken("powerbi")}`, ...(body === undefined ? {} : { "Content-Type": "application/json" }) }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
|
|
96
|
+
return this.assert(response);
|
|
97
|
+
}
|
|
98
|
+
async triggerRefresh(workspaceId, semanticModelId) { return this.powerBi(`/groups/${segment(workspaceId)}/datasets/${segment(semanticModelId)}/refreshes`, "POST", { notifyOption: "NoNotification" }); }
|
|
99
|
+
async getRefreshHistory(workspaceId, semanticModelId) { const response = await this.powerBi(`/groups/${segment(workspaceId)}/datasets/${segment(semanticModelId)}/refreshes?$top=5`, "GET"); return response.value ?? []; }
|
|
100
|
+
async executeDax(workspaceId, semanticModelId, query) { const response = await this.powerBi(`/groups/${segment(workspaceId)}/datasets/${segment(semanticModelId)}/executeQueries`, "POST", { queries: [{ query }], serializerSettings: { includeNulls: true } }); return response.results?.[0]?.tables?.[0]?.rows ?? []; }
|
|
101
|
+
}
|