Line data Source code
1 : import type * as dashql from '@ankoh/dashql-core';
2 :
3 : import { ConnectorType } from '../../connections/connector_info.js';
4 : import { SNIPPET_DATASETS_FOLDER, type Snippet } from './snippet.js';
5 : import { parseSnippetFile, type SnippetFile } from './snippet_header.js';
6 :
7 : /// The snippets of the snippet browser
8 : export interface SnippetRegistry {
9 : /// The snippets, the datasets first, then ordered by path
10 : snippets: Snippet[];
11 : /// The snippets by id
12 : byId: ReadonlyMap<string, Snippet>;
13 : /// The problems of the snippet files, the affected snippets are left out
14 : errors: string[];
15 : }
16 :
17 : /// The snippet files of examples/snippets, bundled as text. The Vite config aliases the folder.
18 1 : const SNIPPET_FILES = import.meta.glob<string>('@dashql/snippets/**/*.sql', {
19 : query: '?raw',
20 : import: 'default',
21 : eager: true,
22 1 : });
23 : /// The snippet files of the extensions, `<extension>/snippets/<folders>/<name>.sql`
24 1 : const EXTENSION_SNIPPET_FILES = import.meta.glob<string>('@dashql/extensions/*/snippets/**/*.sql', {
25 : query: '?raw',
26 : import: 'default',
27 : eager: true,
28 1 : });
29 :
30 : /// The text of a snippet for a connector
31 1 : export function snippetText(snippet: Snippet, connector: ConnectorType): string {
32 1 : const body = snippet.bodies.get(connector);
33 1 : if (body == null) {
34 0 : throw new Error(`snippet ${snippet.id} does not support connector ${ConnectorType[connector]}`);
35 : }
36 1 : return `${body}\n`;
37 1 : }
38 :
39 : /// Merge the files of one snippet, the engine variants of a snippet share its id
40 1 : function mergeSnippetFiles(files: SnippetFile[]): Snippet {
41 1 : const bodies = new Map<ConnectorType, string>();
42 1 : const paths = new Map<ConnectorType, string>();
43 1 : for (const file of files) {
44 1 : for (const connector of file.connectors) {
45 1 : bodies.set(connector, file.body);
46 1 : paths.set(connector, file.path);
47 1 : }
48 : }
49 1 : return {
50 1 : id: files[0].id,
51 1 : category: files[0].category,
52 1 : connectors: [...bodies.keys()],
53 1 : bodies,
54 1 : paths,
55 1 : };
56 1 : }
57 :
58 : /// Compare the ids of snippets by their path segments, so that `d360` sorts before `d360-api`
59 1 : function compareSnippetIds(left: string, right: string): number {
60 1 : const l = left.split('/');
61 1 : const r = right.split('/');
62 1 : for (let i = 0; i < Math.min(l.length, r.length); ++i) {
63 1 : const order = l[i].localeCompare(r[i]);
64 1 : if (order !== 0) return order;
65 1 : }
66 0 : return l.length - r.length;
67 1 : }
68 :
69 : /// Build the registry from the raw snippet files by path, starting at their root folders
70 1 : export function buildSnippetRegistry(core: dashql.DashQL, rawFiles: Record<string, string>): SnippetRegistry {
71 1 : const errors: string[] = [];
72 :
73 : // Parse the files and group the engine variants of a snippet
74 1 : const filesById = new Map<string, SnippetFile[]>();
75 1 : for (const [path, raw] of Object.entries(rawFiles).sort(([l], [r]) => l.localeCompare(r))) {
76 1 : try {
77 1 : const file = parseSnippetFile(core, path, raw);
78 1 : const files = filesById.get(file.id) ?? [];
79 1 : files.push(file);
80 1 : filesById.set(file.id, files);
81 1 : } catch (error) {
82 1 : errors.push(String(error instanceof Error ? error.message : error));
83 1 : }
84 : }
85 1 : const byId = new Map([...filesById].map(([id, files]) => [id, mergeSnippetFiles(files)]));
86 1 : const datasetsFirst = (snippet: Snippet) => snippet.category === SNIPPET_DATASETS_FOLDER ? 0 : 1;
87 1 : const snippets = [...byId.values()].sort((l, r) => (datasetsFirst(l) - datasetsFirst(r)) || compareSnippetIds(l.id, r.id));
88 1 : return { snippets, byId, errors };
89 1 : }
90 :
91 : /// The bundled snippet files by path, starting at their root folders
92 1 : export function bundledSnippetFiles(): Record<string, string> {
93 1 : const files: Record<string, string> = {};
94 : // The depth below the folders varies, and the paths of the folders differ between the dev server and a build
95 1 : const folder = '/snippets/';
96 1 : for (const [path, raw] of Object.entries(SNIPPET_FILES)) {
97 1 : files[path.slice(path.indexOf(folder) + folder.length)] = raw;
98 : }
99 1 : for (const [path, raw] of Object.entries(EXTENSION_SNIPPET_FILES)) {
100 1 : const begin = path.indexOf(folder);
101 1 : const extension = path.slice(path.lastIndexOf('/', begin - 1) + 1, begin);
102 1 : files[`${extension}/${path.slice(begin + folder.length)}`] = raw;
103 1 : }
104 1 : return files;
105 1 : }
106 :
107 : /// The registries of the bundled snippets by core instance
108 1 : const BUNDLED_REGISTRIES = new WeakMap<dashql.DashQL, SnippetRegistry>();
109 :
110 : /// The bundled snippets, parsed once per core instance
111 1 : export function getBundledSnippets(core: dashql.DashQL): SnippetRegistry {
112 1 : let registry = BUNDLED_REGISTRIES.get(core);
113 1 : if (registry == null) {
114 1 : registry = buildSnippetRegistry(core, bundledSnippetFiles());
115 1 : BUNDLED_REGISTRIES.set(core, registry);
116 1 : }
117 1 : return registry;
118 1 : }
119 :
120 : /// Does a snippet run on a connector?
121 1 : export function snippetSupports(snippet: Snippet, connector: ConnectorType): boolean {
122 1 : return snippet.bodies.has(connector);
123 1 : }
124 :
125 : /// The snippets that run on a connector, in the order of the registry
126 1 : export function snippetsForConnector(registry: SnippetRegistry, connector: ConnectorType): Snippet[] {
127 1 : return registry.snippets.filter(snippet => snippetSupports(snippet, connector));
128 1 : }
|