@chengxianglibra/dsh-data-analysis 0.1.1 → 0.1.2
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/README.md +83 -34
- package/lib/bin/environment.js +1 -1
- package/lib/bin/presentation-lint.js +21 -0
- package/lib/bridges.js +2 -2
- package/lib/client/credentials/create-datasource.js +67 -0
- package/lib/client/credentials/install.js +191 -0
- package/lib/client/credentials/model.js +494 -0
- package/lib/client/credentials/styles.js +17 -0
- package/lib/client/workspace-header-action.js +11 -0
- package/lib/client.js +1133 -736
- package/lib/compatibility.js +1 -1
- package/lib/datasource/authoring.js +70 -0
- package/lib/datasource/bridge-programs.js +30 -18
- package/lib/datasource/bridge.js +100 -8
- package/lib/datasource/index.js +3 -2
- package/lib/datasource/python.js +115 -0
- package/lib/datasource/resolver-program.js +112 -0
- package/lib/datasource/rpc.js +88 -0
- package/lib/datasource/service.js +687 -0
- package/lib/datasource/shell-env.js +2 -333
- package/lib/datasource/test.js +24 -51
- package/lib/environment/binding.js +54 -9
- package/lib/environment/runtime.js +89 -78
- package/lib/environment/subprocess.js +6 -1
- package/lib/environment/summary.js +3 -3
- package/lib/environment/workspace.js +5 -0
- package/lib/index.js +1 -1
- package/lib/plugin.js +168 -51
- package/lib/presentation/assets/portable.js +606 -0
- package/lib/presentation/assets/static.js +945 -0
- package/lib/presentation/build/index.js +71 -0
- package/lib/presentation/commit.js +156 -0
- package/lib/presentation/contracts/charts.js +476 -0
- package/lib/presentation/contracts/editing.js +58 -0
- package/lib/presentation/contracts/index.js +651 -0
- package/lib/presentation/contracts/interaction.js +62 -0
- package/lib/presentation/contracts/types.js +15 -0
- package/lib/presentation/contracts/values.js +28 -0
- package/lib/presentation/delivery.js +56 -0
- package/lib/presentation/files.js +83 -0
- package/lib/presentation/index.js +6 -0
- package/lib/presentation/lint.js +46 -0
- package/lib/presentation/projection/files.js +85 -0
- package/lib/presentation/projection/index.js +195 -0
- package/lib/presentation/projection/program.js +144 -0
- package/lib/presentation/projection/sql-code.js +79 -0
- package/lib/presentation/receipt.js +34 -0
- package/lib/presentation/reports.js +122 -0
- package/lib/presentation/rpc.js +148 -0
- package/lib/presentation/tool.js +122 -0
- package/lib/python-execution-contracts.js +1 -0
- package/lib/python-execution.js +232 -0
- package/lib/semantic-browser/contracts.js +80 -0
- package/lib/semantic-browser/definition.js +64 -0
- package/lib/semantic-browser/program.js +119 -0
- package/lib/semantic-browser/service.js +72 -0
- package/lib/semantic-reference/bridge.js +118 -0
- package/lib/semantic-reference/contracts.js +113 -0
- package/lib/semantic-reference/index.js +3 -0
- package/lib/semantic-reference/rpc.js +122 -0
- package/lib/semantic-reference/search.js +94 -0
- package/lib/semantic-reference/usage.js +143 -0
- package/lib/types/bridges.d.ts +2 -2
- package/lib/types/client/credentials/create-datasource.d.ts +5 -0
- package/lib/types/client/credentials/install.d.ts +1 -0
- package/lib/types/client/credentials/model.d.ts +59 -0
- package/lib/types/client/credentials/styles.d.ts +1 -0
- package/lib/types/client/presentation/ask-dsh.d.ts +9 -0
- package/lib/types/client/presentation/chart-explorer.d.ts +12 -0
- package/lib/types/client/presentation/chart-geometry.d.ts +30 -0
- package/lib/types/client/presentation/chart-renderer.d.ts +10 -0
- package/lib/types/client/presentation/chart-specials.d.ts +10 -0
- package/lib/types/client/presentation/chart-styles.d.ts +1 -0
- package/lib/types/client/presentation/chart-tooltip.d.ts +13 -0
- package/lib/types/client/presentation/chart-view.d.ts +23 -0
- package/lib/types/client/presentation/copy-context.d.ts +5 -0
- package/lib/types/client/presentation/delivery-model.d.ts +54 -0
- package/lib/types/client/presentation/delivery.d.ts +77 -0
- package/lib/types/client/presentation/editor-controls.d.ts +12 -0
- package/lib/types/client/presentation/global-filters.d.ts +6 -0
- package/lib/types/client/presentation/host-entry.d.ts +12 -0
- package/lib/types/client/presentation/icons.d.ts +4 -0
- package/lib/types/client/presentation/install.d.ts +15 -0
- package/lib/types/client/presentation/markdown.d.ts +5 -0
- package/lib/types/client/presentation/model.d.ts +62 -0
- package/lib/types/client/presentation/reader.d.ts +11 -0
- package/lib/types/client/presentation/source-code-model.d.ts +16 -0
- package/lib/types/client/presentation/source-code.d.ts +11 -0
- package/lib/types/client/presentation/source-dialog.d.ts +13 -0
- package/lib/types/client/presentation/source-facts.d.ts +20 -0
- package/lib/types/client/presentation/source-format.d.ts +5 -0
- package/lib/types/client/presentation/source-highlight.d.ts +7 -0
- package/lib/types/client/presentation/sources.d.ts +13 -0
- package/lib/types/client/presentation/styles.d.ts +1 -0
- package/lib/types/client/presentation/table.d.ts +12 -0
- package/lib/types/client/presentation/use-element-width.d.ts +5 -0
- package/lib/types/client/workspace-header-action.d.ts +7 -0
- package/lib/types/client.d.ts +4 -122
- package/lib/types/compatibility.d.ts +2 -2
- package/lib/types/datasource/authoring.d.ts +25 -0
- package/lib/types/datasource/bridge.d.ts +21 -3
- package/lib/types/datasource/index.d.ts +3 -5
- package/lib/types/datasource/python.d.ts +4 -0
- package/lib/types/datasource/resolver-program.d.ts +6 -0
- package/lib/types/datasource/rpc.d.ts +4 -0
- package/lib/types/datasource/service.d.ts +112 -0
- package/lib/types/datasource/shell-env.d.ts +0 -29
- package/lib/types/datasource/test.d.ts +6 -29
- package/lib/types/environment/binding.d.ts +5 -1
- package/lib/types/environment/errors.d.ts +1 -1
- package/lib/types/environment/runtime.d.ts +5 -5
- package/lib/types/environment/types.d.ts +13 -2
- package/lib/types/index.d.ts +1 -1
- package/lib/types/plugin.d.ts +11 -5
- package/lib/types/presentation/build/index.d.ts +11 -0
- package/lib/types/presentation/commit.d.ts +4 -0
- package/lib/types/presentation/contracts/charts.d.ts +13 -0
- package/lib/types/presentation/contracts/editing.d.ts +8 -0
- package/lib/types/presentation/contracts/index.d.ts +11 -0
- package/lib/types/presentation/contracts/interaction.d.ts +8 -0
- package/lib/types/presentation/contracts/types.d.ts +252 -0
- package/lib/types/presentation/contracts/values.d.ts +9 -0
- package/lib/types/presentation/delivery.d.ts +3 -0
- package/lib/types/presentation/files.d.ts +7 -0
- package/lib/types/presentation/index.d.ts +6 -0
- package/lib/types/presentation/lint.d.ts +13 -0
- package/lib/types/presentation/projection/files.d.ts +2 -0
- package/lib/types/presentation/projection/index.d.ts +20 -0
- package/lib/types/presentation/projection/program.d.ts +2 -0
- package/lib/types/presentation/projection/sql-code.d.ts +2 -0
- package/lib/types/presentation/receipt.d.ts +14 -0
- package/lib/types/presentation/reports.d.ts +5 -0
- package/lib/types/presentation/rpc.d.ts +24 -0
- package/lib/types/presentation/tool.d.ts +12 -0
- package/lib/types/python-execution-contracts.d.ts +10 -0
- package/lib/types/python-execution.d.ts +10 -0
- package/lib/types/semantic-browser/contracts.d.ts +40 -0
- package/lib/types/semantic-browser/definition.d.ts +20 -0
- package/lib/types/semantic-browser/program.d.ts +2 -0
- package/lib/types/semantic-browser/service.d.ts +20 -0
- package/lib/types/semantic-reference/bridge.d.ts +11 -0
- package/lib/types/semantic-reference/contracts.d.ts +51 -0
- package/lib/types/semantic-reference/index.d.ts +3 -0
- package/lib/types/semantic-reference/rpc.d.ts +15 -0
- package/lib/types/semantic-reference/search.d.ts +10 -0
- package/lib/types/semantic-reference/usage.d.ts +43 -0
- package/package.json +43 -19
- package/python/presentation-kit/dist/dsh_data_analysis_presentation_kit-1.1.0-py3-none-any.whl +0 -0
- package/skills/dsh-data-analysis-presentation/SKILL.md +24 -0
- package/skills/dsh-data-analysis-presentation/references/chart-examples.md +18 -0
- package/skills/dsh-data-analysis-presentation/references/charts.md +55 -0
- package/skills/dsh-data-analysis-presentation/references/examples/artifact.draft.json +27 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-bins.dataset.json +33 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-boxes.dataset.json +45 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-composition.dataset.json +33 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-funnel.dataset.json +27 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-precision.dataset.json +21 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-series.dataset.json +58 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-shares.dataset.json +33 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-waterfall.dataset.json +44 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts.draft.json +392 -0
- package/skills/dsh-data-analysis-presentation/references/examples/comparison-selected.dataset.json +24 -0
- package/skills/dsh-data-analysis-presentation/references/examples/comparison-summary.dataset.json +22 -0
- package/skills/dsh-data-analysis-presentation/references/examples/comparison.dataset.json +18 -0
- package/skills/dsh-data-analysis-presentation/references/examples/comparison.draft.json +79 -0
- package/skills/dsh-data-analysis-presentation/references/examples/computed-sources.draft.json +35 -0
- package/skills/dsh-data-analysis-presentation/references/examples/computed.dataset.json +12 -0
- package/skills/dsh-data-analysis-presentation/references/examples/computed.draft.json +57 -0
- package/skills/dsh-data-analysis-presentation/references/examples/interaction-detail.dataset.json +30 -0
- package/skills/dsh-data-analysis-presentation/references/examples/interaction-summary.dataset.json +22 -0
- package/skills/dsh-data-analysis-presentation/references/examples/interaction.draft.json +278 -0
- package/skills/dsh-data-analysis-presentation/references/examples/source-only.draft.json +19 -0
- package/skills/dsh-data-analysis-presentation/references/examples/write-charts.py +73 -0
- package/skills/dsh-data-analysis-presentation/references/examples/write-comparison.py +42 -0
- package/skills/dsh-data-analysis-presentation/references/examples/write-computed.py +21 -0
- package/skills/dsh-data-analysis-presentation/references/examples/write-interaction.py +33 -0
- package/skills/dsh-data-analysis-presentation/references/examples.md +42 -0
- package/skills/dsh-data-analysis-presentation/references/interaction.md +56 -0
- package/skills/dsh-data-analysis-presentation/references/narrative.md +37 -0
- package/skills/dsh-data-analysis-presentation/references/schema.md +138 -0
- package/lib/datasource/access.js +0 -60
- package/lib/datasource/credentials.js +0 -24
- package/lib/evidence/bridge-program.js +0 -153
- package/lib/evidence/bridge.js +0 -166
- package/lib/evidence/index.js +0 -2
- package/lib/evidence/sources.js +0 -263
- package/lib/types/datasource/access.d.ts +0 -27
- package/lib/types/evidence/bridge.d.ts +0 -40
- package/lib/types/evidence/index.d.ts +0 -4
- package/lib/types/evidence/sources.d.ts +0 -57
- package/python/report-kit/dist/dsh_data_analysis_report_kit-3.0.0-py3-none-any.whl +0 -0
- package/skills/dsh-data-analysis-report/SKILL.md +0 -57
- package/skills/dsh-data-analysis-report/assets/marivo-artifact.js +0 -113
- package/skills/dsh-data-analysis-report/assets/marivo-session-dag.js +0 -1201
- package/skills/dsh-data-analysis-report/assets/report-data.js +0 -449
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
import { chartColumns, parseChartViewShape, validateChartView } from "./charts.js";
|
|
2
|
+
import { PRESENTATION_BUDGETS as budgets, } from "./types.js";
|
|
3
|
+
import { PresentationContractError } from "./values.js";
|
|
4
|
+
export * from "./charts.js";
|
|
5
|
+
export * from "./types.js";
|
|
6
|
+
export * from "./values.js";
|
|
7
|
+
function fail(path, message, code = 'invalid_value') {
|
|
8
|
+
throw new PresentationContractError(code, path, message);
|
|
9
|
+
}
|
|
10
|
+
function pointer(path, key) {
|
|
11
|
+
return `${path}/${String(key).replaceAll('~', '~0').replaceAll('/', '~1')}`;
|
|
12
|
+
}
|
|
13
|
+
function object(value, path) {
|
|
14
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
15
|
+
fail(path, 'Expected an object.');
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
function keys(value, required, optional, path) {
|
|
19
|
+
for (const key of required) {
|
|
20
|
+
if (!Object.hasOwn(value, key))
|
|
21
|
+
fail(pointer(path, key), 'Required field is missing.');
|
|
22
|
+
}
|
|
23
|
+
for (const key of Object.keys(value)) {
|
|
24
|
+
if (!required.includes(key) && !optional.includes(key)) {
|
|
25
|
+
fail(pointer(path, key), 'Unknown field.', 'unknown_field');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function string(value, path, max = budgets.text) {
|
|
30
|
+
if (typeof value !== 'string' ||
|
|
31
|
+
value.length === 0 ||
|
|
32
|
+
value.length > max ||
|
|
33
|
+
value.includes('\0') ||
|
|
34
|
+
/[\uD800-\uDFFF]/u.test(value)) {
|
|
35
|
+
fail(path, `Expected a nonempty string of at most ${max} characters.`);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function integer(value, path, min = 0, max = Number.MAX_SAFE_INTEGER) {
|
|
40
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < min || value > max) {
|
|
41
|
+
fail(path, `Expected a safe integer between ${min} and ${max}.`);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function array(value, path, max, min = 0) {
|
|
46
|
+
if (!Array.isArray(value) || value.length < min || value.length > max) {
|
|
47
|
+
fail(path, `Expected an array with ${min} to ${max} items.`, 'budget');
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
function unique(values, path) {
|
|
52
|
+
if (new Set(values).size !== values.length)
|
|
53
|
+
fail(path, 'Duplicate identifiers.', 'duplicate_id');
|
|
54
|
+
}
|
|
55
|
+
function stringArray(value, path, max, min = 0) {
|
|
56
|
+
const result = array(value, path, max, min).map((entry, i) => string(entry, pointer(path, i), 512));
|
|
57
|
+
unique(result, path);
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
function bytes(value, max, path = '') {
|
|
61
|
+
let json;
|
|
62
|
+
try {
|
|
63
|
+
json = JSON.stringify(value);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
fail(path, 'Expected serializable JSON.');
|
|
67
|
+
}
|
|
68
|
+
if (json === undefined || new TextEncoder().encode(json).length > max) {
|
|
69
|
+
fail(path, `JSON exceeds the ${max} byte budget.`, 'budget');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function version(value, path, expected = 1) {
|
|
73
|
+
if (value.schemaVersion !== expected)
|
|
74
|
+
fail(pointer(path, 'schemaVersion'), `Only schemaVersion ${expected} is accepted.`);
|
|
75
|
+
}
|
|
76
|
+
export function parsePresentationBuildId(value, path = '/buildId') {
|
|
77
|
+
const result = string(value, path, 80);
|
|
78
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(result))
|
|
79
|
+
fail(path, 'Expected a safe build identifier.');
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
function date(value, path, datetime) {
|
|
83
|
+
const text = string(value, path, 64);
|
|
84
|
+
const expression = datetime
|
|
85
|
+
? /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/
|
|
86
|
+
: /^\d{4}-\d{2}-\d{2}$/;
|
|
87
|
+
if (!expression.test(text) || !Number.isFinite(Date.parse(text)))
|
|
88
|
+
fail(path, 'Invalid ISO date/time.');
|
|
89
|
+
if (text.slice(0, 4) === '0000')
|
|
90
|
+
fail(path, 'Calendar year must be between 0001 and 9999.');
|
|
91
|
+
if (datetime) {
|
|
92
|
+
if (Number(text.slice(11, 13)) > 23 ||
|
|
93
|
+
Number(text.slice(14, 16)) > 59 ||
|
|
94
|
+
Number(text.slice(17, 19)) > 59) {
|
|
95
|
+
fail(path, 'Invalid clock time.');
|
|
96
|
+
}
|
|
97
|
+
const offset = /[+-](\d{2}):(\d{2})$/.exec(text);
|
|
98
|
+
if (offset && (Number(offset[1]) > 23 || Number(offset[2]) > 59))
|
|
99
|
+
fail(path, 'Invalid timezone offset.');
|
|
100
|
+
}
|
|
101
|
+
// Date.parse normalizes impossible calendar dates such as February 30.
|
|
102
|
+
const day = text.slice(0, 10);
|
|
103
|
+
if (new Date(`${day}T00:00:00Z`).toISOString().slice(0, 10) !== day) {
|
|
104
|
+
fail(path, 'Invalid calendar date.');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function sourceRef(value, path) {
|
|
108
|
+
const ref = object(value, path);
|
|
109
|
+
keys(ref, ['sessionId', 'artifactRef'], ['findingId'], path);
|
|
110
|
+
string(ref.sessionId, pointer(path, 'sessionId'), 512);
|
|
111
|
+
string(ref.artifactRef, pointer(path, 'artifactRef'), 512);
|
|
112
|
+
if (ref.findingId !== undefined)
|
|
113
|
+
string(ref.findingId, pointer(path, 'findingId'), 512);
|
|
114
|
+
}
|
|
115
|
+
function sourceCode(value, path) {
|
|
116
|
+
const snapshot = object(value, path);
|
|
117
|
+
keys(snapshot, ['snippets', 'notices'], [], path);
|
|
118
|
+
const identities = array(snapshot.snippets, `${path}/snippets`, budgets.codeSnippets).map((value, i) => {
|
|
119
|
+
const location = `${path}/snippets/${i}`;
|
|
120
|
+
const entry = object(value, location);
|
|
121
|
+
keys(entry, ['language', 'text', 'provenance', 'runId', 'queryId', 'artifactRef'], [], location);
|
|
122
|
+
if (entry.language !== 'sql' || entry.provenance !== 'execution')
|
|
123
|
+
fail(location, 'Expected SQL captured from a persisted execution.');
|
|
124
|
+
if (!string(entry.text, `${location}/text`).trim())
|
|
125
|
+
fail(`${location}/text`, 'Expected nonblank source code.');
|
|
126
|
+
for (const field of ['runId', 'queryId', 'artifactRef'])
|
|
127
|
+
string(entry[field], `${location}/${field}`, 512);
|
|
128
|
+
return JSON.stringify([entry.runId, entry.queryId]);
|
|
129
|
+
});
|
|
130
|
+
unique(identities, `${path}/snippets`);
|
|
131
|
+
stringArray(snapshot.notices, `${path}/notices`, 64);
|
|
132
|
+
}
|
|
133
|
+
function pythonCode(value, path, generated) {
|
|
134
|
+
const identities = array(value, path, budgets.codeSnippets).map((value, i) => {
|
|
135
|
+
const location = `${path}/${i}`;
|
|
136
|
+
const entry = object(value, location);
|
|
137
|
+
keys(entry, generated
|
|
138
|
+
? ['executionId', 'sha256', 'language', 'text', 'provenance']
|
|
139
|
+
: ['executionId', 'sha256'], [], location);
|
|
140
|
+
const id = string(entry.executionId, `${location}/executionId`, 36);
|
|
141
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(id))
|
|
142
|
+
fail(`${location}/executionId`, 'Expected a captured execution UUID.');
|
|
143
|
+
const digest = string(entry.sha256, `${location}/sha256`, 64);
|
|
144
|
+
if (!/^[0-9a-f]{64}$/.test(digest))
|
|
145
|
+
fail(`${location}/sha256`, 'Expected the captured execution SHA-256 digest.');
|
|
146
|
+
if (generated) {
|
|
147
|
+
if (entry.language !== 'python' || entry.provenance !== 'execution')
|
|
148
|
+
fail(location, 'Expected Python captured from an execution.');
|
|
149
|
+
const text = string(entry.text, `${location}/text`, 131_072);
|
|
150
|
+
if (!text.trim() || new TextEncoder().encode(text).length > 131_072)
|
|
151
|
+
fail(`${location}/text`, 'Expected nonblank Python source of at most 128 KiB.');
|
|
152
|
+
}
|
|
153
|
+
return id;
|
|
154
|
+
});
|
|
155
|
+
unique(identities, path);
|
|
156
|
+
}
|
|
157
|
+
function column(value, path) {
|
|
158
|
+
const entry = object(value, path);
|
|
159
|
+
keys(entry, ['id', 'label', 'type', 'nullable'], ['unit'], path);
|
|
160
|
+
string(entry.id, pointer(path, 'id'), 256);
|
|
161
|
+
string(entry.label, pointer(path, 'label'), 256);
|
|
162
|
+
if (typeof entry.type !== 'string' ||
|
|
163
|
+
!['string', 'boolean', 'float64', 'int64', 'decimal', 'date', 'datetime'].includes(entry.type)) {
|
|
164
|
+
fail(pointer(path, 'type'), 'Unknown presentation column type.');
|
|
165
|
+
}
|
|
166
|
+
if (typeof entry.nullable !== 'boolean')
|
|
167
|
+
fail(pointer(path, 'nullable'), 'Expected a boolean.');
|
|
168
|
+
if (entry.unit !== undefined)
|
|
169
|
+
string(entry.unit, pointer(path, 'unit'), 128);
|
|
170
|
+
return entry;
|
|
171
|
+
}
|
|
172
|
+
function cell(value, column, path) {
|
|
173
|
+
try {
|
|
174
|
+
validateCell(value, column, path);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (!(error instanceof PresentationContractError))
|
|
178
|
+
throw error;
|
|
179
|
+
throw new PresentationContractError(error.code, error.path, `column ${column.id} type=${column.type}: ${error.reason}`, error.hint);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function validateCell(value, column, path) {
|
|
183
|
+
if (value === null) {
|
|
184
|
+
if (!column.nullable)
|
|
185
|
+
fail(path, 'Column does not allow null.');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
switch (column.type) {
|
|
189
|
+
case 'boolean':
|
|
190
|
+
if (typeof value !== 'boolean')
|
|
191
|
+
fail(path, 'Expected a boolean.');
|
|
192
|
+
break;
|
|
193
|
+
case 'float64':
|
|
194
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
195
|
+
fail(path, 'Expected a finite number.');
|
|
196
|
+
if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
197
|
+
fail(path, 'An unsafe integer must use an int64 or decimal string.', 'numeric_precision');
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
case 'int64': {
|
|
201
|
+
if (typeof value !== 'string') {
|
|
202
|
+
throw new PresentationContractError('invalid_value', path, 'Expected an exact integer string for signed int64; JSON numbers are not accepted. Write 42 as "42".', 'Encode the original exact integer as a JSON string (for example, "42"). Do not stringify an already rounded number; use the original data or the Python write_dataset helper.');
|
|
203
|
+
}
|
|
204
|
+
const text = string(value, path, 21);
|
|
205
|
+
if (!/^-?(?:0|[1-9]\d*)$/.test(text))
|
|
206
|
+
fail(path, 'Expected an exact integer string.');
|
|
207
|
+
const number = BigInt(text);
|
|
208
|
+
if (number < -9223372036854775808n || number > 9223372036854775807n) {
|
|
209
|
+
fail(path, 'Integer is outside signed int64; use decimal for larger integers.');
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case 'decimal': {
|
|
214
|
+
const text = string(value, path, 256);
|
|
215
|
+
if (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(text)) {
|
|
216
|
+
fail(path, 'Expected an exact finite decimal string.');
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
case 'date':
|
|
221
|
+
case 'datetime':
|
|
222
|
+
date(value, path, column.type === 'datetime');
|
|
223
|
+
break;
|
|
224
|
+
case 'string':
|
|
225
|
+
if (typeof value !== 'string' ||
|
|
226
|
+
value.length > budgets.text ||
|
|
227
|
+
value.includes('\0') ||
|
|
228
|
+
/[\uD800-\uDFFF]/u.test(value)) {
|
|
229
|
+
fail(path, 'Expected a bounded string.');
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function dataset(value, path) {
|
|
235
|
+
const entry = object(value, path);
|
|
236
|
+
keys(entry, ['schemaVersion', 'columns', 'rows', 'rowCount', 'limit', 'truncated'], [], path);
|
|
237
|
+
version(entry, path);
|
|
238
|
+
const columns = array(entry.columns, pointer(path, 'columns'), budgets.columns, 1).map((value, i) => column(value, pointer(pointer(path, 'columns'), i)));
|
|
239
|
+
unique(columns.map((value) => value.id), pointer(path, 'columns'));
|
|
240
|
+
const rows = array(entry.rows, pointer(path, 'rows'), budgets.rows);
|
|
241
|
+
if (rows.length * columns.length > budgets.cells)
|
|
242
|
+
fail(pointer(path, 'rows'), 'Cell budget exceeded.', 'budget');
|
|
243
|
+
const total = integer(entry.rowCount, pointer(path, 'rowCount'));
|
|
244
|
+
const limit = integer(entry.limit, pointer(path, 'limit'), 1, budgets.rows);
|
|
245
|
+
if (rows.length !== Math.min(total, limit))
|
|
246
|
+
fail(pointer(path, 'rows'), 'Row count must equal min(rowCount, limit).');
|
|
247
|
+
if (entry.truncated !== total > rows.length)
|
|
248
|
+
fail(pointer(path, 'truncated'), 'Truncation must agree with rowCount and written rows.');
|
|
249
|
+
rows.forEach((value, i) => {
|
|
250
|
+
const rowPath = pointer(pointer(path, 'rows'), i);
|
|
251
|
+
array(value, rowPath, columns.length, columns.length).forEach((value, j) => {
|
|
252
|
+
cell(value, columns[j], pointer(rowPath, j));
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
return entry;
|
|
256
|
+
}
|
|
257
|
+
export function parseTypedDataset(value, path = '') {
|
|
258
|
+
bytes(value, budgets.datasetBytes, path);
|
|
259
|
+
return dataset(value, path);
|
|
260
|
+
}
|
|
261
|
+
/** Tables retain the original decimal/int64 spelling, including trailing decimal zeroes. */
|
|
262
|
+
export function formatCell(value, _column) {
|
|
263
|
+
return value === null ? '—' : String(value);
|
|
264
|
+
}
|
|
265
|
+
function common(value, generated) {
|
|
266
|
+
version(value, '', generated ? 2 : 1);
|
|
267
|
+
string(value.title, '/title', 512);
|
|
268
|
+
const sources = array(value.sources, '/sources', budgets.sources);
|
|
269
|
+
const sourceIds = sources.map((value, i) => {
|
|
270
|
+
const path = `/sources/${i}`;
|
|
271
|
+
const entry = object(value, path);
|
|
272
|
+
if (!generated)
|
|
273
|
+
keys(entry, ['id', 'ref'], [], path);
|
|
274
|
+
else if (entry.status === 'available') {
|
|
275
|
+
keys(entry, ['id', 'ref', 'status', 'label', 'facts'], ['code'], path);
|
|
276
|
+
if (entry.code !== undefined)
|
|
277
|
+
sourceCode(entry.code, `${path}/code`);
|
|
278
|
+
string(entry.label, `${path}/label`, 512);
|
|
279
|
+
array(entry.facts, `${path}/facts`, 64).forEach((value, i) => {
|
|
280
|
+
const factPath = `${path}/facts/${i}`;
|
|
281
|
+
const fact = object(value, factPath);
|
|
282
|
+
keys(fact, ['label', 'value'], [], factPath);
|
|
283
|
+
string(fact.label, `${factPath}/label`, 256);
|
|
284
|
+
string(fact.value, `${factPath}/value`);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
else if (entry.status === 'unavailable') {
|
|
288
|
+
keys(entry, ['id', 'ref', 'status', 'reason'], [], path);
|
|
289
|
+
string(entry.reason, `${path}/reason`, 1024);
|
|
290
|
+
}
|
|
291
|
+
else
|
|
292
|
+
fail(`${path}/status`, 'Expected available or unavailable.');
|
|
293
|
+
sourceRef(entry.ref, `${path}/ref`);
|
|
294
|
+
return string(entry.id, `${path}/id`, 256);
|
|
295
|
+
});
|
|
296
|
+
unique(sourceIds, '/sources');
|
|
297
|
+
function references(ids, path, scalar = false) {
|
|
298
|
+
ids.forEach((id, i) => {
|
|
299
|
+
if (!sourceIds.includes(id))
|
|
300
|
+
fail(scalar ? path : pointer(path, i), 'Unknown source identifier.', 'invalid_reference');
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const datasets = array(value.datasets, '/datasets', budgets.datasets).map((value, i) => {
|
|
304
|
+
const path = `/datasets/${i}`;
|
|
305
|
+
const entry = object(value, path);
|
|
306
|
+
string(entry.id, `${path}/id`, 256);
|
|
307
|
+
if (generated) {
|
|
308
|
+
keys(entry, ['id', 'origin', 'data', 'sourceIds'], ['code'], path);
|
|
309
|
+
if (entry.code !== undefined)
|
|
310
|
+
pythonCode(entry.code, `${path}/code`, true);
|
|
311
|
+
if (entry.origin !== 'artifact' && entry.origin !== 'computed')
|
|
312
|
+
fail(`${path}/origin`, 'Expected artifact or computed.');
|
|
313
|
+
bytes(entry.data, budgets.datasetBytes, `${path}/data`);
|
|
314
|
+
dataset(entry.data, `${path}/data`);
|
|
315
|
+
const ids = stringArray(entry.sourceIds, `${path}/sourceIds`, budgets.sources);
|
|
316
|
+
references(ids, `${path}/sourceIds`);
|
|
317
|
+
if (entry.origin === 'artifact' &&
|
|
318
|
+
(ids.length !== 1 ||
|
|
319
|
+
object(sources[sourceIds.indexOf(ids[0])], '/sources').status !== 'available')) {
|
|
320
|
+
fail(`${path}/sourceIds`, 'A direct Artifact dataset requires one available source.', 'invalid_reference');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else if (entry.kind === 'artifact') {
|
|
324
|
+
keys(entry, ['id', 'kind', 'sourceId', 'rowLimit'], ['columns', 'codeRefs'], path);
|
|
325
|
+
references([string(entry.sourceId, `${path}/sourceId`, 256)], `${path}/sourceId`, true);
|
|
326
|
+
integer(entry.rowLimit, `${path}/rowLimit`, 1, budgets.rows);
|
|
327
|
+
if (entry.columns !== undefined)
|
|
328
|
+
stringArray(entry.columns, `${path}/columns`, budgets.columns, 1);
|
|
329
|
+
}
|
|
330
|
+
else if (entry.kind === 'computed') {
|
|
331
|
+
keys(entry, ['id', 'kind', 'path', 'sourceIds'], ['codeRefs'], path);
|
|
332
|
+
const file = string(entry.path, `${path}/path`, 1024);
|
|
333
|
+
if (file.startsWith('/') ||
|
|
334
|
+
file.includes('\\') ||
|
|
335
|
+
file.split('/').some((segment) => !segment || segment === '.' || segment === '..')) {
|
|
336
|
+
fail(`${path}/path`, 'Expected a relative Workspace file path without traversal.', 'file_boundary');
|
|
337
|
+
}
|
|
338
|
+
references(stringArray(entry.sourceIds, `${path}/sourceIds`, budgets.sources), `${path}/sourceIds`);
|
|
339
|
+
}
|
|
340
|
+
else
|
|
341
|
+
fail(`${path}/kind`, 'Expected artifact or computed.');
|
|
342
|
+
if (!generated && entry.codeRefs !== undefined)
|
|
343
|
+
pythonCode(entry.codeRefs, `${path}/codeRefs`, false);
|
|
344
|
+
return entry;
|
|
345
|
+
});
|
|
346
|
+
unique(datasets.map((entry) => String(entry.id)), '/datasets');
|
|
347
|
+
const declaredInteraction = value.interaction;
|
|
348
|
+
const inRegion = (id) => Array.isArray(declaredInteraction?.blockIds) && declaredInteraction.blockIds.includes(id);
|
|
349
|
+
const blockIds = array(value.blocks, '/blocks', budgets.blocks, generated ? 0 : 1).map((value, i) => {
|
|
350
|
+
const path = `/blocks/${i}`;
|
|
351
|
+
const entry = object(value, path);
|
|
352
|
+
const id = string(entry.id, `${path}/id`, 256);
|
|
353
|
+
if (entry.kind === 'markdown') {
|
|
354
|
+
keys(entry, ['id', 'kind', 'text'], [], path);
|
|
355
|
+
string(entry.text, `${path}/text`);
|
|
356
|
+
return id;
|
|
357
|
+
}
|
|
358
|
+
if (entry.kind === 'source') {
|
|
359
|
+
keys(entry, ['id', 'kind', 'sourceIds'], [], path);
|
|
360
|
+
references(stringArray(entry.sourceIds, `${path}/sourceIds`, budgets.sources, 1), `${path}/sourceIds`);
|
|
361
|
+
return id;
|
|
362
|
+
}
|
|
363
|
+
let selected;
|
|
364
|
+
if (entry.kind === 'table') {
|
|
365
|
+
keys(entry, ['id', 'kind', 'datasetId'], ['columns'], path);
|
|
366
|
+
selected =
|
|
367
|
+
entry.columns === undefined
|
|
368
|
+
? []
|
|
369
|
+
: stringArray(entry.columns, `${path}/columns`, budgets.columns, 1);
|
|
370
|
+
}
|
|
371
|
+
else if (entry.kind === 'metric') {
|
|
372
|
+
keys(entry, ['id', 'kind', 'datasetId', 'columnId', 'label'], ['rowIndex', 'rowSelection', 'description', 'comparisons'], path);
|
|
373
|
+
if (entry.rowSelection !== undefined) {
|
|
374
|
+
if (entry.rowSelection !== 'slice' || Object.hasOwn(entry, 'rowIndex'))
|
|
375
|
+
fail(path, 'Use either rowIndex or rowSelection: slice.');
|
|
376
|
+
}
|
|
377
|
+
else
|
|
378
|
+
integer(entry.rowIndex, `${path}/rowIndex`, 0, budgets.rows - 1);
|
|
379
|
+
selected = [string(entry.columnId, `${path}/columnId`, 256)];
|
|
380
|
+
string(entry.label, `${path}/label`, 512);
|
|
381
|
+
if (entry.description !== undefined)
|
|
382
|
+
string(entry.description, `${path}/description`, 512);
|
|
383
|
+
if (entry.comparisons !== undefined) {
|
|
384
|
+
const comparisonLabels = [];
|
|
385
|
+
array(entry.comparisons, `${path}/comparisons`, 4, 1).forEach((value, i) => {
|
|
386
|
+
const at = `${path}/comparisons/${i}`;
|
|
387
|
+
const comparison = object(value, at);
|
|
388
|
+
keys(comparison, ['label'], ['referenceColumnId', 'deltaColumnId', 'relativeColumnId', 'sentiment'], at);
|
|
389
|
+
comparisonLabels.push(string(comparison.label, `${at}/label`, 256));
|
|
390
|
+
const bindings = ['referenceColumnId', 'deltaColumnId', 'relativeColumnId'].filter((key) => comparison[key] !== undefined);
|
|
391
|
+
if (!bindings.length)
|
|
392
|
+
fail(at, 'Comparison requires at least one prepared value.');
|
|
393
|
+
for (const key of bindings)
|
|
394
|
+
selected.push(string(comparison[key], `${at}/${key}`, 256));
|
|
395
|
+
if (comparison.sentiment !== undefined &&
|
|
396
|
+
(typeof comparison.sentiment !== 'string' ||
|
|
397
|
+
!['higher-is-better', 'lower-is-better', 'neutral'].includes(comparison.sentiment)))
|
|
398
|
+
fail(`${at}/sentiment`, 'Unknown comparison sentiment.');
|
|
399
|
+
});
|
|
400
|
+
unique(comparisonLabels, `${path}/comparisons`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
else if (entry.kind === 'chart') {
|
|
404
|
+
keys(entry, ['id', 'kind', 'datasetId', 'chart', 'x', 'y', 'numericMode'], ['bindings', 'options', 'preparedViews'], path);
|
|
405
|
+
const { id: _id, kind: _kind, preparedViews, ...view } = entry;
|
|
406
|
+
parseChartViewShape(view, path);
|
|
407
|
+
selected = chartColumns(view);
|
|
408
|
+
if (preparedViews !== undefined) {
|
|
409
|
+
const ids = array(preparedViews, `${path}/preparedViews`, budgets.blocks, 1).map((value, i) => {
|
|
410
|
+
const viewPath = `${path}/preparedViews/${i}`;
|
|
411
|
+
const prepared = object(value, viewPath);
|
|
412
|
+
const { id, label, ...binding } = prepared;
|
|
413
|
+
string(id, `${viewPath}/id`, 256);
|
|
414
|
+
string(label, `${viewPath}/label`, 512);
|
|
415
|
+
parseChartViewShape(binding, viewPath);
|
|
416
|
+
const target = datasets.find((entry) => entry.id === binding.datasetId);
|
|
417
|
+
if (!target)
|
|
418
|
+
fail(`${viewPath}/datasetId`, 'Unknown dataset identifier.', 'invalid_reference');
|
|
419
|
+
if (generated)
|
|
420
|
+
validateChartView(binding, inRegion(String(entry.id))
|
|
421
|
+
? { ...target.data, rows: [] }
|
|
422
|
+
: target.data, viewPath, `/datasets/${datasets.indexOf(target)}/data`);
|
|
423
|
+
return id;
|
|
424
|
+
});
|
|
425
|
+
unique(ids, `${path}/preparedViews`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
else
|
|
429
|
+
fail(`${path}/kind`, 'Unknown presentation block.');
|
|
430
|
+
const datasetId = string(entry.datasetId, `${path}/datasetId`, 256);
|
|
431
|
+
const target = datasets.find((entry) => entry.id === datasetId);
|
|
432
|
+
if (!target)
|
|
433
|
+
fail(`${path}/datasetId`, 'Unknown dataset identifier.', 'invalid_reference');
|
|
434
|
+
if (generated) {
|
|
435
|
+
const data = target.data;
|
|
436
|
+
selected.forEach((columnId) => {
|
|
437
|
+
if (!data.columns.some((entry) => entry.id === columnId))
|
|
438
|
+
fail(path, `Unknown column ${columnId}.`, 'invalid_reference');
|
|
439
|
+
});
|
|
440
|
+
if (entry.kind === 'metric' && entry.comparisons !== undefined) {
|
|
441
|
+
for (const id of selected) {
|
|
442
|
+
if (!['int64', 'decimal', 'float64'].includes(data.columns.find((column) => column.id === id).type))
|
|
443
|
+
fail(path, 'Metric comparisons require numeric columns.');
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (entry.kind === 'metric' && Number(entry.rowIndex) >= data.rows.length)
|
|
447
|
+
fail(`${path}/rowIndex`, 'Metric must select one existing row.', 'invalid_reference');
|
|
448
|
+
if (entry.kind === 'chart') {
|
|
449
|
+
validateChartView(entry, inRegion(id) ? { ...data, rows: [] } : data, path, `/datasets/${datasets.indexOf(target)}/data`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return id;
|
|
453
|
+
});
|
|
454
|
+
unique(blockIds, '/blocks');
|
|
455
|
+
interaction(value, datasets, generated);
|
|
456
|
+
}
|
|
457
|
+
function interaction(value, datasets, generated) {
|
|
458
|
+
const blocks = value.blocks;
|
|
459
|
+
const dynamic = blocks.filter((block) => block.kind === 'metric' && block.rowSelection === 'slice');
|
|
460
|
+
if (value.interaction === undefined) {
|
|
461
|
+
if (dynamic.length)
|
|
462
|
+
fail('/interaction', 'Dynamic metrics require an interaction region.');
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const path = '/interaction';
|
|
466
|
+
const entry = object(value.interaction, path);
|
|
467
|
+
keys(entry, ['title', 'blockIds', 'filters', 'slices'], [], path);
|
|
468
|
+
string(entry.title, `${path}/title`, 512);
|
|
469
|
+
const ids = stringArray(entry.blockIds, `${path}/blockIds`, budgets.blocks, 1);
|
|
470
|
+
const targets = ids.map((id, i) => {
|
|
471
|
+
const block = blocks.find((block) => block.id === id);
|
|
472
|
+
if (!block || !('datasetId' in block))
|
|
473
|
+
fail(`${path}/blockIds/${i}`, 'Region requires existing metric, chart or table blocks.', 'invalid_reference');
|
|
474
|
+
if (block.kind === 'metric' && block.rowSelection !== 'slice')
|
|
475
|
+
fail(`${path}/blockIds/${i}`, 'Region metrics must select a slice row.');
|
|
476
|
+
return block;
|
|
477
|
+
});
|
|
478
|
+
unique(ids, `${path}/blockIds`);
|
|
479
|
+
const positions = targets.map((block) => blocks.indexOf(block));
|
|
480
|
+
if (positions.some((position, i) => position !== positions[0] + i))
|
|
481
|
+
fail(`${path}/blockIds`, 'Region blocks must be consecutive and follow document order.');
|
|
482
|
+
if (dynamic.some((block) => !ids.includes(block.id)))
|
|
483
|
+
fail(`${path}/blockIds`, 'Every dynamic metric must belong to the region.');
|
|
484
|
+
const required = new Set(targets.flatMap((block) => [
|
|
485
|
+
block.datasetId,
|
|
486
|
+
...(block.kind === 'chart' ? (block.preparedViews ?? []).map((view) => view.datasetId) : []),
|
|
487
|
+
]));
|
|
488
|
+
const filters = array(entry.filters, `${path}/filters`, budgets.columns, 1).map((item, i) => {
|
|
489
|
+
const at = `${path}/filters/${i}`;
|
|
490
|
+
const filter = object(item, at);
|
|
491
|
+
keys(filter, ['id', 'label', 'allOptionId', 'options'], [], at);
|
|
492
|
+
string(filter.id, `${at}/id`, 256);
|
|
493
|
+
string(filter.label, `${at}/label`, 512);
|
|
494
|
+
const all = string(filter.allOptionId, `${at}/allOptionId`, 256);
|
|
495
|
+
const options = array(filter.options, `${at}/options`, budgets.rows, 2).map((item, j) => {
|
|
496
|
+
const optionPath = `${at}/options/${j}`;
|
|
497
|
+
const option = object(item, optionPath);
|
|
498
|
+
keys(option, ['id', 'label'], [], optionPath);
|
|
499
|
+
string(option.label, `${optionPath}/label`, 512);
|
|
500
|
+
return string(option.id, `${optionPath}/id`, 256);
|
|
501
|
+
});
|
|
502
|
+
unique(options, `${at}/options`);
|
|
503
|
+
if (!options.includes(all))
|
|
504
|
+
fail(`${at}/allOptionId`, 'Unknown all option.', 'invalid_reference');
|
|
505
|
+
return { id: filter.id, options };
|
|
506
|
+
});
|
|
507
|
+
unique(filters.map((filter) => filter.id), `${path}/filters`);
|
|
508
|
+
const slices = array(entry.slices, `${path}/slices`, budgets.draftBytes, 1);
|
|
509
|
+
// Bound the product by the already byte-bounded slice list, without expanding combinations.
|
|
510
|
+
let combinations = 1;
|
|
511
|
+
for (const filter of filters) {
|
|
512
|
+
combinations *= filter.options.length;
|
|
513
|
+
if (combinations > slices.length)
|
|
514
|
+
fail(`${path}/slices`, 'Every selectable combination requires one slice.');
|
|
515
|
+
}
|
|
516
|
+
if (combinations !== slices.length)
|
|
517
|
+
fail(`${path}/slices`, 'Every selectable combination requires exactly one slice.');
|
|
518
|
+
const seen = new Set();
|
|
519
|
+
slices.forEach((item, i) => {
|
|
520
|
+
const at = `${path}/slices/${i}`;
|
|
521
|
+
const slice = object(item, at);
|
|
522
|
+
keys(slice, ['selection', 'datasets'], [], at);
|
|
523
|
+
const selection = object(slice.selection, `${at}/selection`);
|
|
524
|
+
keys(selection, filters.map((filter) => filter.id), [], `${at}/selection`);
|
|
525
|
+
const combination = filters.map((filter) => {
|
|
526
|
+
const selected = string(selection[filter.id], pointer(`${at}/selection`, filter.id), 256);
|
|
527
|
+
if (!filter.options.includes(selected))
|
|
528
|
+
fail(`${at}/selection`, 'Unknown filter option.', 'invalid_reference');
|
|
529
|
+
return selected;
|
|
530
|
+
});
|
|
531
|
+
const key = JSON.stringify(combination);
|
|
532
|
+
if (seen.has(key))
|
|
533
|
+
fail(`${at}/selection`, 'Duplicate filter combination.');
|
|
534
|
+
seen.add(key);
|
|
535
|
+
const bindings = array(slice.datasets, `${at}/datasets`, budgets.datasets, 1).map((item, j) => {
|
|
536
|
+
const bindingPath = `${at}/datasets/${j}`;
|
|
537
|
+
const binding = object(item, bindingPath);
|
|
538
|
+
keys(binding, ['datasetId', 'rowIndices'], [], bindingPath);
|
|
539
|
+
const id = string(binding.datasetId, `${bindingPath}/datasetId`, 256);
|
|
540
|
+
if (!required.has(id))
|
|
541
|
+
fail(`${bindingPath}/datasetId`, 'Unexpected slice dataset.', 'invalid_reference');
|
|
542
|
+
const data = datasets.find((dataset) => dataset.id === id)?.data;
|
|
543
|
+
const rows = array(binding.rowIndices, `${bindingPath}/rowIndices`, budgets.rows).map((row, k) => integer(row, `${bindingPath}/rowIndices/${k}`, 0, budgets.rows - 1));
|
|
544
|
+
if (new Set(rows).size !== rows.length)
|
|
545
|
+
fail(`${bindingPath}/rowIndices`, 'Duplicate row index.');
|
|
546
|
+
if (generated && rows.some((row) => row >= data.rows.length))
|
|
547
|
+
fail(`${bindingPath}/rowIndices`, 'Slice references a missing row.', 'invalid_reference');
|
|
548
|
+
if (dynamic.some((block) => 'datasetId' in block && block.datasetId === id) &&
|
|
549
|
+
rows.length !== 1)
|
|
550
|
+
fail(`${bindingPath}/rowIndices`, 'Dynamic metrics require exactly one prepared row per slice.');
|
|
551
|
+
return id;
|
|
552
|
+
});
|
|
553
|
+
unique(bindings, `${at}/datasets`);
|
|
554
|
+
if (bindings.length !== required.size)
|
|
555
|
+
fail(`${at}/datasets`, 'Slice must cover every region dataset and prepared view.');
|
|
556
|
+
if (generated) {
|
|
557
|
+
for (const block of targets) {
|
|
558
|
+
if (block.kind !== 'chart')
|
|
559
|
+
continue;
|
|
560
|
+
for (const view of [block, ...(block.preparedViews ?? [])]) {
|
|
561
|
+
const target = datasets.find((dataset) => dataset.id === view.datasetId);
|
|
562
|
+
const binding = slice.datasets.find((binding) => binding.datasetId === view.datasetId);
|
|
563
|
+
validateChartView(view, target.data, at, `/datasets/${datasets.indexOf(target)}/data`, binding.rowIndices);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
export function parsePresentationDraft(value) {
|
|
570
|
+
bytes(value, budgets.draftBytes);
|
|
571
|
+
const entry = object(value, '');
|
|
572
|
+
keys(entry, ['schemaVersion', 'title', 'datasets', 'sources', 'blocks'], ['interaction'], '');
|
|
573
|
+
common(entry, false);
|
|
574
|
+
return entry;
|
|
575
|
+
}
|
|
576
|
+
export function parsePresentationDocument(value) {
|
|
577
|
+
bytes(value, budgets.documentBytes);
|
|
578
|
+
const entry = object(value, '');
|
|
579
|
+
keys(entry, [
|
|
580
|
+
'schemaVersion',
|
|
581
|
+
'workspaceId',
|
|
582
|
+
'reportId',
|
|
583
|
+
'buildId',
|
|
584
|
+
'title',
|
|
585
|
+
'generatedAt',
|
|
586
|
+
'datasets',
|
|
587
|
+
'sources',
|
|
588
|
+
'blocks',
|
|
589
|
+
'diagnostics',
|
|
590
|
+
], ['interaction'], '');
|
|
591
|
+
string(entry.workspaceId, '/workspaceId', 512);
|
|
592
|
+
parsePresentationBuildId(entry.reportId, '/reportId');
|
|
593
|
+
parsePresentationBuildId(entry.buildId, '/buildId');
|
|
594
|
+
date(entry.generatedAt, '/generatedAt', true);
|
|
595
|
+
array(entry.diagnostics, '/diagnostics', 128).forEach((value, i) => {
|
|
596
|
+
const path = `/diagnostics/${i}`;
|
|
597
|
+
const diagnostic = object(value, path);
|
|
598
|
+
keys(diagnostic, ['code', 'path', 'message'], [], path);
|
|
599
|
+
string(diagnostic.code, `${path}/code`, 128);
|
|
600
|
+
string(diagnostic.message, `${path}/message`, 1024);
|
|
601
|
+
if (typeof diagnostic.path !== 'string' || !/^(?:\/(?:[^~]|~[01])*)*$/.test(diagnostic.path)) {
|
|
602
|
+
fail(`${path}/path`, 'Expected a JSON pointer.');
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
common(entry, true);
|
|
606
|
+
return entry;
|
|
607
|
+
}
|
|
608
|
+
export function parsePresentationReceipt(value) {
|
|
609
|
+
bytes(value, 16 * 1024);
|
|
610
|
+
const entry = object(value, '');
|
|
611
|
+
keys(entry, ['schemaVersion', 'kind', 'workspaceId', 'reportId', 'buildId', 'title', 'summary', 'files'], [], '');
|
|
612
|
+
version(entry, '', 2);
|
|
613
|
+
if (entry.kind !== 'marivo.presentation')
|
|
614
|
+
fail('/kind', 'Expected marivo.presentation receipt.');
|
|
615
|
+
string(entry.workspaceId, '/workspaceId', 512);
|
|
616
|
+
const reportId = parsePresentationBuildId(entry.reportId, '/reportId');
|
|
617
|
+
const id = parsePresentationBuildId(entry.buildId, '/buildId');
|
|
618
|
+
string(entry.title, '/title', 512);
|
|
619
|
+
string(entry.summary, '/summary', 2048);
|
|
620
|
+
const files = object(entry.files, '/files');
|
|
621
|
+
keys(files, ['document', 'html'], [], '/files');
|
|
622
|
+
for (const [key, asset, max] of [
|
|
623
|
+
['document', 'presentation.json', budgets.documentBytes],
|
|
624
|
+
['html', 'index.html', budgets.htmlBytes],
|
|
625
|
+
]) {
|
|
626
|
+
const path = `/files/${key}`;
|
|
627
|
+
const file = object(files[key], path);
|
|
628
|
+
keys(file, ['asset', 'path', 'sha256', 'bytes'], [], path);
|
|
629
|
+
if (file.asset !== asset)
|
|
630
|
+
fail(`${path}/asset`, 'Unexpected fixed asset name.');
|
|
631
|
+
const filePath = string(file.path, `${path}/path`, 4096);
|
|
632
|
+
if (!filePath.startsWith('/') ||
|
|
633
|
+
filePath.includes('\\') ||
|
|
634
|
+
filePath
|
|
635
|
+
.split('/')
|
|
636
|
+
.slice(1)
|
|
637
|
+
.some((segment) => !segment || segment === '.' || segment === '..') ||
|
|
638
|
+
!filePath.endsWith(`/.dsh-data-analysis/presentations/${reportId}/builds/${id}/${asset}`)) {
|
|
639
|
+
fail(`${path}/path`, 'Asset path must identify this build in its Workspace.', 'file_boundary');
|
|
640
|
+
}
|
|
641
|
+
if (typeof file.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(file.sha256))
|
|
642
|
+
fail(`${path}/sha256`, 'Expected a lowercase SHA-256 digest.');
|
|
643
|
+
integer(file.bytes, `${path}/bytes`, 1, max);
|
|
644
|
+
}
|
|
645
|
+
const documentPath = String(object(files.document, '/files/document').path);
|
|
646
|
+
const htmlPath = String(object(files.html, '/files/html').path);
|
|
647
|
+
if (documentPath.slice(0, -'presentation.json'.length) !== htmlPath.slice(0, -'index.html'.length)) {
|
|
648
|
+
fail('/files/html/path', 'Both files must belong to the same build directory.', 'file_boundary');
|
|
649
|
+
}
|
|
650
|
+
return entry;
|
|
651
|
+
}
|