Line data Source code
1 : import type * as app_session from '@ankoh/dashql-jsonschema/app_session.js';
2 : import type { TopLevelSpec } from 'vega-lite';
3 :
4 : import type * as core_buffers from '@ankoh/dashql-core/buffers';
5 : import { randomScriptName } from './script_name.js';
6 :
7 : export interface DashboardSpec {
8 : row?: number | null;
9 : column?: number | null;
10 : width?: number | null;
11 : height?: number | null;
12 : }
13 :
14 : /// Script collection type definitions.
15 : ///
16 : /// These types represent the runtime state of the session's scripts.
17 : /// Storage format types (SessionMetadata, SessionOriginType) come from JSON Schema.
18 :
19 : // Re-export storage format types from JSON Schema
20 : export type SessionMetadata = app_session.SessionMetadata;
21 : export type SessionOriginType = NonNullable<app_session.SessionMetadata['originType']>;
22 :
23 : /// A query with `@vis` tags resolved against the session. Discriminated on the
24 : /// renderer named by the tag: `vegalite` carries a parsed Vega-Lite spec,
25 : /// `markdown` carries the compiled row template.
26 : /// All carry the SQL to execute against the backend and the statement with the tags.
27 : export type ResolvedVisualizeQuery =
28 : | {
29 : renderer: 'vegalite';
30 : // The SQL to execute against the backend
31 : sql: string;
32 : // The statement whose @vis tags describe the visualization
33 : statementId: number;
34 : // The Vega-Lite spec generated by the analyzer
35 : vegaLiteSpec: TopLevelSpec;
36 : dashboardSpec?: DashboardSpec | null;
37 : }
38 : | {
39 : renderer: 'markdown';
40 : // The SQL to execute against the backend
41 : sql: string;
42 : // The statement whose @vis tags describe the visualization
43 : statementId: number;
44 : // The row template compiled by dashql-core.
45 : markdownSpec: core_buffers.visualization.MarkdownSpecT;
46 : dashboardSpec?: DashboardSpec | null;
47 : }
48 : | {
49 : renderer: 'table';
50 : // The SQL to execute against the backend
51 : sql: string;
52 : // The statement whose @vis tags describe the visualization
53 : statementId: number;
54 : dashboardSpec?: DashboardSpec | null;
55 : };
56 :
57 : /// Script annotations derived from analysis
58 : export interface ScriptAnnotations {
59 : // The table definitions
60 : tableDefs: string[];
61 : // The resolved visualization of this script (its @vis tags), if any.
62 : visualizeQuery: ResolvedVisualizeQuery | null;
63 : // The placement of the script on the dataflow canvas, its `@flow` tag, if any: a position, a size, or both.
64 : flowPosition?: FlowPosition | null;
65 : // The dashboard placement of the script, the `@board` tag of its last statement, if any. A script without a
66 : // visualization is a collapsed card on the board.
67 : boardPosition?: BoardPosition | null;
68 : // Is the last statement a query? Only a query is visualized.
69 : lastStatementIsQuery?: boolean;
70 : // Does the script run only on request, its `@manual` tag? Execute All and input changes leave it out.
71 : manual?: boolean;
72 : }
73 :
74 : /// The dashboard placement of a script
75 : export interface BoardPosition extends DashboardSpec {
76 : // The statement whose annotation holds the `@board` tag
77 : statementId: number;
78 : }
79 :
80 : /// The position of a script on the dataflow canvas
81 : export interface FlowPosition {
82 : // The statement whose annotation holds the `@flow` tag
83 : statementId: number;
84 : // The top left corner of the box, both null if the layout places the box
85 : x: number | null;
86 : y: number | null;
87 : // The width of the box, null for the default width
88 : width: number | null;
89 : // The height of the chart of the box, null for the default height
90 : height: number | null;
91 : }
92 :
93 : /// A reference in the session's flat ordered script collection.
94 : export interface ScriptRef {
95 : // The script id
96 : scriptId: number;
97 : // The file name (e.g., "extract.sql")
98 : fileName: string;
99 : }
100 :
101 : /// Helper to create empty annotations
102 1 : export function createEmptyAnnotations(): ScriptAnnotations {
103 1 : return {
104 1 : tableDefs: [],
105 1 : visualizeQuery: null,
106 1 : flowPosition: null,
107 1 : boardPosition: null,
108 1 : lastStatementIsQuery: false,
109 1 : };
110 1 : }
111 :
112 : /// Helper to create empty metadata
113 1 : export function createEmptyMetadata(): SessionMetadata {
114 1 : return {
115 1 : originType: 'LOCAL',
116 1 : originalFileName: '',
117 1 : originalHttpUrl: '',
118 1 : };
119 1 : }
120 :
121 : /// Trailing ".sql" extension, stripped for display.
122 1 : const SCRIPT_EXTENSION_RE = /\.sql$/i;
123 :
124 : /// The display name of a script, its file name without the ".sql" extension.
125 : /// "01_extract.sql" -> "01_extract". Used for tab/feed/details labels and to pre-fill the rename input.
126 1 : export function scriptDisplayName(fileName: string): string {
127 1 : return fileName.replace(SCRIPT_EXTENSION_RE, '');
128 1 : }
129 :
130 : /// Return a script base name (no extension) that is unique among the display names of the existing
131 : /// scripts and the other names of table parameters, e.g. those of attachments, suffixing "-2", "-3", ... on
132 : /// collision. Names are compared in lower case, like the core finds a table parameter.
133 1 : export function uniqueScriptBase(
134 1 : base: string,
135 1 : existingScripts: { [fileName: string]: ScriptRef },
136 1 : excludeFileName: string = '',
137 1 : takenNames: Iterable<string> = [],
138 1 : ): string {
139 1 : const taken = new Set<string>();
140 1 : for (const key of Object.keys(existingScripts)) {
141 1 : if (key === excludeFileName) continue;
142 1 : taken.add(scriptDisplayName(key).toLowerCase());
143 1 : }
144 1 : for (const name of takenNames) taken.add(name.toLowerCase());
145 1 : let candidate = base;
146 1 : for (let suffix = 2; taken.has(candidate.toLowerCase()); ++suffix) {
147 1 : candidate = `${base}-${suffix}`;
148 : }
149 1 : return candidate;
150 1 : }
151 :
152 : /// Helper to generate a script file name that doesn't collide with existing entries, a random
153 : /// "<adjective>_<animal>" name by default or the explicit `base` if given, e.g. "brave_otter.sql".
154 : /// The order of the scripts lives in the session file, not in the file names.
155 1 : export function generateScriptFileName(
156 1 : existingScripts: { [fileName: string]: ScriptRef },
157 1 : base?: string,
158 1 : takenNames: Iterable<string> = [],
159 1 : ): string {
160 1 : const taken = [...takenNames];
161 1 : const requested = base ?? randomScriptName(new Set([...Object.keys(existingScripts).map(scriptDisplayName), ...taken]));
162 1 : return `${uniqueScriptBase(requested, existingScripts, '', taken)}.sql`;
163 1 : }
164 :
165 : /// Helper to create a committed script entry.
166 1 : export function createScriptRef(scriptId: number, fileName: string): ScriptRef {
167 1 : return {
168 1 : scriptId,
169 1 : fileName,
170 1 : };
171 1 : }
|