mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-13 17:22:15 -05:00
Add more SsmsMin interactions (#5267)
* Add support for more SsmsMin property dialogs and the Generate Script Wizard. Also fixed bug with ExtHostObjectExplorerNode getParent function * Localize package.json entries * Fix localization tokens * Address PR comments * Fix regex and getParent
This commit is contained in:
@@ -1,61 +1,75 @@
|
||||
{
|
||||
"name": "admin-tool-ext-win",
|
||||
"displayName": "Database Admin Tool Extensions for Windows",
|
||||
"description": "Adds additional Windows-specific functionality to Azure Data Studio",
|
||||
"version": "0.0.1",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt",
|
||||
"icon": "images/sqlserver.png",
|
||||
"aiKey": "AIF-5574968e-856d-40d2-af67-c89a14e76412",
|
||||
"engines": {
|
||||
"vscode": "^1.30.1",
|
||||
"sqlops": "*"
|
||||
},
|
||||
"activationEvents": [
|
||||
"*"
|
||||
],
|
||||
"main": "./out/main",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Microsoft/azuredatastudio.git"
|
||||
},
|
||||
"extensionDependencies": [
|
||||
"Microsoft.mssql"
|
||||
],
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "adminToolExtWin.launchSsmsServerPropertiesDialog",
|
||||
"title": "%adminToolExtWin.launchSsmsServerPropertiesDialog%",
|
||||
"category": "AdminToolExtWin"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "adminToolExtWin.launchSsmsServerPropertiesDialog",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"objectExplorer/item/context": [
|
||||
{
|
||||
"command": "adminToolExtWin.launchSsmsServerPropertiesDialog",
|
||||
"when": "isWindows && connectionProvider == MSSQL && nodeType && nodeType == Server",
|
||||
"group": "AdminToolExtWin"
|
||||
}
|
||||
]
|
||||
},
|
||||
"outputChannels": [
|
||||
"admin-tool-ext-win"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"service-downloader": "github:anthonydresser/service-downloader#0.1.5",
|
||||
"vscode-extension-telemetry": "^0.0.15",
|
||||
"vscode-nls": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vscode": "1.0.1"
|
||||
}
|
||||
}
|
||||
"name": "admin-tool-ext-win",
|
||||
"displayName": "%adminToolExtWin.displayName%",
|
||||
"description": "%adminToolExtWin.description%",
|
||||
"version": "0.0.1",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt",
|
||||
"icon": "images/sqlserver.png",
|
||||
"aiKey": "AIF-5574968e-856d-40d2-af67-c89a14e76412",
|
||||
"engines": {
|
||||
"vscode": "^1.30.1",
|
||||
"sqlops": "*"
|
||||
},
|
||||
"activationEvents": [
|
||||
"*"
|
||||
],
|
||||
"main": "./out/main",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Microsoft/azuredatastudio.git"
|
||||
},
|
||||
"extensionDependencies": [
|
||||
"Microsoft.mssql"
|
||||
],
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "adminToolExtWin.launchSsmsMinPropertiesDialog",
|
||||
"title": "%adminToolExtWin.propertiesMenuItem%",
|
||||
"category": "AdminToolExtWin"
|
||||
},
|
||||
{
|
||||
"command": "adminToolExtWin.launchSsmsMinGswDialog",
|
||||
"title": "%adminToolExtWin.launchGswMenuItem%",
|
||||
"category": "AdminToolExtWin"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "adminToolExtWin.launchSsmsMinPropertiesDialog",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "adminToolExtWin.launchSsmsMinGswDialog",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"objectExplorer/item/context": [
|
||||
{
|
||||
"command": "adminToolExtWin.launchSsmsMinGswDialog",
|
||||
"when": "isWindows && connectionProvider == MSSQL && nodeType && nodeType == Database",
|
||||
"group": "z-AdminToolExt@1"
|
||||
},
|
||||
{
|
||||
"command": "adminToolExtWin.launchSsmsMinPropertiesDialog",
|
||||
"when": "isWindows && connectionProvider == MSSQL && nodeType && nodeType =~ /^(Server|Database|Table|Column|Index|Statistic|View|ServerLevelLogin|ServerLevelServerRole|ServerLevelCredential|ServerLevelServerAudit|ServerLevelServerAuditSpecification|StoredProcedure|ScalarValuedFunction|TableValuedFunction|AggregateFunction|Synonym|Assembly|UserDefinedDataType|UserDefinedType|UserDefinedTableType|Sequence|User|DatabaseRole|ApplicationRole|Schema|SecurityPolicy|ServerLevelLinkedServer)$/",
|
||||
"group": "z-AdminToolExt@2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"outputChannels": [
|
||||
"admin-tool-ext-win"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"service-downloader": "github:anthonydresser/service-downloader#0.1.5",
|
||||
"vscode-extension-telemetry": "^0.0.15",
|
||||
"vscode-nls": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vscode": "1.0.1"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"adminToolExtWin.launchSsmsServerPropertiesDialog": "Properties"
|
||||
"adminToolExtWin.displayName": "Database Administration Tool Extensions for Windows",
|
||||
"adminToolExtWin.description": "Adds additional Windows-specific functionality to Azure Data Studio",
|
||||
"adminToolExtWin.propertiesMenuItem": "Properties",
|
||||
"adminToolExtWin.launchGswMenuItem": "Generate Scripts..."
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { IConfig, ServerProvider } from 'service-downloader';
|
||||
import { Telemetry } from './telemetry';
|
||||
import * as utils from './utils';
|
||||
import { doubleEscapeSingleQuotes, backEscapeDoubleQuotes, getConfiguration } from './utils';
|
||||
import { ChildProcess, exec } from 'child_process';
|
||||
|
||||
const baseConfig = require('./config.json');
|
||||
@@ -17,6 +17,43 @@ const localize = nls.loadMessageBundle();
|
||||
let exePath: string;
|
||||
let runningProcesses: Map<number, ChildProcess> = new Map<number, ChildProcess>();
|
||||
|
||||
|
||||
interface SmoMapping {
|
||||
action: string;
|
||||
urnName: string;
|
||||
}
|
||||
|
||||
const nodeTypeToUrnNameMapping: { [oeNodeType: string]: SmoMapping } = {
|
||||
'Database': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.Database', urnName: 'Database' },
|
||||
'Server': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.Server', urnName: 'Server' },
|
||||
'ServerLevelServerAudit': { action: 'sqla:AuditProperties', urnName: 'Audit' },
|
||||
'ServerLevelCredential': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.Credential', urnName: 'Credential' },
|
||||
'ServerLevelServerRole': { action: 'sqla:ManageServerRole', urnName: 'Role' },
|
||||
'ServerLevelServerAuditSpecification': { action: 'sqla:ServerAuditSpecificationProperties', urnName: 'ServerAuditSpecification' },
|
||||
'ServerLevelLinkedServer': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.LinkedServer', urnName: 'LinkedServer' },
|
||||
'Table': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.Table', urnName: 'Table' },
|
||||
'View': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.View', urnName: 'View' },
|
||||
'Column': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.Column', urnName: 'Column' },
|
||||
'Index': { action: 'sqla:IndexProperties', urnName: 'Index' },
|
||||
'Statistic': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.Statistic', urnName: 'Statistic' },
|
||||
'StoredProcedure': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.StoredProcedure', urnName: 'StoredProcedure' },
|
||||
'ScalarValuedFunction': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.UserDefinedFunction', urnName: 'UserDefinedFunction' },
|
||||
'TableValuedFunction': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.UserDefinedFunction', urnName: 'UserDefinedFunction' },
|
||||
'AggregateFunction': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.UserDefinedFunction', urnName: 'UserDefinedFunction' },
|
||||
'Synonym': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.Synonym', urnName: 'Synonym' },
|
||||
'Assembly': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.SqlAssembly', urnName: 'SqlAssembly' },
|
||||
'UserDefinedDataType': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.UserDefinedDataType', urnName: 'UserDefinedDataType' },
|
||||
'UserDefinedType': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.UserDefinedType', urnName: 'UserDefinedType' },
|
||||
'UserDefinedTableType': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.UserDefinedTableType', urnName: 'UserDefinedTableType' },
|
||||
'Sequence': { action: 'sqla:SequenceProperties', urnName: 'Sequence' },
|
||||
'User': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.User', urnName: 'User' },
|
||||
'DatabaseRole': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.DatabaseRole', urnName: 'Role' },
|
||||
'ApplicationRole': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.ApplicationRole', urnName: 'ApplicationRole' },
|
||||
'Schema': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.Schema', urnName: 'Schema' },
|
||||
'SecurityPolicy': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.SecurityPolicy', urnName: 'SecurityPolicy' },
|
||||
'ServerLevelLogin': { action: 'sqla:Properties@Microsoft.SqlServer.Management.Smo.Login', urnName: 'Login' },
|
||||
};
|
||||
|
||||
// Params to pass to SsmsMin.exe, only an action and server are required - the rest are optional based on the
|
||||
// action used. Exported for use in testing.
|
||||
export interface LaunchSsmsDialogParams {
|
||||
@@ -36,8 +73,8 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
|
||||
|
||||
let config: IConfig = JSON.parse(JSON.stringify(baseConfig));
|
||||
config.installDirectory = path.join(context.extensionPath, config.installDirectory);
|
||||
config.proxy = utils.getConfiguration('http').get('proxy');
|
||||
config.strictSSL = utils.getConfiguration('http').get('proxyStrictSSL') || true;
|
||||
config.proxy = getConfiguration('http').get('proxy');
|
||||
config.strictSSL = getConfiguration('http').get('proxyStrictSSL') || true;
|
||||
|
||||
const serverdownloader = new ServerProvider(config);
|
||||
const installationStart = Date.now();
|
||||
@@ -52,9 +89,9 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
|
||||
} else {
|
||||
throw new Error('Could not find SsmsMin.exe after downloading');
|
||||
}
|
||||
// Add the command now that we have the exePath to run the tool with
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('adminToolExtWin.launchSsmsServerPropertiesDialog', handleLaunchSsmsServerPropertiesDialogCommand));
|
||||
|
||||
// Register the commands now that we have the exePath to run the tool with
|
||||
registerCommands(context);
|
||||
|
||||
Telemetry.sendTelemetryEvent('startup/ExtensionStarted', {
|
||||
installationTime: String(installationComplete - installationStart),
|
||||
@@ -75,16 +112,52 @@ export async function deactivate(): Promise<void> {
|
||||
runningProcesses.forEach(p => exec('taskkill /pid ' + p.pid + ' /T /F'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers extension commands with command subsystem
|
||||
* @param context The context used to register the commands
|
||||
*/
|
||||
function registerCommands(context: vscode.ExtensionContext): void {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('adminToolExtWin.launchSsmsMinPropertiesDialog', handleLaunchSsmsMinPropertiesDialogCommand));
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('adminToolExtWin.launchSsmsMinGswDialog', handleLaunchSsmsMinGswDialogCommand));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for command to launch SSMS Server Properties dialog
|
||||
* @param connectionId The connection context from the command
|
||||
*/
|
||||
function handleLaunchSsmsServerPropertiesDialogCommand(connectionContext?: azdata.ObjectExplorerContext) {
|
||||
if (connectionContext && connectionContext.connectionProfile) {
|
||||
launchSsmsDialog(
|
||||
/*action*/'sqla:Properties@Microsoft.SqlServer.Management.Smo.Server',
|
||||
/*connectionProfile*/connectionContext.connectionProfile);
|
||||
async function handleLaunchSsmsMinPropertiesDialogCommand(connectionContext?: azdata.ObjectExplorerContext): Promise<void> {
|
||||
if (!connectionContext) {
|
||||
console.error('No ConnectionContext provided for handleLaunchSsmsMinPropertiesDialogCommand');
|
||||
return;
|
||||
}
|
||||
|
||||
let nodeType: string;
|
||||
if (connectionContext.isConnectionNode) {
|
||||
nodeType = 'Server';
|
||||
}
|
||||
else if (connectionContext.nodeInfo) {
|
||||
nodeType = connectionContext.nodeInfo.nodeType;
|
||||
}
|
||||
else {
|
||||
console.error(`Could not determine NodeType for handleLaunchSsmsMinPropertiesDialogCommand with context ${connectionContext}`);
|
||||
return;
|
||||
}
|
||||
|
||||
launchSsmsDialog(
|
||||
nodeTypeToUrnNameMapping[nodeType].action,
|
||||
connectionContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for command to launch SSMS "Generate Script Wizard" dialog
|
||||
* @param connectionId The connection context from the command
|
||||
*/
|
||||
async function handleLaunchSsmsMinGswDialogCommand(connectionContext?: azdata.ObjectExplorerContext): Promise<void> {
|
||||
launchSsmsDialog(
|
||||
'GenerateScripts',
|
||||
connectionContext);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,29 +166,49 @@ function handleLaunchSsmsServerPropertiesDialogCommand(connectionContext?: azdat
|
||||
* @param params The params used to construct the command
|
||||
* @param urn The URN to pass to SsmsMin
|
||||
*/
|
||||
function launchSsmsDialog(action: string, connectionProfile: azdata.IConnectionProfile, urn?: string) {
|
||||
async function launchSsmsDialog(action: string, connectionContext: azdata.ObjectExplorerContext): Promise<void> {
|
||||
if (!exePath) {
|
||||
vscode.window.showErrorMessage(localize('adminToolExtWin.noExeError', 'Unable to find SsmsMin.exe.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connectionContext.connectionProfile) {
|
||||
console.error(`No ConnectionProfile provided from connectionContext : ${connectionContext}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let oeNode: azdata.objectexplorer.ObjectExplorerNode;
|
||||
// Server node is a Connection node and so doesn't have the NodeInfo
|
||||
if (connectionContext.isConnectionNode) {
|
||||
oeNode = undefined;
|
||||
}
|
||||
else if (connectionContext.nodeInfo && connectionContext.nodeInfo.nodeType && connectionContext.connectionProfile) {
|
||||
oeNode = await azdata.objectexplorer.getNode(connectionContext.connectionProfile.id, connectionContext.nodeInfo.nodePath);
|
||||
}
|
||||
else {
|
||||
console.error(`Could not determine Object Explorer node from connectionContext : ${connectionContext}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let urn: string = await buildUrn(connectionContext.connectionProfile.serverName, oeNode);
|
||||
|
||||
Telemetry.sendTelemetryEvent('LaunchSsmsDialog', { 'action': action });
|
||||
|
||||
let params: LaunchSsmsDialogParams = {
|
||||
action: action,
|
||||
server: connectionProfile.serverName,
|
||||
database: connectionProfile.databaseName,
|
||||
password: connectionProfile.password,
|
||||
user: connectionProfile.userName,
|
||||
useAad: connectionProfile.authenticationType === 'AzureMFA',
|
||||
server: connectionContext.connectionProfile.serverName,
|
||||
database: connectionContext.connectionProfile.databaseName,
|
||||
password: connectionContext.connectionProfile.password,
|
||||
user: connectionContext.connectionProfile.userName,
|
||||
useAad: connectionContext.connectionProfile.authenticationType === 'AzureMFA',
|
||||
urn: urn
|
||||
};
|
||||
let args = buildSsmsMinCommandArgs(params);
|
||||
|
||||
// This will be an async call since we pass in the callback
|
||||
let proc: ChildProcess = exec(
|
||||
/*command*/`"${exePath}" ${args}`,
|
||||
/*options*/undefined,
|
||||
/*command*/ `"${exePath}" ${args}`,
|
||||
/*options*/ undefined,
|
||||
(execException, stdout, stderr) => {
|
||||
// Process has exited so remove from map of running processes
|
||||
runningProcesses.delete(proc.pid);
|
||||
@@ -145,10 +238,34 @@ function launchSsmsDialog(action: string, connectionProfile: azdata.IConnectionP
|
||||
* @param params The params used to build up the command parameter string
|
||||
*/
|
||||
export function buildSsmsMinCommandArgs(params: LaunchSsmsDialogParams): string {
|
||||
return `${params.action ? '-a "' + params.action.replace(/"/g, '\\"') + '"' : ''}\
|
||||
${params.server ? ' -S "' + params.server.replace(/"/g, '\\"') + '"' : ''}\
|
||||
${params.database ? ' -D "' + params.database.replace(/"/g, '\\"') + '"' : ''}\
|
||||
${params.useAad !== true && params.user ? ' -U "' + params.user.replace(/"/g, '\\"') + '"' : ''}\
|
||||
return `${params.action ? '-a "' + backEscapeDoubleQuotes(params.action) + '"' : ''}\
|
||||
${params.server ? ' -S "' + backEscapeDoubleQuotes(params.server) + '"' : ''}\
|
||||
${params.database ? ' -D "' + backEscapeDoubleQuotes(params.database) + '"' : ''}\
|
||||
${params.useAad !== true && params.user ? ' -U "' + backEscapeDoubleQuotes(params.user) + '"' : ''}\
|
||||
${params.useAad === true ? ' -G' : ''}\
|
||||
${params.urn ? ' -u "' + params.urn.replace(/"/g, '\\"') + '"' : ''}`;
|
||||
${params.urn ? ' -u "' + backEscapeDoubleQuotes(params.urn) + '"' : ''}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the URN string for a given ObjectExplorerNode in the form understood by SsmsMin
|
||||
* @param serverName The name of the Server to use for the Server segment
|
||||
* @param node The node to get the URN of
|
||||
*/
|
||||
export async function buildUrn(serverName: string, node: azdata.objectexplorer.ObjectExplorerNode): Promise<string> {
|
||||
let urnNodes: string[] = [];
|
||||
while (node) {
|
||||
// Server is special since it's a connection node - always add it as the root
|
||||
if (node.nodeType === 'Server') {
|
||||
break;
|
||||
}
|
||||
else if (node.metadata && node.nodeType !== 'Folder') {
|
||||
// SFC URN expects Name and Schema to be separate properties
|
||||
let urnSegment = node.metadata.schema && node.metadata.schema !== '' ?
|
||||
`${nodeTypeToUrnNameMapping[node.nodeType].urnName}[@Name='${doubleEscapeSingleQuotes(node.metadata.name)}' and @Schema='${doubleEscapeSingleQuotes(node.metadata.schema)}']` :
|
||||
`${nodeTypeToUrnNameMapping[node.nodeType].urnName}[@Name='${doubleEscapeSingleQuotes(node.metadata.name)}']`;
|
||||
urnNodes = [urnSegment].concat(urnNodes);
|
||||
}
|
||||
node = await node.getParent();
|
||||
}
|
||||
return [`Server[@Name='${doubleEscapeSingleQuotes(serverName)}']`].concat(urnNodes).join('/');
|
||||
}
|
||||
68
extensions/admin-tool-ext-win/src/test/stubs.ts
Normal file
68
extensions/admin-tool-ext-win/src/test/stubs.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
/**
|
||||
* Helper stub class for mocking ExtHostObjectExplorerNode
|
||||
*/
|
||||
export class ExtHostObjectExplorerNodeStub implements azdata.objectexplorer.ObjectExplorerNode {
|
||||
// Stub properties
|
||||
private parent: azdata.objectexplorer.ObjectExplorerNode;
|
||||
|
||||
// Base properties
|
||||
public connectionId: string;
|
||||
public nodePath: string;
|
||||
public nodeType: string;
|
||||
public nodeSubType: string;
|
||||
public nodeStatus: string;
|
||||
public label: string;
|
||||
public isLeaf: boolean;
|
||||
public metadata: azdata.ObjectMetadata;
|
||||
public errorMessage: string;
|
||||
|
||||
constructor(nodeName: string, nodeSchema: string, nodeType, parent: azdata.objectexplorer.ObjectExplorerNode) {
|
||||
this.parent = parent;
|
||||
this.nodeType = nodeType;
|
||||
this.metadata = { metadataType: undefined, metadataTypeName: undefined, name: nodeName, schema: nodeSchema, urn: undefined };
|
||||
}
|
||||
|
||||
isExpanded(): Thenable<boolean> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
setExpandedState(expandedState: vscode.TreeItemCollapsibleState): Thenable<void> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
setSelected(selected: boolean, clearOtherSelections: boolean = undefined): Thenable<void> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
getChildren(): Thenable<azdata.objectexplorer.ObjectExplorerNode[]> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
getParent(): Thenable<azdata.objectexplorer.ObjectExplorerNode> {
|
||||
return Promise.resolve(this.parent);
|
||||
}
|
||||
|
||||
refresh(): Thenable<void> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param nodeName Helperfunction to create a node that is a child of this one
|
||||
* @param nodeSchema The schema to give the child node
|
||||
* @param nodeType The type of node this should be
|
||||
*/
|
||||
createChild(nodeName: string, nodeSchema: string, nodeType: string): ExtHostObjectExplorerNodeStub {
|
||||
return new ExtHostObjectExplorerNodeStub(nodeName, nodeSchema, nodeType, this);
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,13 @@
|
||||
import * as should from 'should';
|
||||
import 'mocha';
|
||||
|
||||
import * as extensionMain from '../main';
|
||||
import { buildSsmsMinCommandArgs, buildUrn, LaunchSsmsDialogParams } from '../main';
|
||||
import { doubleEscapeSingleQuotes, backEscapeDoubleQuotes } from '../utils';
|
||||
import { ExtHostObjectExplorerNodeStub } from './stubs';
|
||||
|
||||
describe('buildSsmsMinCommandArgs Method Tests', () => {
|
||||
it('Should be built correctly with all params and UseAAD as false', function (): void {
|
||||
let params: extensionMain.LaunchSsmsDialogParams = {
|
||||
const params: LaunchSsmsDialogParams = {
|
||||
action: 'myAction',
|
||||
server: 'myServer',
|
||||
database: 'myDatabase',
|
||||
@@ -21,12 +23,12 @@ describe('buildSsmsMinCommandArgs Method Tests', () => {
|
||||
useAad: false,
|
||||
urn: 'Server\\Database\\Table'
|
||||
};
|
||||
let args = extensionMain.buildSsmsMinCommandArgs(params);
|
||||
const args = buildSsmsMinCommandArgs(params);
|
||||
should(args).equal('-a "myAction" -S "myServer" -D "myDatabase" -U "user" -u "Server\\Database\\Table"');
|
||||
});
|
||||
|
||||
it('Should be built correctly with all params and UseAAD as true', function (): void {
|
||||
let params: extensionMain.LaunchSsmsDialogParams = {
|
||||
const params: LaunchSsmsDialogParams = {
|
||||
action: 'myAction',
|
||||
server: 'myServer',
|
||||
database: 'myDatabase',
|
||||
@@ -35,13 +37,13 @@ describe('buildSsmsMinCommandArgs Method Tests', () => {
|
||||
useAad: true,
|
||||
urn: 'Server\\Database\\Table'
|
||||
};
|
||||
let args = extensionMain.buildSsmsMinCommandArgs(params);
|
||||
const args = buildSsmsMinCommandArgs(params);
|
||||
// User is omitted since UseAAD is true
|
||||
should(args).equal('-a "myAction" -S "myServer" -D "myDatabase" -G -u "Server\\Database\\Table"');
|
||||
});
|
||||
|
||||
it('Should be built correctly and names escaped correctly', function (): void {
|
||||
let params: extensionMain.LaunchSsmsDialogParams = {
|
||||
const params: LaunchSsmsDialogParams = {
|
||||
action: 'myAction\'"/\\[]tricky',
|
||||
server: 'myServer\'"/\\[]tricky',
|
||||
database: 'myDatabase\'"/\\[]tricky',
|
||||
@@ -50,18 +52,87 @@ describe('buildSsmsMinCommandArgs Method Tests', () => {
|
||||
useAad: true,
|
||||
urn: 'Server\\Database[\'myDatabase\'\'"/\\[]tricky\']\\Table["myTable\'""/\\[]tricky"]'
|
||||
};
|
||||
let args = extensionMain.buildSsmsMinCommandArgs(params);
|
||||
const args = buildSsmsMinCommandArgs(params);
|
||||
// User is omitted since UseAAD is true
|
||||
should(args).equal('-a "myAction\'\\"/\\[]tricky" -S "myServer\'\\"/\\[]tricky" -D "myDatabase\'\\"/\\[]tricky" -G -u "Server\\Database[\'myDatabase\'\'\\"/\\[]tricky\']\\Table[\\"myTable\'\\"\\"/\\[]tricky\\"]"');
|
||||
});
|
||||
|
||||
it('Should be built correctly with only action and server', function (): void {
|
||||
|
||||
let params: extensionMain.LaunchSsmsDialogParams = {
|
||||
const params: LaunchSsmsDialogParams = {
|
||||
action: 'myAction',
|
||||
server: 'myServer'
|
||||
};
|
||||
let args = extensionMain.buildSsmsMinCommandArgs(params);
|
||||
const args = buildSsmsMinCommandArgs(params);
|
||||
should(args).equal('-a "myAction" -S "myServer"');
|
||||
});
|
||||
});
|
||||
|
||||
const serverName = 'My\'Server';
|
||||
const escapedServerName = doubleEscapeSingleQuotes(serverName);
|
||||
const dbName = 'My\'Db';
|
||||
const escapedDbName = doubleEscapeSingleQuotes(dbName);
|
||||
const dbSchema = 'db\'sch';
|
||||
const escapedDbSchema = doubleEscapeSingleQuotes(dbSchema);
|
||||
const tableName = 'My\'Table';
|
||||
const escapedTableName = doubleEscapeSingleQuotes(tableName);
|
||||
const tableSchema = 'tbl\'sch';
|
||||
const escapedTableSchema = doubleEscapeSingleQuotes(tableSchema);
|
||||
|
||||
describe('buildUrn Method Tests', () => {
|
||||
it('Urn should be correct with just server', async function (): Promise<void> {
|
||||
should(await buildUrn(serverName, undefined)).equal(`Server[@Name=\'${escapedServerName}\']`);
|
||||
});
|
||||
|
||||
it('Urn should be correct with Server and only Databases folder', async function (): Promise<void> {
|
||||
const leafNode: ExtHostObjectExplorerNodeStub =
|
||||
new ExtHostObjectExplorerNodeStub('Databases', undefined, 'Folder', undefined);
|
||||
should(await buildUrn(serverName, leafNode)).equal(`Server[@Name='${escapedServerName}']`);
|
||||
});
|
||||
|
||||
it('Urn should be correct with Server and Database node', async function (): Promise<void> {
|
||||
const leafNode: ExtHostObjectExplorerNodeStub =
|
||||
new ExtHostObjectExplorerNodeStub('Databases', undefined, 'Folder', undefined)
|
||||
.createChild(dbName, dbSchema, 'Database');
|
||||
should(await buildUrn(serverName, leafNode)).equal(
|
||||
`Server[@Name='${escapedServerName}']/Database[@Name='${escapedDbName}' and @Schema='${escapedDbSchema}']`);
|
||||
});
|
||||
|
||||
it('Urn should be correct with Multiple levels of Nodes', async function (): Promise<void> {
|
||||
const rootNode: ExtHostObjectExplorerNodeStub =
|
||||
new ExtHostObjectExplorerNodeStub('Databases', undefined, 'Folder', undefined)
|
||||
.createChild(dbName, dbSchema, 'Database')
|
||||
.createChild('Tables', undefined, 'Folder')
|
||||
.createChild(tableName, tableSchema, 'Table');
|
||||
should(await buildUrn(serverName, rootNode)).equal(
|
||||
`Server[@Name='${escapedServerName}']/Database[@Name='${escapedDbName}' and @Schema='${escapedDbSchema}']/Table[@Name='${escapedTableName}' and @Schema='${escapedTableSchema}']`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('doubleEscapeSingleQuotes Method Tests', () => {
|
||||
it('Should return original string if no single quotes', function (): void {
|
||||
const testString: string = 'MyTestString';
|
||||
const ret = doubleEscapeSingleQuotes(testString);
|
||||
should(ret).equal(testString);
|
||||
});
|
||||
|
||||
it('Should return escaped original string if it contains single quotes', function (): void {
|
||||
const testString: string = 'MyTestString\'\'WithQuotes';
|
||||
const ret = doubleEscapeSingleQuotes(testString);
|
||||
should(ret).equal('MyTestString\'\'\'\'WithQuotes');
|
||||
});
|
||||
});
|
||||
|
||||
describe('backEscapeDoubleQuotes Method Tests', () => {
|
||||
it('Should return original string if no double quotes', function (): void {
|
||||
const testString: string = 'MyTestString';
|
||||
const ret = backEscapeDoubleQuotes(testString);
|
||||
should(ret).equal(testString);
|
||||
});
|
||||
|
||||
it('Should return escaped original string if it contains double quotes', function (): void {
|
||||
const testString: string = 'MyTestString\"\"WithQuotes';
|
||||
const ret = backEscapeDoubleQuotes(testString);
|
||||
should(ret).equal('MyTestString\\"\\"WithQuotes');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,3 +40,21 @@ export function getConfiguration(extensionName?: string, resource?: vscode.Uri |
|
||||
}
|
||||
return vscode.workspace.getConfiguration(extensionName, resource as vscode.Uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes all single-quotes (') by prefixing them with another single quote ('')
|
||||
* ' => ''
|
||||
* @param value The string to escape
|
||||
*/
|
||||
export function doubleEscapeSingleQuotes(value: string): string {
|
||||
return value.replace(/'/g, '\'\'');
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape all double-quotes (") by prefixing them with a \
|
||||
* " => \"
|
||||
* @param value The string to escape
|
||||
*/
|
||||
export function backEscapeDoubleQuotes(value: string): string {
|
||||
return value.replace(/"/g, '\\"');
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import { IMainContext } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { ExtHostObjectExplorerShape, SqlMainContext, MainThreadObjectExplorerShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { entries } from 'sql/base/common/objects';
|
||||
|
||||
export class ExtHostObjectExplorer implements ExtHostObjectExplorerShape {
|
||||
|
||||
@@ -41,7 +40,7 @@ export class ExtHostObjectExplorer implements ExtHostObjectExplorerShape {
|
||||
}
|
||||
}
|
||||
|
||||
class ExtHostObjectExplorerNode implements azdata.objectexplorer.ObjectExplorerNode {
|
||||
export class ExtHostObjectExplorerNode implements azdata.objectexplorer.ObjectExplorerNode {
|
||||
public connectionId: string;
|
||||
public nodePath: string;
|
||||
public nodeType: string;
|
||||
@@ -74,11 +73,21 @@ class ExtHostObjectExplorerNode implements azdata.objectexplorer.ObjectExplorerN
|
||||
}
|
||||
|
||||
getParent(): Thenable<azdata.objectexplorer.ObjectExplorerNode> {
|
||||
let parentPathEndIndex = this.nodePath.lastIndexOf('/');
|
||||
if (parentPathEndIndex === -1) {
|
||||
return Promise.resolve(undefined);
|
||||
// Object nodes have a name like <schema>.<name> in the nodePath - we can't use label because
|
||||
// that may have additional display information appended to it. Items without metadata are nodes
|
||||
// such as folders that don't correspond to actual objects and so just use the label
|
||||
let nodePathName = this.metadata ?
|
||||
`${this.metadata.schema ? this.metadata.schema + '.' : ''}${this.metadata.name}` :
|
||||
this.label;
|
||||
|
||||
// -1 to remove the / as well
|
||||
let parentPathEndIndex: number = this.nodePath.lastIndexOf(nodePathName) - 1;
|
||||
if (parentPathEndIndex < 0) {
|
||||
// At root node
|
||||
Promise.resolve(undefined);
|
||||
}
|
||||
return this._proxy.$getNode(this.connectionId, this.nodePath.slice(0, parentPathEndIndex)).then(nodeInfo => nodeInfo ? new ExtHostObjectExplorerNode(nodeInfo, this.connectionId, this._proxy) : undefined);
|
||||
return this._proxy.$getNode(this.connectionId, this.nodePath.slice(0, parentPathEndIndex)).then(
|
||||
nodeInfo => nodeInfo ? new ExtHostObjectExplorerNode(nodeInfo, this.connectionId, this._proxy) : undefined);
|
||||
}
|
||||
|
||||
refresh(): Thenable<void> {
|
||||
|
||||
111
src/sqltest/workbench/api/extHostObjectExplorer.test.ts
Normal file
111
src/sqltest/workbench/api/extHostObjectExplorer.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as assert from 'assert';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
|
||||
import { MainThreadObjectExplorerShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
|
||||
import { ExtHostObjectExplorerNode } from 'sql/workbench/api/node/extHostObjectExplorer';
|
||||
|
||||
const nodes: { [nodeName: string]: azdata.NodeInfo } =
|
||||
{
|
||||
'Server1': {
|
||||
nodePath: 'MyServer',
|
||||
nodeStatus: '',
|
||||
nodeSubType: '',
|
||||
nodeType: 'Server',
|
||||
isLeaf: false,
|
||||
label: 'MyServer',
|
||||
metadata: undefined,
|
||||
errorMessage: ''
|
||||
},
|
||||
'DatabasesFolder': {
|
||||
nodePath: 'MyServer/Databases',
|
||||
nodeStatus: '',
|
||||
nodeSubType: '',
|
||||
nodeType: 'Folder',
|
||||
isLeaf: false,
|
||||
label: 'Databases',
|
||||
metadata: undefined,
|
||||
errorMessage: ''
|
||||
},
|
||||
'Database1': {
|
||||
nodePath: 'MyServer/Databases/MyDatabase',
|
||||
nodeStatus: '',
|
||||
nodeSubType: '',
|
||||
nodeType: 'Database',
|
||||
isLeaf: false,
|
||||
label: 'MyDatabase',
|
||||
metadata: undefined,
|
||||
errorMessage: ''
|
||||
},
|
||||
'Database2': {
|
||||
nodePath: 'MyServer/Databases/My/TrickyDatabase',
|
||||
nodeStatus: '',
|
||||
nodeSubType: '',
|
||||
nodeType: 'Database',
|
||||
isLeaf: false,
|
||||
label: 'My/TrickyDatabase',
|
||||
metadata: undefined,
|
||||
errorMessage: ''
|
||||
},
|
||||
'TablesFolder': {
|
||||
nodePath: 'MyServer/Databases/My/TrickyDatabase/Tables',
|
||||
nodeStatus: '',
|
||||
nodeSubType: '',
|
||||
nodeType: 'Folder',
|
||||
isLeaf: false,
|
||||
label: 'Tables',
|
||||
metadata: undefined,
|
||||
errorMessage: ''
|
||||
}
|
||||
};
|
||||
|
||||
suite('ExtHostObjectExplorer Tests', () => {
|
||||
let mockProxy: TypeMoq.Mock<MainThreadObjectExplorerShape>;
|
||||
suiteSetup(() => {
|
||||
mockProxy = TypeMoq.Mock.ofInstance(<MainThreadObjectExplorerShape>{
|
||||
$getNode: (connectionId: string, nodePath?: string): Thenable<azdata.NodeInfo> => undefined
|
||||
});
|
||||
|
||||
mockProxy.setup(p =>
|
||||
p.$getNode(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.returns((connectionId, nodePath) => {
|
||||
return Promise.resolve<azdata.NodeInfo>(nodes[Object.keys(nodes).find(key =>
|
||||
nodes[key].nodePath === nodePath)]);
|
||||
});
|
||||
});
|
||||
|
||||
suite('ExtHostObjectExplorerNode', () => {
|
||||
let extHostObjectExplorerNode: ExtHostObjectExplorerNode;
|
||||
suite('getParent', () => {
|
||||
test('Should return undefined if no parent', async () => {
|
||||
extHostObjectExplorerNode = new ExtHostObjectExplorerNode(nodes['Server1'], 'connectionId', mockProxy.object);
|
||||
assert.equal(await extHostObjectExplorerNode.getParent(), undefined);
|
||||
});
|
||||
|
||||
test('should return root with direct descendent of root', async () => {
|
||||
extHostObjectExplorerNode = new ExtHostObjectExplorerNode(nodes['DatabasesFolder'], 'connectionId', mockProxy.object);
|
||||
assert.equal((await extHostObjectExplorerNode.getParent()).nodePath, nodes['Server1'].nodePath);
|
||||
});
|
||||
|
||||
test('should return correct parent with further descendent of root', async () => {
|
||||
extHostObjectExplorerNode = new ExtHostObjectExplorerNode(nodes['Database1'], 'connectionId', mockProxy.object);
|
||||
assert.equal((await extHostObjectExplorerNode.getParent()).nodePath, nodes['DatabasesFolder'].nodePath);
|
||||
});
|
||||
|
||||
test('should return correct parent with node having / in its name', async () => {
|
||||
extHostObjectExplorerNode = new ExtHostObjectExplorerNode(nodes['Database2'], 'connectionId', mockProxy.object);
|
||||
assert.equal((await extHostObjectExplorerNode.getParent()).nodePath, nodes['DatabasesFolder'].nodePath);
|
||||
});
|
||||
|
||||
test('should return correct parent with parent node having / in its name', async () => {
|
||||
extHostObjectExplorerNode = new ExtHostObjectExplorerNode(nodes['TablesFolder'], 'connectionId', mockProxy.object);
|
||||
assert.equal((await extHostObjectExplorerNode.getParent()).nodePath, nodes['Database2'].nodePath);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user