@salesforce/afv-skills 1.61.0 → 1.63.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.
Files changed (44) hide show
  1. package/package.json +3 -3
  2. package/skills/agentforce-architecture-analyze/README.md +2 -1
  3. package/skills/agentforce-d360-analyze/README.md +1 -1
  4. package/skills/agentforce-generate/README.md +1 -0
  5. package/skills/crm-analytics-wave-generate/SKILL.md +212 -0
  6. package/skills/crm-analytics-wave-generate/assets/dashboard-template.json +66 -0
  7. package/skills/crm-analytics-wave-generate/assets/wapp-meta-template.xml +10 -0
  8. package/skills/crm-analytics-wave-generate/assets/wdash-meta-template.xml +6 -0
  9. package/skills/crm-analytics-wave-generate/assets/wdpr-meta-template.xml +11 -0
  10. package/skills/crm-analytics-wave-generate/examples/industry-binding-dashboard.wdash +67 -0
  11. package/skills/crm-analytics-wave-generate/examples/opportunity-revenue-recipe.wdpr +132 -0
  12. package/skills/crm-analytics-wave-generate/examples/pipeline-by-industry-quarter.wdpr +236 -0
  13. package/skills/crm-analytics-wave-generate/examples/wdpr-skeleton.wdpr +62 -0
  14. package/skills/crm-analytics-wave-generate/references/dashboard-authoring.md +157 -0
  15. package/skills/crm-analytics-wave-generate/references/dataset-hygiene.md +27 -0
  16. package/skills/crm-analytics-wave-generate/references/external-data-upload.md +61 -0
  17. package/skills/crm-analytics-wave-generate/references/method-decision-tree.md +12 -0
  18. package/skills/crm-analytics-wave-generate/references/r3-node-schema.md +194 -0
  19. package/skills/crm-analytics-wave-generate/references/scripts-reference.md +17 -0
  20. package/skills/crm-analytics-wave-generate/references/ui-section-template.md +68 -0
  21. package/skills/crm-analytics-wave-generate/scripts/check_connector_error.sh +53 -0
  22. package/skills/crm-analytics-wave-generate/scripts/create_app.js +59 -0
  23. package/skills/crm-analytics-wave-generate/scripts/deploy_and_run_recipe.js +122 -0
  24. package/skills/crm-analytics-wave-generate/scripts/fetch_dataset_xmd.js +59 -0
  25. package/skills/crm-analytics-wave-generate/scripts/find_unused_dataset_fields.js +96 -0
  26. package/skills/crm-analytics-wave-generate/scripts/gen_synthetic_dataset.js +137 -0
  27. package/skills/crm-analytics-wave-generate/scripts/patch_t_oppo_view.js +99 -0
  28. package/skills/crm-analytics-wave-generate/scripts/probe_recipe.js +47 -0
  29. package/skills/crm-analytics-wave-generate/scripts/upload_dataset.js +119 -0
  30. package/skills/crm-analytics-wave-generate/scripts/verify_dashboard_steps.js +99 -0
  31. package/skills/crm-analytics-wave-generate/scripts/verify_synthetic_dataset.js +86 -0
  32. package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +60 -20
  33. package/skills/service-itsm-agentic-setup-incident-management/examples/output-templates.md +5 -1
  34. package/skills/service-itsm-agentic-setup-incident-management/references/incident-persona-psg.md +2 -2
  35. package/skills/service-itsm-agentic-setup-incident-management/references/incident-preferences.md +2 -2
  36. package/skills/service-itsm-agentic-setup-incident-management/references/major-incident-management.md +5 -5
  37. package/skills/service-itsm-agentic-setup-incident-management/references/service-mgmt-privilege.md +5 -5
  38. package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +67 -100
  39. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone-action.json +2 -2
  40. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +2 -2
  41. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +23 -22
  42. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +3 -3
  43. package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +182 -58
  44. package/skills/service-itsm-agentic-setup-incident-sla-configure/references/workflow.md +157 -88
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ // Given a dataset name, list every XMD field that no deployed dashboard SAQL references.
3
+ // Caveat: aggregateflex steps, multi-dataset SAQL, and archived app dashboards are out of scope.
4
+ // Usage: node scripts/find_unused_dataset_fields.js <alias> <datasetName>
5
+
6
+ const { execSync, execFileSync } = require("child_process");
7
+
8
+ function getOrgCredentials(alias) {
9
+ const result = execFileSync('sf', ['org', 'display', '-o', alias, '--json'], { encoding: "utf8" });
10
+ const json = JSON.parse(result);
11
+ return { accessToken: json.result.accessToken, instanceUrl: json.result.instanceUrl };
12
+ }
13
+
14
+ function apiFetch(instanceUrl, accessToken, apiPath) {
15
+ const url = `${instanceUrl}${apiPath}`;
16
+ const args = ["-s", "-H", `Authorization: Bearer ${accessToken}`, "-w", "\\nHTTP_STATUS:%{http_code}", url];
17
+ const raw = execSync(`curl ${args.map(a => `'${String(a).replace(/'/g, "'\\''")}'`).join(" ")}`, {
18
+ encoding: "utf8", maxBuffer: 20 * 1024 * 1024,
19
+ });
20
+ const parts = raw.split("\nHTTP_STATUS:");
21
+ const httpStatus = parseInt(parts[1] || "0", 10);
22
+ if (httpStatus < 200 || httpStatus >= 300) {
23
+ throw new Error(`HTTP ${httpStatus} on GET ${apiPath}: ${parts[0]}`);
24
+ }
25
+ return JSON.parse(parts[0]);
26
+ }
27
+
28
+ function decodeHtmlEntities(s) {
29
+ let prev = null;
30
+ while (prev !== s) {
31
+ prev = s;
32
+ s = s.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, '&');
33
+ }
34
+ return s;
35
+ }
36
+
37
+ const [,, alias, datasetName] = process.argv;
38
+ if (!alias || !datasetName) {
39
+ console.error("Usage: node find_unused_dataset_fields.js <alias> <datasetName>");
40
+ process.exit(1);
41
+ }
42
+
43
+ const { accessToken, instanceUrl } = getOrgCredentials(alias);
44
+
45
+ // Fetch dataset XMD
46
+ const datasets = apiFetch(instanceUrl, accessToken, "/services/data/v59.0/wave/datasets");
47
+ const dataset = (datasets.datasets || []).find(d => d.name === datasetName || d.developerName === datasetName);
48
+ if (!dataset) {
49
+ console.error(`Dataset "${datasetName}" not found.`);
50
+ process.exit(1);
51
+ }
52
+
53
+ const versionId = dataset.currentVersionId;
54
+ const xmdResult = apiFetch(instanceUrl, accessToken,
55
+ `/services/data/v59.0/wave/datasets/${dataset.id}/versions/${versionId}/xmd`
56
+ );
57
+ const xmd = xmdResult.xmd || xmdResult;
58
+
59
+ const allFields = [
60
+ ...(xmd.dimensions || []).map(d => d.fieldName || d.name),
61
+ ...(xmd.measures || []).map(m => m.fieldName || m.name),
62
+ ...(xmd.dates || []).map(d => d.fieldName || d.name),
63
+ ];
64
+
65
+ // Collect all SAQL from deployed dashboards
66
+ const dashboards = apiFetch(instanceUrl, accessToken, "/services/data/v59.0/wave/dashboards");
67
+ let allSaql = "";
68
+
69
+ for (const dash of (dashboards.dashboards || [])) {
70
+ try {
71
+ const detail = apiFetch(instanceUrl, accessToken, `/services/data/v59.0/wave/dashboards/${dash.id}`);
72
+ const steps = detail.state?.steps || {};
73
+ for (const step of Object.values(steps)) {
74
+ if (step.query) allSaql += " " + decodeHtmlEntities(step.query);
75
+ }
76
+ } catch {
77
+ // Skip inaccessible dashboards
78
+ }
79
+ }
80
+
81
+ // Check which fields are referenced
82
+ const unusedFields = allFields.filter(field => {
83
+ const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
84
+ return !new RegExp(escaped).test(allSaql);
85
+ });
86
+
87
+ const usedFields = allFields.filter(f => !unusedFields.includes(f));
88
+
89
+ console.log(JSON.stringify({
90
+ datasetName,
91
+ totalFields: allFields.length,
92
+ usedCount: usedFields.length,
93
+ unusedCount: unusedFields.length,
94
+ unusedFields,
95
+ note: "aggregateflex steps, multi-dataset SAQL, and archived app dashboards are not scanned",
96
+ }, null, 2));
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ // Schema-driven synthetic data generator. Reads live XMD, classifies each column by name pattern
3
+ // (*Id → id, *Industry/*Type/*Status → category, *Year/*Month → date, *Amount/*Revenue/*Count → numeric),
4
+ // renames dotted fields to underscored, writes CSV + Wave-compatible metadata.json.
5
+ // Default output dataset: <sourceDatasetName>_Synthetic
6
+ // Usage: node scripts/gen_synthetic_dataset.js <alias> <sourceDatasetName>
7
+
8
+ const { execSync, execFileSync } = require("child_process");
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+
12
+ function getOrgCredentials(alias) {
13
+ const result = execFileSync('sf', ['org', 'display', '-o', alias, '--json'], { encoding: "utf8" });
14
+ const json = JSON.parse(result);
15
+ return { accessToken: json.result.accessToken, instanceUrl: json.result.instanceUrl };
16
+ }
17
+
18
+ function apiFetch(instanceUrl, accessToken, apiPath) {
19
+ const url = `${instanceUrl}${apiPath}`;
20
+ const args = ["-s", "-H", `Authorization: Bearer ${accessToken}`, "-w", "\\nHTTP_STATUS:%{http_code}", url];
21
+ const raw = execSync(`curl ${args.map(a => `'${String(a).replace(/'/g, "'\\''")}'`).join(" ")}`, {
22
+ encoding: "utf8", maxBuffer: 10 * 1024 * 1024,
23
+ });
24
+ const parts = raw.split("\nHTTP_STATUS:");
25
+ const httpStatus = parseInt(parts[1] || "0", 10);
26
+ if (httpStatus < 200 || httpStatus >= 300) {
27
+ throw new Error(`HTTP ${httpStatus} on GET ${apiPath}: ${parts[0]}`);
28
+ }
29
+ return JSON.parse(parts[0]);
30
+ }
31
+
32
+ function classifyField(name) {
33
+ const n = name.toLowerCase();
34
+ if (/id$/.test(n)) return "id";
35
+ if (/(industry|type|status|stage|source|region|category|priority)/.test(n)) return "category";
36
+ if (/(year|month|quarter)/.test(n)) return "date_part";
37
+ if (/(amount|revenue|count|qty|quantity|size|value|total|sum)/.test(n)) return "numeric";
38
+ return "text";
39
+ }
40
+
41
+ function generateValue(fieldName, type, rowIndex) {
42
+ switch (type) {
43
+ case "id": return `00Q${String(rowIndex).padStart(15, "0")}`;
44
+ case "category": {
45
+ const categories = ["Tech", "Finance", "Healthcare", "Retail", "Manufacturing"];
46
+ return categories[rowIndex % categories.length];
47
+ }
48
+ case "date_part": return String(2020 + (rowIndex % 5));
49
+ case "numeric": return String(Math.round(10000 + rowIndex * 1337.7));
50
+ default: return `Value_${rowIndex % 10}`;
51
+ }
52
+ }
53
+
54
+ const [,, alias, sourceDatasetName] = process.argv;
55
+ if (!alias || !sourceDatasetName) {
56
+ console.error("Usage: node gen_synthetic_dataset.js <alias> <sourceDatasetName>");
57
+ process.exit(1);
58
+ }
59
+
60
+ const { accessToken, instanceUrl } = getOrgCredentials(alias);
61
+
62
+ // Fetch dataset and XMD
63
+ const datasets = apiFetch(instanceUrl, accessToken, "/services/data/v59.0/wave/datasets");
64
+ const dataset = (datasets.datasets || []).find(d => d.name === sourceDatasetName || d.developerName === sourceDatasetName);
65
+ if (!dataset) {
66
+ console.error(`Dataset "${sourceDatasetName}" not found.`);
67
+ process.exit(1);
68
+ }
69
+
70
+ const versionId = dataset.currentVersionId;
71
+ const xmdResult = apiFetch(instanceUrl, accessToken,
72
+ `/services/data/v59.0/wave/datasets/${dataset.id}/versions/${versionId}/xmd`
73
+ );
74
+ const xmd = xmdResult.xmd || xmdResult;
75
+
76
+ // Collect fields from XMD
77
+ const fields = [];
78
+ const dimensions = xmd.dimensions || [];
79
+ const measures = xmd.measures || [];
80
+ const dates = xmd.dates || [];
81
+
82
+ dimensions.forEach(d => fields.push({ name: d.fieldName || d.name, type: "text" }));
83
+ measures.forEach(m => fields.push({ name: m.fieldName || m.name, type: "numeric" }));
84
+ dates.forEach(d => fields.push({ name: d.fieldName || d.name, type: "date_part" }));
85
+
86
+ if (fields.length === 0) {
87
+ console.error("No fields found in XMD. Cannot generate synthetic data.");
88
+ process.exit(1);
89
+ }
90
+
91
+ // Rename dotted field names to underscored
92
+ const sanitized = fields.map(f => ({
93
+ ...f,
94
+ name: f.name.replace(/\./g, "_"),
95
+ originalName: f.name,
96
+ }));
97
+
98
+ const ROW_COUNT = 50;
99
+ const headers = sanitized.map(f => f.name);
100
+ const rows = [headers.join(",")];
101
+ for (let i = 0; i < ROW_COUNT; i++) {
102
+ const row = sanitized.map(f => {
103
+ const val = generateValue(f.name, f.type === "text" ? classifyField(f.name) : f.type, i);
104
+ return val.includes(",") ? `"${val}"` : val;
105
+ });
106
+ rows.push(row.join(","));
107
+ }
108
+
109
+ const outputDir = path.join(process.cwd(), "synthetic_data");
110
+ fs.mkdirSync(outputDir, { recursive: true });
111
+
112
+ const csvPath = path.join(outputDir, `${sourceDatasetName}_Synthetic.csv`);
113
+ const metaPath = path.join(outputDir, `${sourceDatasetName}_Synthetic_metadata.json`);
114
+
115
+ fs.writeFileSync(csvPath, rows.join("\n"), "utf8");
116
+
117
+ // Wave-compatible metadata.json
118
+ const metadata = {
119
+ fileFormat: { charsetName: "UTF-8", fieldsDelimitedBy: ",", linesTerminatedBy: "\n" },
120
+ objects: [{
121
+ connector: "CSV",
122
+ fullyQualifiedName: `${sourceDatasetName}_Synthetic`,
123
+ label: `${sourceDatasetName} Synthetic`,
124
+ name: `${sourceDatasetName}_Synthetic`,
125
+ fields: sanitized.map(f => ({
126
+ fullyQualifiedName: f.name,
127
+ name: f.name,
128
+ label: f.name,
129
+ type: f.type === "numeric" ? "Numeric" : "Text",
130
+ ...(f.type === "numeric" ? { precision: 18, scale: 2, defaultValue: "0" } : { length: 255 }),
131
+ })),
132
+ }],
133
+ };
134
+
135
+ fs.writeFileSync(metaPath, JSON.stringify(metadata, null, 2), "utf8");
136
+
137
+ console.log(JSON.stringify({ csvPath, metaPath, rowCount: ROW_COUNT, fieldCount: fields.length }));
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ // Full-cycle PATCH template: GET dashboard state → decodeAll every step query → merge dark theme → PATCH.
3
+ // Use as the canonical pattern for future dashboard PATCH scripts.
4
+ // Edit DASH_ID and THEME constants before running.
5
+ // Usage: node scripts/patch_t_oppo_view.js <alias>
6
+
7
+ const { execSync, execFileSync } = require("child_process");
8
+
9
+ // ── Edit these constants before running ──────────────────────────────────────
10
+ const DASH_ID = "0FK000000000000AAA"; // Wave Dashboard 18-char ID
11
+
12
+ const THEME = {
13
+ layoutStyle: { backgroundColor: "#0d1117", cellSpacingX: 8, cellSpacingY: 8, gutterColor: "#0d1117" },
14
+ widgetStyle: { backgroundColor: "#161b22", borderColor: "#30363d", borderEdges: ["all"], borderRadius: 8, borderWidth: 1 },
15
+ };
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+
18
+ function getOrgCredentials(alias) {
19
+ const result = execFileSync('sf', ['org', 'display', '-o', alias, '--json'], { encoding: "utf8" });
20
+ const json = JSON.parse(result);
21
+ return { accessToken: json.result.accessToken, instanceUrl: json.result.instanceUrl };
22
+ }
23
+
24
+ function apiFetch(instanceUrl, accessToken, apiPath, method = "GET", body = null) {
25
+ const url = `${instanceUrl}${apiPath}`;
26
+ const args = [
27
+ "-s",
28
+ "-H", `Authorization: Bearer ${accessToken}`,
29
+ "-H", "Content-Type: application/json",
30
+ "-X", method,
31
+ "-w", "\\nHTTP_STATUS:%{http_code}",
32
+ ];
33
+ if (body !== null) args.push("-d", JSON.stringify(body));
34
+ args.push(url);
35
+ const raw = execSync(`curl ${args.map(a => `'${String(a).replace(/'/g, "'\\''")}'`).join(" ")}`, {
36
+ encoding: "utf8", maxBuffer: 20 * 1024 * 1024,
37
+ });
38
+ const parts = raw.split("\nHTTP_STATUS:");
39
+ const httpStatus = parseInt(parts[1] || "0", 10);
40
+ if (httpStatus < 200 || httpStatus >= 300) {
41
+ throw new Error(`HTTP ${httpStatus} on ${method} ${apiPath}: ${parts[0]}`);
42
+ }
43
+ return JSON.parse(parts[0]);
44
+ }
45
+
46
+ // Always decode all HTML-entity encoding before any mutation + PATCH.
47
+ // Doing a GET → PATCH without decoding re-encodes SAQL → double-encoded entities → errorCode 119.
48
+ function decodeAll(s) {
49
+ let prev = null;
50
+ while (prev !== s) {
51
+ prev = s;
52
+ s = s.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, '&');
53
+ }
54
+ return s;
55
+ }
56
+
57
+ const [,, alias] = process.argv;
58
+ if (!alias) {
59
+ console.error("Usage: node patch_t_oppo_view.js <alias>");
60
+ console.error("Edit DASH_ID and THEME constants at the top of this file before running.");
61
+ process.exit(1);
62
+ }
63
+
64
+ const { accessToken, instanceUrl } = getOrgCredentials(alias);
65
+
66
+ // Step 1: GET current dashboard state
67
+ const current = apiFetch(instanceUrl, accessToken, `/services/data/v59.0/wave/dashboards/${DASH_ID}`);
68
+ const state = JSON.parse(JSON.stringify(current.state));
69
+
70
+ // Step 2: Decode all step queries
71
+ const steps = state.steps || {};
72
+ for (const stepName of Object.keys(steps)) {
73
+ if (steps[stepName].query) {
74
+ steps[stepName].query = decodeAll(steps[stepName].query);
75
+ }
76
+ }
77
+
78
+ // Step 3: Merge dark theme into layout
79
+ const layouts = state.gridLayouts || [];
80
+ for (const layout of layouts) {
81
+ layout.style = { ...(layout.style || {}), ...THEME.layoutStyle };
82
+ const pages = layout.pages || [];
83
+ for (const page of pages) {
84
+ const widgets = page.widgets || [];
85
+ for (const widget of widgets) {
86
+ widget.widgetStyle = { ...(widget.widgetStyle || {}), ...THEME.widgetStyle };
87
+ }
88
+ }
89
+ }
90
+
91
+ // Step 4: PATCH with decoded + themed state
92
+ const patched = apiFetch(instanceUrl, accessToken, `/services/data/v59.0/wave/dashboards/${DASH_ID}`, "PATCH", { state });
93
+
94
+ console.log(JSON.stringify({
95
+ status: "patched",
96
+ dashboardId: patched.id,
97
+ label: patched.label,
98
+ stepsDecoded: Object.keys(steps).length,
99
+ }));
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ // Fetch a recipe with ?format=R3 and print compile status + targetDataflowId.
3
+ // Usage: node scripts/probe_recipe.js <alias> <recipeId>
4
+
5
+ const { execSync, execFileSync } = require("child_process");
6
+
7
+ function getOrgCredentials(alias) {
8
+ const result = execFileSync('sf', ['org', 'display', '-o', alias, '--json'], { encoding: "utf8" });
9
+ const json = JSON.parse(result);
10
+ return { accessToken: json.result.accessToken, instanceUrl: json.result.instanceUrl };
11
+ }
12
+
13
+ function apiFetch(instanceUrl, accessToken, path) {
14
+ const url = `${instanceUrl}${path}`;
15
+ const args = [
16
+ "-s",
17
+ "-H", `Authorization: Bearer ${accessToken}`,
18
+ "-H", "Content-Type: application/json",
19
+ "-w", "\\nHTTP_STATUS:%{http_code}",
20
+ url,
21
+ ];
22
+ const raw = execSync(`curl ${args.map(a => `'${String(a).replace(/'/g, "'\\''")}'`).join(" ")}`, { encoding: "utf8" });
23
+ const parts = raw.split("\nHTTP_STATUS:");
24
+ const httpStatus = parseInt(parts[1] || "0", 10);
25
+ if (httpStatus < 200 || httpStatus >= 300) {
26
+ throw new Error(`HTTP ${httpStatus} on GET ${path}: ${parts[0]}`);
27
+ }
28
+ return JSON.parse(parts[0]);
29
+ }
30
+
31
+ const [,, alias, recipeId] = process.argv;
32
+ if (!alias || !recipeId) {
33
+ console.error("Usage: node probe_recipe.js <alias> <recipeId>");
34
+ process.exit(1);
35
+ }
36
+
37
+ const { accessToken, instanceUrl } = getOrgCredentials(alias);
38
+ const recipe = apiFetch(instanceUrl, accessToken, `/services/data/v59.0/wave/recipes/${recipeId}?format=R3`);
39
+
40
+ console.log(JSON.stringify({
41
+ id: recipe.id,
42
+ name: recipe.name,
43
+ label: recipe.label,
44
+ status: recipe.status,
45
+ targetDataflowId: recipe.targetDataflowId || null,
46
+ folderId: recipe.folder?.id || null,
47
+ }, null, 2));
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ // Upload a CSV to a new or existing dataset via InsightsExternalData API.
3
+ // Polls InsightsExternalDataPart and InsightsExternalData until ingestion is terminal.
4
+ // Usage: node scripts/upload_dataset.js <alias> <csvPath> <metadataJsonPath> <datasetAlias> <folderId>
5
+
6
+ const { execSync, execFileSync } = require("child_process");
7
+ const fs = require("fs");
8
+ const path = require("path");
9
+
10
+ const TERMINAL_STATUSES = new Set(["Completed", "CompletedWithWarnings", "Failed", "NotProcessed"]);
11
+ const POLL_INTERVAL_MS = 8000;
12
+ const MAX_WAIT_MS = 15 * 60 * 1000;
13
+
14
+ function getOrgCredentials(alias) {
15
+ const result = execFileSync('sf', ['org', 'display', '-o', alias, '--json'], { encoding: "utf8" });
16
+ const json = JSON.parse(result);
17
+ return { accessToken: json.result.accessToken, instanceUrl: json.result.instanceUrl };
18
+ }
19
+
20
+ function soqlQuery(instanceUrl, accessToken, soql) {
21
+ const encoded = encodeURIComponent(soql);
22
+ const args = [
23
+ "-s",
24
+ "-H", `Authorization: Bearer ${accessToken}`,
25
+ "-w", "\\nHTTP_STATUS:%{http_code}",
26
+ `${instanceUrl}/services/data/v59.0/query?q=${encoded}`,
27
+ ];
28
+ const raw = execSync(`curl ${args.map(a => `'${String(a).replace(/'/g, "'\\''")}'`).join(" ")}`, { encoding: "utf8" });
29
+ const parts = raw.split("\nHTTP_STATUS:");
30
+ const httpStatus = parseInt(parts[1] || "0", 10);
31
+ if (httpStatus < 200 || httpStatus >= 300) {
32
+ throw new Error(`HTTP ${httpStatus} on SOQL query: ${parts[0]}`);
33
+ }
34
+ return JSON.parse(parts[0]);
35
+ }
36
+
37
+ function apiFetch(instanceUrl, accessToken, apiPath, method = "GET", body = null) {
38
+ const url = `${instanceUrl}${apiPath}`;
39
+ const args = [
40
+ "-s",
41
+ "-H", `Authorization: Bearer ${accessToken}`,
42
+ "-H", "Content-Type: application/json",
43
+ "-X", method,
44
+ "-w", "\\nHTTP_STATUS:%{http_code}",
45
+ ];
46
+ if (body !== null) args.push("-d", JSON.stringify(body));
47
+ args.push(url);
48
+ const raw = execSync(`curl ${args.map(a => `'${String(a).replace(/'/g, "'\\''")}'`).join(" ")}`, { encoding: "utf8" });
49
+ const parts = raw.split("\nHTTP_STATUS:");
50
+ const httpStatus = parseInt(parts[1] || "0", 10);
51
+ if (httpStatus < 200 || httpStatus >= 300) {
52
+ throw new Error(`HTTP ${httpStatus} on ${method} ${apiPath}: ${parts[0]}`);
53
+ }
54
+ return JSON.parse(parts[0]);
55
+ }
56
+
57
+ function sleep(ms) {
58
+ execSync(`sleep ${ms / 1000}`);
59
+ }
60
+
61
+ const [,, alias, csvPath, metadataJsonPath, datasetAlias, folderId] = process.argv;
62
+ if (!alias || !csvPath || !metadataJsonPath || !datasetAlias || !folderId) {
63
+ console.error("Usage: node upload_dataset.js <alias> <csvPath> <metadataJsonPath> <datasetAlias> <folderId>");
64
+ process.exit(1);
65
+ }
66
+
67
+ const { accessToken, instanceUrl } = getOrgCredentials(alias);
68
+
69
+ const metadataJson = fs.readFileSync(path.resolve(metadataJsonPath), "utf8");
70
+ const csvContent = fs.readFileSync(path.resolve(csvPath), "utf8");
71
+ const csvBase64 = Buffer.from(csvContent, "utf8").toString("base64");
72
+ const metaBase64 = Buffer.from(metadataJson, "utf8").toString("base64");
73
+
74
+ // Create InsightsExternalData record
75
+ const edRecord = apiFetch(instanceUrl, accessToken, "/services/data/v59.0/sobjects/InsightsExternalData", "POST", {
76
+ EdgemartAlias: datasetAlias,
77
+ EdgemartLabel: datasetAlias,
78
+ MetadataJson: metaBase64,
79
+ Format: "Csv",
80
+ Operation: "Overwrite",
81
+ Action: "None",
82
+ EdgemartContainer: folderId,
83
+ });
84
+
85
+ const edId = edRecord.id;
86
+ console.log(`InsightsExternalData created: ${edId}`);
87
+
88
+ // Upload CSV part
89
+ apiFetch(instanceUrl, accessToken, "/services/data/v59.0/sobjects/InsightsExternalDataPart", "POST", {
90
+ DataFile: csvBase64,
91
+ InsightsExternalDataId: edId,
92
+ PartNumber: 1,
93
+ });
94
+ console.log("CSV part uploaded.");
95
+
96
+ // Trigger processing
97
+ apiFetch(instanceUrl, accessToken, `/services/data/v59.0/sobjects/InsightsExternalData/${edId}`, "PATCH", {
98
+ Action: "Process",
99
+ });
100
+ console.log("Processing triggered.");
101
+
102
+ // Poll until terminal
103
+ const deadline = Date.now() + MAX_WAIT_MS;
104
+ while (Date.now() < deadline) {
105
+ sleep(POLL_INTERVAL_MS);
106
+ const result = soqlQuery(instanceUrl, accessToken,
107
+ `SELECT Status, StatusMessage FROM InsightsExternalData WHERE Id = '${edId}'`
108
+ );
109
+ const record = result.records?.[0];
110
+ if (!record) continue;
111
+ console.log(`Status: ${record.Status}`);
112
+ if (TERMINAL_STATUSES.has(record.Status)) {
113
+ console.log(JSON.stringify({ edId, status: record.Status, message: record.StatusMessage || null }));
114
+ process.exit(record.Status === "Completed" ? 0 : 1);
115
+ }
116
+ }
117
+
118
+ console.error("Timed out waiting for ingestion to complete.");
119
+ process.exit(1);
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ // For each step in a deployed dashboard, GET stored SAQL, decode HTML entities,
3
+ // swap dataset name for id/version, POST to /wave/query. Prints HTTP status + row count per step.
4
+ // Edit DASH_ID and DS constants at the top before running.
5
+ // Usage: node scripts/verify_dashboard_steps.js <alias>
6
+
7
+ const { execSync, execFileSync } = require("child_process");
8
+
9
+ // ── Edit these constants before running ──────────────────────────────────────
10
+ const DASH_ID = "0FK000000000000AAA"; // Wave Dashboard 18-char ID
11
+ const DS = {
12
+ name: "MyDataset", // Name-based load identifier used in stored SAQL
13
+ id: "0Fb000000000000AAA", // Dataset 18-char ID
14
+ versionId: "0Fc000000000000AAA", // Current version ID
15
+ };
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+
18
+ function getOrgCredentials(alias) {
19
+ const result = execFileSync('sf', ['org', 'display', '-o', alias, '--json'], { encoding: "utf8" });
20
+ const json = JSON.parse(result);
21
+ return { accessToken: json.result.accessToken, instanceUrl: json.result.instanceUrl };
22
+ }
23
+
24
+ function apiFetch(instanceUrl, accessToken, apiPath, method = "GET", body = null) {
25
+ const url = `${instanceUrl}${apiPath}`;
26
+ const args = [
27
+ "-s",
28
+ "-H", `Authorization: Bearer ${accessToken}`,
29
+ "-H", "Content-Type: application/json",
30
+ "-X", method,
31
+ "-w", "\\nHTTP_STATUS:%{http_code}",
32
+ ];
33
+ if (body !== null) args.push("-d", JSON.stringify(body));
34
+ args.push(url);
35
+ const raw = execSync(`curl ${args.map(a => `'${String(a).replace(/'/g, "'\\''")}'`).join(" ")}`, {
36
+ encoding: "utf8", maxBuffer: 10 * 1024 * 1024,
37
+ });
38
+ const parts = raw.split("\nHTTP_STATUS:");
39
+ const httpStatus = parseInt(parts[1] || "0", 10);
40
+ let parsed = null;
41
+ try { parsed = JSON.parse(parts[0]); } catch { parsed = parts[0]; }
42
+ return { httpStatus, body: parsed };
43
+ }
44
+
45
+ function decodeHtmlEntities(s) {
46
+ let prev = null;
47
+ while (prev !== s) {
48
+ prev = s;
49
+ s = s.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, '&');
50
+ }
51
+ return s;
52
+ }
53
+
54
+ const [,, alias] = process.argv;
55
+ if (!alias) {
56
+ console.error("Usage: node verify_dashboard_steps.js <alias>");
57
+ console.error("Edit DASH_ID and DS constants at the top of this file before running.");
58
+ process.exit(1);
59
+ }
60
+
61
+ const { accessToken, instanceUrl } = getOrgCredentials(alias);
62
+
63
+ // Fetch dashboard state
64
+ const { httpStatus: dashStatus, body: dash } = apiFetch(instanceUrl, accessToken, `/services/data/v59.0/wave/dashboards/${DASH_ID}`);
65
+ if (dashStatus !== 200) {
66
+ console.error(`Failed to fetch dashboard ${DASH_ID}: HTTP ${dashStatus}`);
67
+ process.exit(1);
68
+ }
69
+
70
+ const steps = dash.state?.steps || {};
71
+ const stepNames = Object.keys(steps);
72
+ const results = [];
73
+
74
+ for (const stepName of stepNames) {
75
+ const step = steps[stepName];
76
+ if (step.type !== "saql") {
77
+ results.push({ step: stepName, type: step.type, skipped: true, reason: "non-saql step" });
78
+ continue;
79
+ }
80
+
81
+ const decoded = decodeHtmlEntities(step.query || "");
82
+ // Swap name-based load for id/version for /wave/query
83
+ const queryForVerify = decoded.replace(
84
+ new RegExp(`"${DS.name}"`, "g"),
85
+ `"${DS.id}/${DS.versionId}"`
86
+ );
87
+
88
+ const { httpStatus, body: queryResult } = apiFetch(
89
+ instanceUrl, accessToken, "/services/data/v59.0/wave/query", "POST",
90
+ { query: queryForVerify, queryLanguage: "SAQL" }
91
+ );
92
+
93
+ const rowCount = queryResult.results?.records?.length ?? null;
94
+ results.push({ step: stepName, httpStatus, rowCount, ok: httpStatus === 200 && rowCount !== null });
95
+ }
96
+
97
+ const allOk = results.every(r => r.skipped || r.ok);
98
+ console.log(JSON.stringify({ allOk, steps: results }, null, 2));
99
+ process.exit(allOk ? 0 : 1);