Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vscode-documentdb-api-experimental-beta",
"version": "0.2.0",
"version": "0.3.0",
"description": "Extension API for VS Code DocumentDB extension (preview)",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
31 changes: 23 additions & 8 deletions api/src/utils/getApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const DOCUMENTDB_EXTENSION_ID = 'ms-azuretools.vscode-documentdb';
*/
interface DocumentDBApiConfig {
'x-documentdbApi'?: {
registeredClients?: string[];
verifiedClients?: string[];
};
}

Expand Down Expand Up @@ -44,11 +44,11 @@ function isValidPackageJson(packageJson: unknown): packageJson is DocumentDBApiC
* ```
*/
export async function getDocumentDBExtensionApi(
_context: vscode.ExtensionContext,
context: vscode.ExtensionContext,
apiVersionRange: string,
): Promise<DocumentDBExtensionApi> {
// Get the calling extension's ID from the context
const callingExtensionId = _context.extension.id;
const callingExtensionId = context.extension.id;

// Get the DocumentDB extension to access its package.json configuration
const extension = vscode.extensions.getExtension<DocumentDBExtensionApi>(DOCUMENTDB_EXTENSION_ID);
Expand All @@ -58,15 +58,15 @@ export async function getDocumentDBExtensionApi(

// Check if the calling extension is whitelisted
const packageJson = extension.packageJSON as unknown;
const registeredClients = isValidPackageJson(packageJson)
? packageJson['x-documentdbApi']?.registeredClients
const verifiedClients = isValidPackageJson(packageJson)
? packageJson['x-documentdbApi']?.verifiedClients
: undefined;

if (!registeredClients || !Array.isArray(registeredClients)) {
throw new Error(`DocumentDB for VS Code API configuration is invalid. No registered clients found.`);
if (!verifiedClients || !Array.isArray(verifiedClients)) {
throw new Error(`DocumentDB for VS Code API configuration is invalid. No verified client list found.`);
}

if (!registeredClients.includes(callingExtensionId)) {
if (!verifiedClients.includes(callingExtensionId)) {
throw new Error(
`Extension '${callingExtensionId}' is not authorized to use the DocumentDB for VS Code API. ` +
`This is an experimental API with whitelisted access. ` +
Expand All @@ -89,5 +89,20 @@ export async function getDocumentDBExtensionApi(
console.warn(`API version mismatch. Expected ${apiVersionRange}, got ${api.apiVersion}`);
}

try {
// going via an "internal" command here to avoid making the registraction function public
const success = await vscode.commands.executeCommand(
'vscode-documentdb.command.internal.api.registerClientExtension',
context.extension.id,
);

if (success !== true) {
console.warn(`Client registration may have failed for "${callingExtensionId}"`);
}
} catch (error) {
// Log error but don't fail API retrieval
console.warn(`Failed to register client "${callingExtensionId}": ${error}`);
}

return api;
}
1 change: 1 addition & 0 deletions l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"An error has occurred. Check output window for more details.": "An error has occurred. Check output window for more details.",
"An item with id \"{0}\" already exists for workspace \"{1}\".": "An item with id \"{0}\" already exists for workspace \"{1}\".",
"API version \"{0}\" for extension id \"{1}\" is no longer supported. Minimum version is \"{2}\".": "API version \"{0}\" for extension id \"{1}\" is no longer supported. Minimum version is \"{2}\".",
"API: Registered new client extension: \"{clientExtensionId}\"": "API: Registered new client extension: \"{clientExtensionId}\"",
"API: Registered new migration provider: \"{providerId}\" - \"{providerLabel}\"": "API: Registered new migration provider: \"{providerId}\" - \"{providerLabel}\"",
"Are you sure?": "Are you sure?",
"Attempting to authenticate with \"{cluster}\"…": "Attempting to authenticate with \"{cluster}\"…",
Expand Down
23 changes: 15 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@
{
"//": "Data Migration",
"category": "DocumentDB",
"command": "vscode-documentdb.command.chooseDataMigrationExtension",
"command": "vscode-documentdb.command.accessDataMigrationServices",
"title": "Data Migration…"
},
{
Expand Down Expand Up @@ -636,8 +636,8 @@
},
{
"//": "[Collection] Data Migration",
"command": "vscode-documentdb.command.chooseDataMigrationExtension",
"when": "view =~ /connectionsView|discoveryView/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i && migrationProvidersAvailable",
"command": "vscode-documentdb.command.accessDataMigrationServices",
"when": "view =~ /connectionsView|discoveryView/ && viewItem =~ /treeitem[.]mongoCluster(?![a-z.\\/])/i",
"group": "1@2"
},
{
Expand Down Expand Up @@ -748,7 +748,7 @@
"when": "never"
},
{
"command": "vscode-documentdb.command.chooseDataMigrationExtension",
"command": "vscode-documentdb.command.accessDataMigrationServices",
"when": "never"
},
{
Expand Down Expand Up @@ -912,9 +912,16 @@
]
},
"x-documentdbApi": {
"registeredClients": [
"vscode-cosmosdb",
"vscode-mongo-migration"
"verifiedClients": [
"ms-azurecosmosdbtools.vscode-mongo-migration"
]
}
},
"x-announcedMigrationProviders": [
{
"id": "ms-azurecosmosdbtools.vscode-mongo-migration",
"name": "Azure Cosmos DB Migration",
"description": "Assess and migrate your databases to Azure Cosmos DB.",
"url": "https://marketplace.visualstudio.com/items?itemName=ms-azurecosmosdbtools.vscode-mongo-migration"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { nonNullValue, type IActionContext } from '@microsoft/vscode-azext-utils';
import * as l10n from '@vscode/l10n';
import { commands, QuickPickItemKind, type QuickPickItem } from 'vscode';
import { CredentialCache } from '../../documentdb/CredentialCache';
import { MigrationService } from '../../services/migrationServices';
import { type ClusterItemBase } from '../../tree/documentdb/ClusterItemBase';
import { openUrl } from '../../utils/openUrl';

const ANNOUNCED_PROVIDER_PREFIX = 'announced-provider';

export async function accessDataMigrationServices(context: IActionContext, node: ClusterItemBase) {
const installedProviders: (QuickPickItem & { id: string })[] = MigrationService.listProviders()
// Map to QuickPickItem format
.map((provider) => ({
id: provider.id,
label: provider.label,
detail: provider.description,
iconPath: provider.iconPath,

group: 'Installed Providers',
alwaysShow: true,
}))
// Sort alphabetically
.sort((a, b) => a.label.localeCompare(b.label));

const announcedProviers: (QuickPickItem & { id: string })[] = MigrationService.listAnnouncedProviders(true)
// Map to QuickPickItem format
.map((provider) => ({
id: `${ANNOUNCED_PROVIDER_PREFIX}-${provider.id}`, // please note, the prefix is a magic string here, and needed to correctly support vs code marketplace integration
label: `$(extensions) ${provider.name}`,
detail: `Open the VS Code Marketplace to learn more about "${provider.name}"`,
url: provider.url,

marketplaceId: provider.id,
group: 'Visit Marketplace',
alwaysShow: true,
}))
// Sort alphabetically
.sort((a, b) => a.label.localeCompare(b.label));

const commonItems = [
// {
// id: 'addMigrationProvider',
// label: l10n.t('Add New Migration Provider…'),
// detail: l10n.t('Explore more data migration providers.'),
// iconPath: new ThemeIcon('plus'),

// group: 'Migration Providers',
// alwaysShow: true,
// },
{ label: '', kind: QuickPickItemKind.Separator },
{
id: 'learnMore',
label: l10n.t('Learn more…'),
detail: l10n.t('Learn more about DocumentDB and MongoDB migrations.'),

url: 'https://aka.ms/vscode-documentdb-migration-support',

group: 'Learn More',
alwaysShow: true,
},
];

const selectedItem = await context.ui.showQuickPick([...installedProviders, ...announcedProviers, ...commonItems], {
enableGrouping: true,
placeHolder: l10n.t('Choose the data migration provider…'),
stepName: 'selectMigrationProvider',
suppressPersistence: true,
});

context.telemetry.properties.connectionMode = selectedItem.id;

if (selectedItem.id === 'learnMore') {
context.telemetry.properties.migrationLearnMore = 'true';
if ('url' in selectedItem && selectedItem.url) {
await openUrl(selectedItem.url);
}
}

if (selectedItem.id?.startsWith(ANNOUNCED_PROVIDER_PREFIX)) {
context.telemetry.properties.migrationAddProvider = 'true';
if ('marketplaceId' in selectedItem && selectedItem.marketplaceId) {
commands.executeCommand('extension.open', selectedItem.marketplaceId);
}
}

// if (selectedItem.id === 'addMigrationProvider') {
// context.telemetry.properties.addMigrationProvider = 'true';
// commands.executeCommand('workbench.extensions.search', '"DocumentDB Migration Plugin"');
// return;
// }

if (installedProviders.some((provider) => provider.id === selectedItem.id)) {
const selectedProvider = MigrationService.getProvider(nonNullValue(selectedItem.id, 'selectedItem.id'));

if (!selectedProvider) {
return;
}

context.telemetry.properties.migrationProvider = selectedProvider.id;

// Check if the selected provider requires authentication for the default action
if (selectedProvider.requiresAuthentication) {
const authenticated = await ensureAuthentication(context, node);
if (!authenticated) {
void context.ui.showWarningMessage(
l10n.t('Authentication is required to use this migration provider.'),
{
modal: true,
detail: l10n.t('Please authenticate first by expanding the tree item of the selected cluster.'),
},
);
return;
}
}

try {
// Construct the options object with available context
const options = {
connectionString: await node.getConnectionString(),
extendedProperties: {
clusterId: node.cluster.id,
},
};

// Get available actions from the provider
const availableActions = await selectedProvider.getAvailableActions(options);

if (availableActions.length === 0) {
// No actions available, execute default action
return selectedProvider.executeAction(options);
}

// Extend actions with Learn More option if provider has a learn more URL
const extendedActions: (QuickPickItem & {
id: string;
url?: string;
requiresAuthentication?: boolean;
})[] = [...availableActions];

const url = selectedProvider.getLearnMoreUrl?.();

if (url) {
extendedActions.push(
{ id: 'separator', label: '', kind: QuickPickItemKind.Separator },
{
id: 'learnMore',
label: l10n.t('Learn more…'),
detail: l10n.t('Learn more about {0}.', selectedProvider.label),
url,
alwaysShow: true,
},
);
}

// Show action picker to user
const selectedAction = await context.ui.showQuickPick(extendedActions, {
placeHolder: l10n.t('Choose the migration action…'),
stepName: 'selectMigrationAction',
suppressPersistence: true,
});

if (selectedAction.id === 'learnMore') {
context.telemetry.properties.migrationLearnMore = 'true';
if (selectedAction.url) {
await openUrl(selectedAction.url);
}
return;
}

// Check if selected action requires authentication
if (selectedAction.requiresAuthentication) {
const authenticated = await ensureAuthentication(context, node);
if (!authenticated) {
void context.ui.showWarningMessage(l10n.t('Authentication is required to run this action.'), {
modal: true,
detail: l10n.t('Please authenticate first by expanding the tree item of the selected cluster.'),
});
return;
}
}

context.telemetry.properties.migrationAction = selectedAction.id;

// Execute the selected action
await selectedProvider.executeAction(options, selectedAction.id);
} catch (error) {
// Log the error and re-throw to be handled by the caller
console.error('Error during migration provider execution:', error);
throw error;
}
}
}

/**
* Ensures the user is authenticated for migration operations.
* This function should be implemented to handle the specific authentication flow
* required by the host extension.
*
* @param context - The action context for UI operations and telemetry
* @returns Promise<boolean> - true if authentication succeeded, false otherwise
*/
async function ensureAuthentication(_context: IActionContext, _node: ClusterItemBase): Promise<boolean> {
if (CredentialCache.hasCredentials(_node.cluster.id)) {
return Promise.resolve(true); // Credentials already exist, no need to authenticate again
}

return Promise.resolve(false); // Return false until implementation is complete
}
Loading
Loading