Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 registration 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,217 @@
/*---------------------------------------------------------------------------------------------
* 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 announcedProviders: (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, ...announcedProviders, ...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