Adds autorest-based SQL Project generation to SQL Database Projects extension (#17078)

* Initial changes

* checkpoint

* Constructing project with post deployment script

* Correcting to intentionally read from cached list of projects

* Adding activation event, fixing fresh workspace bug

* Convert netcoreTool and autorestHelper to share a helper class for streamed command

* Include npm package version to force update

* test checkpoint

* Unit tests

* Added contextual quickpicks for autorest dialogs

* Adding projectController test

* Added projectController test, some refactoring for testability

* Merge branch 'main' into benjin/autorest

* Fixing 'which' import

* PR feedback

* Fixing tests

* Adding additional information for when project provider tests fail

* Hopefully fixing failing tests (unable to repro locally)

* Adding Generate Project item to workspace menu

* PR feedback
This commit is contained in:
Benjin Dubishar
2021-09-16 20:38:40 -07:00
committed by GitHub
parent 0cf1abc7c2
commit 08e15bce99
18 changed files with 586 additions and 85 deletions

View File

@@ -24,7 +24,8 @@ import { IDeploySettings } from '../models/IDeploySettings';
import { BaseProjectTreeItem } from '../models/tree/baseTreeItem';
import { ProjectRootTreeItem } from '../models/tree/projectTreeItem';
import { ImportDataModel } from '../models/api/import';
import { NetCoreTool, DotNetCommandOptions, DotNetError } from '../tools/netcoreTool';
import { NetCoreTool, DotNetError } from '../tools/netcoreTool';
import { ShellCommandOptions } from '../tools/shellExecutionHelper';
import { BuildHelper } from '../tools/buildHelper';
import { readPublishProfile } from '../models/publishProfile/publishProfile';
import { AddDatabaseReferenceDialog } from '../dialogs/addDatabaseReferenceDialog';
@@ -38,6 +39,7 @@ import { launchPublishDatabaseQuickpick } from '../dialogs/publishDatabaseQuickp
import { launchPublishToDockerContainerQuickpick } from '../dialogs/deployDatabaseQuickpick';
import { DeployService } from '../models/deploy/deployService';
import { SqlTargetPlatform } from 'sqldbproj';
import { AutorestHelper } from '../tools/autorestHelper';
import { createNewProjectFromDatabaseWithQuickpick } from '../dialogs/createProjectFromDatabaseQuickpick';
import { addDatabaseReferenceQuickpick } from '../dialogs/addDatabaseReferenceQuickpick';
@@ -67,6 +69,7 @@ export class ProjectsController {
private buildInfo: DashboardData[] = [];
private publishInfo: PublishData[] = [];
private deployService: DeployService;
private autorestHelper: AutorestHelper;
projFileWatchers = new Map<string, vscode.FileSystemWatcher>();
@@ -74,6 +77,7 @@ export class ProjectsController {
this.netCoreTool = new NetCoreTool(outputChannel);
this.buildHelper = new BuildHelper();
this.deployService = new DeployService(outputChannel);
this.autorestHelper = new AutorestHelper(outputChannel);
}
public getDashboardPublishData(projectFile: string): (string | dataworkspace.IconCellValue)[][] {
@@ -215,7 +219,7 @@ export class ProjectsController {
// Check mssql extension for project dlls (tracking issue #10273)
await this.buildHelper.createBuildDirFolder();
const options: DotNetCommandOptions = {
const options: ShellCommandOptions = {
commandTitle: 'Build',
workingDirectory: project.projectFolderPath,
argument: this.buildHelper.constructBuildArguments(project.projectFilePath, this.buildHelper.extensionBuildDirPath)
@@ -827,6 +831,171 @@ export class ProjectsController {
return result;
}
public async selectAutorestSpecFile(): Promise<string | undefined> {
let quickpickSelection = await vscode.window.showQuickPick(
[constants.browseEllipsis],
{ title: constants.selectSpecFile, ignoreFocusOut: true });
if (!quickpickSelection) {
return;
}
const filters: { [name: string]: string[] } = {};
filters['OpenAPI/Swagger spec'] = ['yaml'];
let uris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
openLabel: constants.selectString,
filters: filters,
title: constants.selectSpecFile
});
if (!uris) {
return;
}
return uris[0].fsPath;
}
/**
* @returns \{ newProjectFolder: 'C:\Source\MyProject',
* outputFolder: 'C:\Source',
* projectName: 'MyProject'}
*/
public async selectAutorestProjectLocation(specPath: string): Promise<{ newProjectFolder: string, outputFolder: string, projectName: string } | undefined> {
let valid = false;
let newProjectFolder: string = '';
let outputFolder: string = '';
let projectName: string = '';
let quickpickSelection = await vscode.window.showQuickPick(
[constants.browseEllipsis],
{ title: constants.selectProjectLocation, ignoreFocusOut: true });
if (!quickpickSelection) {
return;
}
while (!valid) {
const folders = await vscode.window.showOpenDialog({
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
openLabel: constants.selectString,
defaultUri: vscode.workspace.workspaceFolders?.[0]?.uri,
title: constants.selectProjectLocation
});
if (!folders) {
return;
}
outputFolder = folders[0].fsPath;
projectName = path.basename(specPath, constants.yamlFileExtension);
newProjectFolder = path.join(outputFolder, projectName);
if (await utils.exists(newProjectFolder)) {
quickpickSelection = await vscode.window.showQuickPick(
[constants.browseEllipsis],
{ title: constants.folderAlreadyExistsChooseNewLocation(newProjectFolder), ignoreFocusOut: true });
if (!quickpickSelection) {
return;
}
} else {
valid = true;
}
}
await fs.mkdir(newProjectFolder);
return { newProjectFolder, outputFolder, projectName };
}
public async generateAutorestFiles(specPath: string, newProjectFolder: string): Promise<void> {
await this.autorestHelper.generateAutorestFiles(specPath, newProjectFolder);
}
public async openProjectInWorkspace(projectFilePath: string): Promise<void> {
const workspaceApi = utils.getDataWorkspaceExtensionApi();
await workspaceApi.validateWorkspace();
await workspaceApi.addProjectsToWorkspace([vscode.Uri.file(projectFilePath)]);
workspaceApi.showProjectsView();
}
public async generateProjectFromOpenApiSpec(): Promise<Project | undefined> {
try {
// 1. select spec file
const specPath: string | undefined = await this.selectAutorestSpecFile();
if (!specPath) {
return;
}
// 2. select location, make new folder
const projectInfo = await this.selectAutorestProjectLocation(specPath!);
if (!projectInfo) {
return;
}
// 3. run AutoRest to generate .sql files
await this.generateAutorestFiles(specPath, projectInfo.newProjectFolder);
// 4. create new SQL project
const newProjFilePath = await this.createNewProject({
newProjName: projectInfo.projectName,
folderUri: vscode.Uri.file(projectInfo.outputFolder),
projectTypeId: constants.emptySqlDatabaseProjectTypeId
});
const project = await Project.openProject(newProjFilePath);
// 5. add generated files to SQL project
let fileFolderList: vscode.Uri[] = await this.getSqlFileList(project.projectFolderPath);
await project.addToProject(fileFolderList.filter(f => !f.fsPath.endsWith(constants.autorestPostDeploymentScriptName))); // Add generated file structure to the project
const postDeploymentScript: vscode.Uri | undefined = this.findPostDeploymentScript(fileFolderList);
if (postDeploymentScript) {
await project.addScriptItem(path.relative(project.projectFolderPath, postDeploymentScript.fsPath), undefined, templates.postDeployScript);
}
// 6. add project to workspace and open
await this.openProjectInWorkspace(newProjFilePath);
return project;
} catch (err) {
void vscode.window.showErrorMessage(`${constants.generatingProjectFailed}: ${utils.getErrorMessage(err)}`);
return;
}
}
private findPostDeploymentScript(files: vscode.Uri[]): vscode.Uri | undefined {
const results = files.filter(f => f.fsPath.endsWith(constants.autorestPostDeploymentScriptName));
switch (results.length) {
case 0:
return undefined;
case 1:
return results[0];
default:
throw new Error(constants.multipleMostDeploymentScripts(results.length));
}
}
private async getSqlFileList(folder: string): Promise<vscode.Uri[]> {
const entries = await fs.readdir(folder, { withFileTypes: true });
const folders = entries.filter(dir => dir.isDirectory()).map(dir => path.join(folder, dir.name));
const files = entries.filter(file => !file.isDirectory() && path.extname(file.name) === '.sql').map(file => vscode.Uri.file(path.join(folder, file.name)));
for (const folder of folders) {
files.push(...await this.getSqlFileList(folder));
}
return files;
}
//#region Helper methods
public getPublishDialog(project: Project): PublishDatabaseDialog {