LCOV - code coverage report
Current view: top level - dashql-app/src/session/snippets - snippet_registry.ts Coverage Total Hit
Test: DashQL d805a4b Lines: 97.5 % 79 77
Test Date: 2026-11-09 20:22:13

            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 : }
        

Generated by: LCOV version 2.0-1