@salesforce/afv-skills 1.60.0 → 1.62.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/package.json +3 -3
- package/skills/agentforce-architecture-analyze/README.md +2 -1
- package/skills/agentforce-d360-analyze/README.md +1 -1
- package/skills/agentforce-generate/README.md +1 -0
- package/skills/crm-analytics-wave-generate/SKILL.md +212 -0
- package/skills/crm-analytics-wave-generate/assets/dashboard-template.json +66 -0
- package/skills/crm-analytics-wave-generate/assets/wapp-meta-template.xml +10 -0
- package/skills/crm-analytics-wave-generate/assets/wdash-meta-template.xml +6 -0
- package/skills/crm-analytics-wave-generate/assets/wdpr-meta-template.xml +11 -0
- package/skills/crm-analytics-wave-generate/examples/industry-binding-dashboard.wdash +67 -0
- package/skills/crm-analytics-wave-generate/examples/opportunity-revenue-recipe.wdpr +132 -0
- package/skills/crm-analytics-wave-generate/examples/pipeline-by-industry-quarter.wdpr +236 -0
- package/skills/crm-analytics-wave-generate/examples/wdpr-skeleton.wdpr +62 -0
- package/skills/crm-analytics-wave-generate/references/dashboard-authoring.md +157 -0
- package/skills/crm-analytics-wave-generate/references/dataset-hygiene.md +27 -0
- package/skills/crm-analytics-wave-generate/references/external-data-upload.md +61 -0
- package/skills/crm-analytics-wave-generate/references/method-decision-tree.md +12 -0
- package/skills/crm-analytics-wave-generate/references/r3-node-schema.md +194 -0
- package/skills/crm-analytics-wave-generate/references/scripts-reference.md +17 -0
- package/skills/crm-analytics-wave-generate/references/ui-section-template.md +68 -0
- package/skills/crm-analytics-wave-generate/scripts/check_connector_error.sh +53 -0
- package/skills/crm-analytics-wave-generate/scripts/create_app.js +59 -0
- package/skills/crm-analytics-wave-generate/scripts/deploy_and_run_recipe.js +122 -0
- package/skills/crm-analytics-wave-generate/scripts/fetch_dataset_xmd.js +59 -0
- package/skills/crm-analytics-wave-generate/scripts/find_unused_dataset_fields.js +96 -0
- package/skills/crm-analytics-wave-generate/scripts/gen_synthetic_dataset.js +137 -0
- package/skills/crm-analytics-wave-generate/scripts/patch_t_oppo_view.js +99 -0
- package/skills/crm-analytics-wave-generate/scripts/probe_recipe.js +47 -0
- package/skills/crm-analytics-wave-generate/scripts/upload_dataset.js +119 -0
- package/skills/crm-analytics-wave-generate/scripts/verify_dashboard_steps.js +99 -0
- package/skills/crm-analytics-wave-generate/scripts/verify_synthetic_dataset.js +86 -0
- package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +59 -20
- package/skills/service-itsm-agentic-setup-incident-management/examples/output-templates.md +5 -1
- package/skills/service-itsm-agentic-setup-incident-management/references/incident-persona-psg.md +2 -2
- package/skills/service-itsm-agentic-setup-incident-management/references/incident-preferences.md +2 -2
- package/skills/service-itsm-agentic-setup-incident-management/references/major-incident-management.md +5 -5
- package/skills/service-itsm-agentic-setup-incident-management/references/service-mgmt-privilege.md +5 -5
- package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +67 -100
- package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone-action.json +2 -2
- package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +2 -2
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +23 -22
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +3 -3
- package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +182 -58
- package/skills/service-itsm-agentic-setup-incident-sla-configure/references/workflow.md +157 -88
- package/skills/service-de-channel-consent-configure/SKILL.md +0 -230
|
@@ -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(/"/g, '"').replace(/'/g, "'").replace(/&/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(/"/g, '"').replace(/'/g, "'").replace(/&/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(/"/g, '"').replace(/'/g, "'").replace(/&/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);
|