55 ViewColumn ,
66 window ,
77 workspace ,
8+ WorkspaceFolder ,
89} from "vscode" ;
910import { AbstractWebview , WebviewPanelConfig } from "../abstract-webview" ;
1011import {
@@ -20,9 +21,12 @@ import {
2021import { extLogger } from "../common" ;
2122import { readFile , writeFile } from "fs-extra" ;
2223import { load as loadYaml } from "js-yaml" ;
23- import { DatabaseItem } from "../local-databases" ;
24+ import { DatabaseItem , DatabaseManager } from "../local-databases" ;
2425import { CodeQLCliServer } from "../cli" ;
2526import { asError , assertNever , getErrorMessage } from "../pure/helpers-pure" ;
27+ import { generateFlowModel } from "./generate-flow-model" ;
28+ import { promptImportGithubDatabase } from "../databaseFetcher" ;
29+ import { App } from "../common/app" ;
2630import { ResolvableLocationValue } from "../pure/bqrs-cli-types" ;
2731import { showResolvableLocation } from "../interface-utils" ;
2832import { decodeBqrsToExternalApiUsages } from "./bqrs" ;
@@ -32,12 +36,27 @@ import { createDataExtensionYaml, loadDataExtensionYaml } from "./yaml";
3236import { ExternalApiUsage } from "./external-api-usage" ;
3337import { ModeledMethod } from "./modeled-method" ;
3438
39+ function getQlSubmoduleFolder ( ) : WorkspaceFolder | undefined {
40+ const workspaceFolder = workspace . workspaceFolders ?. find (
41+ ( folder ) => folder . name === "ql" ,
42+ ) ;
43+ if ( ! workspaceFolder ) {
44+ void extLogger . log ( "No workspace folder 'ql' found" ) ;
45+
46+ return ;
47+ }
48+
49+ return workspaceFolder ;
50+ }
51+
3552export class DataExtensionsEditorView extends AbstractWebview <
3653 ToDataExtensionsEditorMessage ,
3754 FromDataExtensionsEditorMessage
3855> {
3956 public constructor (
4057 ctx : ExtensionContext ,
58+ private readonly app : App ,
59+ private readonly databaseManager : DatabaseManager ,
4160 private readonly cliServer : CodeQLCliServer ,
4261 private readonly queryRunner : QueryRunner ,
4362 private readonly queryStorageDir : string ,
@@ -86,6 +105,10 @@ export class DataExtensionsEditorView extends AbstractWebview<
86105 ) ;
87106 await this . loadExternalApiUsages ( ) ;
88107
108+ break ;
109+ case "generateExternalApi" :
110+ await this . generateModeledMethods ( ) ;
111+
89112 break ;
90113 default :
91114 assertNever ( msg ) ;
@@ -158,8 +181,8 @@ export class DataExtensionsEditorView extends AbstractWebview<
158181 }
159182
160183 await this . postMessage ( {
161- t : "setExistingModeledMethods " ,
162- existingModeledMethods,
184+ t : "addModeledMethods " ,
185+ modeledMethods : existingModeledMethods ,
163186 } ) ;
164187 } catch ( e : unknown ) {
165188 void extLogger . log ( `Unable to read data extension YAML: ${ e } ` ) ;
@@ -225,13 +248,106 @@ export class DataExtensionsEditorView extends AbstractWebview<
225248 }
226249 }
227250
251+ protected async generateModeledMethods ( ) : Promise < void > {
252+ const tokenSource = new CancellationTokenSource ( ) ;
253+
254+ const selectedDatabase = this . databaseManager . currentDatabaseItem ;
255+
256+ // The external API methods are in the library source code, so we need to ask
257+ // the user to import the library database. We need to have the database
258+ // imported to the query server, so we need to register it to our workspace.
259+ const database = await promptImportGithubDatabase (
260+ this . app . commands ,
261+ this . databaseManager ,
262+ this . app . workspaceStoragePath ?? this . app . globalStoragePath ,
263+ this . app . credentials ,
264+ ( update ) => this . showProgress ( update ) ,
265+ tokenSource . token ,
266+ this . cliServer ,
267+ ) ;
268+ if ( ! database ) {
269+ await this . clearProgress ( ) ;
270+ void extLogger . log ( "No database chosen" ) ;
271+
272+ return ;
273+ }
274+
275+ // The library database was set as the current database by importing it,
276+ // but we need to set it back to the originally selected database.
277+ await this . databaseManager . setCurrentDatabaseItem ( selectedDatabase ) ;
278+
279+ const workspaceFolder = getQlSubmoduleFolder ( ) ;
280+ if ( ! workspaceFolder ) {
281+ return ;
282+ }
283+
284+ await this . showProgress ( {
285+ step : 0 ,
286+ maxStep : 4000 ,
287+ message : "Generating modeled methods for library" ,
288+ } ) ;
289+
290+ try {
291+ await generateFlowModel ( {
292+ cliServer : this . cliServer ,
293+ queryRunner : this . queryRunner ,
294+ queryStorageDir : this . queryStorageDir ,
295+ qlDir : workspaceFolder . uri . fsPath ,
296+ databaseItem : database ,
297+ onResults : async ( results ) => {
298+ const modeledMethodsByName : Record < string , ModeledMethod > = { } ;
299+
300+ for ( const result of results ) {
301+ modeledMethodsByName [ result . signature ] = result . modeledMethod ;
302+ }
303+
304+ await this . postMessage ( {
305+ t : "addModeledMethods" ,
306+ modeledMethods : modeledMethodsByName ,
307+ overrideNone : true ,
308+ } ) ;
309+ } ,
310+ progress : ( update ) => this . showProgress ( update ) ,
311+ token : tokenSource . token ,
312+ } ) ;
313+ } catch ( e : unknown ) {
314+ void showAndLogExceptionWithTelemetry (
315+ redactableError (
316+ asError ( e ) ,
317+ ) `Failed to generate flow model: ${ getErrorMessage ( e ) } ` ,
318+ ) ;
319+ }
320+
321+ // After the flow model has been generated, we can remove the temporary database
322+ // which we used for generating the flow model.
323+ await this . databaseManager . removeDatabaseItem (
324+ ( ) =>
325+ this . showProgress ( {
326+ step : 3900 ,
327+ maxStep : 4000 ,
328+ message : "Removing temporary database" ,
329+ } ) ,
330+ tokenSource . token ,
331+ database ,
332+ ) ;
333+
334+ await this . clearProgress ( ) ;
335+ }
336+
228337 /*
229338 * Progress in this class is a bit weird. Most of the progress is based on running the query.
230339 * Query progress is always between 0 and 1000. However, we still have some steps that need
231340 * to be done after the query has finished. Therefore, the maximum step is 1500. This captures
232341 * that there's 1000 steps of the query progress since that takes the most time, and then
233342 * an additional 500 steps for the rest of the work. The progress doesn't need to be 100%
234343 * accurate, so this is just a rough estimate.
344+ *
345+ * For generating the modeled methods for an external library, the max step is 4000. This is
346+ * based on the following steps:
347+ * - 1000 for the summary model
348+ * - 1000 for the sink model
349+ * - 1000 for the source model
350+ * - 1000 for the neutral model
235351 */
236352 private async showProgress ( update : ProgressUpdate , maxStep ?: number ) {
237353 await this . postMessage ( {
@@ -251,12 +367,8 @@ export class DataExtensionsEditorView extends AbstractWebview<
251367 }
252368
253369 private calculateModelFilename ( ) : string | undefined {
254- const workspaceFolder = workspace . workspaceFolders ?. find (
255- ( folder ) => folder . name === "ql" ,
256- ) ;
370+ const workspaceFolder = getQlSubmoduleFolder ( ) ;
257371 if ( ! workspaceFolder ) {
258- void extLogger . log ( "No workspace folder 'ql' found" ) ;
259-
260372 return ;
261373 }
262374
0 commit comments