diff --git a/CHANGELOG.md b/CHANGELOG.md index f7a2d8213..41eb91ed4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Change Log +## 0.5.2 + +### New Features & Improvements + +- **Updated Migration API for Integrations**: This release introduces API versioning for the DocumentDB extension API and adds support for a new, more robust v0.3.0 API. The changes update documentation, interfaces, and implementation to reflect the new API version, including stricter provider registration and context validation. The migration provider workflow and usage examples have been clarified, and deprecated API versions are documented. Additional improvements include dependency updates, better credential handling, and minor localization and client registration changes. [#321](https://github.com/microsoft/vscode-documentdb/issues/321), [#322](https://github.com/microsoft/vscode-documentdb/pull/322) + +## 0.5.1 + +### Fixes + +- **Connection String Parsing**: Resolved an issue where connection strings containing special characters (e.g., `@`) in query parameters, such as those from Azure Cosmos DB (`appName=@myaccount@`), would fail to parse. The connection string parser now properly sanitizes query parameters before parsing, ensuring reliable connections. [#314](https://github.com/microsoft/vscode-documentdb/issues/314), [#316](https://github.com/microsoft/vscode-documentdb/pull/316) + ## 0.5.0 ### New Features & Improvements diff --git a/api/README.md b/api/README.md index e56bc9c6d..1abc6f351 100644 --- a/api/README.md +++ b/api/README.md @@ -4,25 +4,32 @@ This package provides the Extension API for integrating with the VS Code DocumentDB extension. +## API Versioning + +- **v0.3.0 (Latest)**: The current and only supported version. It requires the extension context for provider registration and enforces a one-provider-per-extension rule. +- **v0.2.0 (Deprecated)**: This version is deprecated and will be removed in a future release. New integrations should not use it. + ## Installation ```bash -npm install --save-dev @ +npm install --save-dev vscode-documentdb-api-experimental-beta ``` -## Usage +## Usage (v0.3.0) ```typescript +import * as vscode from 'vscode'; import { getDocumentDBExtensionApi, - MigrationProvider, - MigrationProviderPickItem, - ActionsOptions, -} from '@microsoft/vscode-documentdb-api'; + type DocumentDBExtensionApiV030, // Import the v0.3.0 API interface + type MigrationProvider, + type MigrationProviderPickItem, + type ActionsOptions, +} from 'vscode-documentdb-api-experimental-beta'; export async function activate(context: vscode.ExtensionContext) { - // Get the DocumentDB extension API - const api = await getDocumentDBExtensionApi(context, '0.1.0'); + // Get the DocumentDB extension API and cast it to the v0.3.0 type + const api = (await getDocumentDBExtensionApi(context, '0.3.0')) as DocumentDBExtensionApiV030; // Create your migration provider const myProvider: MigrationProvider = { @@ -67,8 +74,9 @@ export async function activate(context: vscode.ExtensionContext) { }, }; - // Register your provider - api.migration.registerProvider(myProvider); + // Register your provider using the extension context. + // Note: Each extension can only register one provider. + api.migration.registerProvider(context, myProvider); } ``` @@ -90,25 +98,25 @@ A migration provider must implement the following interface: **Required Methods:** -- `getAvailableActions(options?: ActionsOptions)`: Returns a list of actions the user can choose from -- `executeAction(id?: string)`: Executes the selected action or a default action +- `getAvailableActions(options?: ActionsOptions)`: Returns a list of actions the user can choose from. +- `executeAction(id?: string)`: Executes the selected action or a default action. **Optional Properties:** -- `requiresAuthentication`: Indicates if authentication is required for the default operation (when no custom actions are provided) +- `requiresAuthentication`: Indicates if authentication is required for the default operation (when no custom actions are provided). **Optional Methods:** -- `getLearnMoreUrl()`: Returns a URL for more information about the provider +- `getLearnMoreUrl()`: Returns a URL for more information about the provider. ### Workflow The migration provider workflow follows these steps: -1. **Get Available Actions**: The system calls `getAvailableActions()` to retrieve a list of possible operations -2. **User Selection**: If actions are returned, they are presented to the user for selection -3. **Execute Action**: The system calls `executeAction()` with the selected action's ID -4. **Default Execution**: If `getAvailableActions()` returns an empty array, `executeAction()` is called immediately without parameters +1. **Get Available Actions**: The system calls `getAvailableActions()` to retrieve a list of possible operations. +2. **User Selection**: If actions are returned, they are presented to the user for selection. +3. **Execute Action**: The system calls `executeAction()` with the selected action's ID. +4. **Default Execution**: If `getAvailableActions()` returns an empty array, `executeAction()` is called immediately without parameters. ### Supporting Interfaces diff --git a/api/package-lock.json b/api/package-lock.json index b6e214131..c0b9a9de9 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -1,15 +1,16 @@ { "name": "vscode-documentdb-api-experimental-beta", - "version": "0.2.0", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vscode-documentdb-api-experimental-beta", - "version": "0.2.0", + "version": "0.3.1", "license": "MIT", "devDependencies": { "@microsoft/api-extractor": "^7.38.0", + "@microsoft/vscode-azext-utils": "~3.3.1", "@types/node": "^18.0.0", "@types/vscode": "^1.90.0", "rimraf": "^6.0.1", @@ -19,6 +20,14 @@ "vscode": "^1.90.0" } }, + "node_modules/@azure/ms-rest-azure-env": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-azure-env/-/ms-rest-azure-env-2.0.0.tgz", + "integrity": "sha512-dG76W7ElfLi+fbTjnZVGj+M9e0BIEJmRxU6fHaUQ12bZBe8EJKYb2GV50YWNaP2uJiVQ5+7nXEVj1VN1UQtaEw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -37,6 +46,34 @@ "node": ">=12" } }, + "node_modules/@microsoft/1ds-core-js": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.10.tgz", + "integrity": "sha512-5fSZmkGwWkH+mrIA5M1GYPZdPM+SjXwCCl2Am7VhFoVwOBJNhRnwvIpAdzw6sFjiebN/rz+/YH0NdxztGZSa9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/applicationinsights-core-js": "3.3.10", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.4 < 2.x", + "@nevware21/ts-utils": ">= 0.11.8 < 2.x" + } + }, + "node_modules/@microsoft/1ds-post-js": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.10.tgz", + "integrity": "sha512-VSLjc9cT+Y+eTiSfYltJHJCejn8oYr0E6Pq2BMhOEO7F6IyLGYIxzKKvo78ze9x+iHX7KPTATcZ+PFgjGXuNqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/1ds-core-js": "4.3.10", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.4 < 2.x", + "@nevware21/ts-utils": ">= 0.11.8 < 2.x" + } + }, "node_modules/@microsoft/api-extractor": { "version": "7.52.8", "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.52.8.tgz", @@ -88,6 +125,95 @@ "node": ">=14.17" } }, + "node_modules/@microsoft/applicationinsights-channel-js": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.10.tgz", + "integrity": "sha512-iolFLz1ocWAzIQqHIEjjov3gNTPkgFQ4ArHnBcJEYoffOGWlJt6copaevS5YPI5rHzmbySsengZ8cLJJBBrXzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/applicationinsights-common": "3.3.10", + "@microsoft/applicationinsights-core-js": "3.3.10", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.4 < 2.x", + "@nevware21/ts-utils": ">= 0.11.8 < 2.x" + }, + "peerDependencies": { + "tslib": ">= 1.0.0" + } + }, + "node_modules/@microsoft/applicationinsights-common": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.10.tgz", + "integrity": "sha512-RVIenPIvNgZCbjJdALvLM4rNHgAFuHI7faFzHCgnI6S2WCUNGHeXlQTs9EUUrL+n2TPp9/cd0KKMILU5VVyYiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/applicationinsights-core-js": "3.3.10", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-utils": ">= 0.11.8 < 2.x" + }, + "peerDependencies": { + "tslib": ">= 1.0.0" + } + }, + "node_modules/@microsoft/applicationinsights-core-js": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.10.tgz", + "integrity": "sha512-5yKeyassZTq2l+SAO4npu6LPnbS++UD+M+Ghjm9uRzoBwD8tumFx0/F8AkSVqbniSREd+ztH/2q2foewa2RZyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.4 < 2.x", + "@nevware21/ts-utils": ">= 0.11.8 < 2.x" + }, + "peerDependencies": { + "tslib": ">= 1.0.0" + } + }, + "node_modules/@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "node_modules/@microsoft/applicationinsights-web-basic": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.10.tgz", + "integrity": "sha512-AZib5DAT3NU0VT0nLWEwXrnoMDDgZ/5S4dso01CNU5ELNxLdg+1fvchstlVdMy4FrAnxzs8Wf/GIQNFYOVgpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/applicationinsights-channel-js": "3.3.10", + "@microsoft/applicationinsights-common": "3.3.10", + "@microsoft/applicationinsights-core-js": "3.3.10", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.4 < 2.x", + "@nevware21/ts-utils": ">= 0.11.8 < 2.x" + }, + "peerDependencies": { + "tslib": ">= 1.0.0" + } + }, + "node_modules/@microsoft/dynamicproto-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", + "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nevware21/ts-utils": ">= 0.10.4 < 2.x" + } + }, "node_modules/@microsoft/tsdoc": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", @@ -108,6 +234,54 @@ "resolve": "~1.22.2" } }, + "node_modules/@microsoft/vscode-azext-utils": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-3.3.3.tgz", + "integrity": "sha512-rltLtVeUTUNHEeGzyw7A0GoRhHNBRWRpB6N2LEETBUXn5J06EqgXg/K6JxO2NCooCAi+eI+g1uSUCn2AM4DsTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/vscode-azureresources-api": "^2.3.1", + "@vscode/extension-telemetry": "^0.9.6", + "dayjs": "^1.11.2", + "escape-string-regexp": "^2.0.0", + "html-to-text": "^8.2.0", + "semver": "^7.3.7", + "uuid": "^9.0.0", + "vscode-tas-client": "^0.1.84", + "vscode-uri": "^3.0.6" + }, + "peerDependencies": { + "@azure/ms-rest-azure-env": "^2.0.0" + } + }, + "node_modules/@microsoft/vscode-azureresources-api": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azureresources-api/-/vscode-azureresources-api-2.6.3.tgz", + "integrity": "sha512-uwFHLc9fsbuBPKC/WOU+p5JMj9VyNyU1k+3T1uFp00l4OMmazqBqiJYKao6jc/d525hy9FW6EzniGPHdocKApA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@azure/ms-rest-azure-env": "^2.0.0" + } + }, + "node_modules/@nevware21/ts-async": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.4.tgz", + "integrity": "sha512-IBTyj29GwGlxfzXw2NPnzty+w0Adx61Eze1/lknH/XIVdxtF9UnOpk76tnrHXWa6j84a1RR9hsOcHQPFv9qJjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nevware21/ts-utils": ">= 0.11.6 < 2.x" + } + }, + "node_modules/@nevware21/ts-utils": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.12.5.tgz", + "integrity": "sha512-JPQZWPKQJjj7kAftdEZL0XDFfbMgXCGiUAZe0d7EhLC3QlXTlZdSckGqqRIQ2QNl0VTEZyZUvRBw6Ednw089Fw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rushstack/node-core-library": { "version": "5.13.1", "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.13.1.tgz", @@ -193,6 +367,20 @@ "string-argv": "~0.3.1" } }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.6.0.tgz", + "integrity": "sha512-J3jpy002TyBjd4N/p6s+s90eX42H2eRhK3SbsZuvTDv977/E8p2U3zikdiehyJja66do7FlxLomZLPlvl2/xaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^4.2.0", + "selderee": "^0.6.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/@types/argparse": { "version": "1.0.38", "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", @@ -217,6 +405,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@vscode/extension-telemetry": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.9.tgz", + "integrity": "sha512-WG/H+H/JRMPnpbXMufXgXlaeJwKszXfAanOERV/nkXBbYyNw0KR84JjUjSg+TgkzYEF/ttRoHTP6fFZWkXdoDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/1ds-core-js": "^4.3.4", + "@microsoft/1ds-post-js": "^4.3.4", + "@microsoft/applicationinsights-web-basic": "^3.3.4" + }, + "engines": { + "vscode": "^1.75.0" + } + }, "node_modules/ajv": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", @@ -341,6 +544,13 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -363,6 +573,89 @@ "node": ">= 8" } }, + "node_modules/dayjs": { + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", + "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -377,6 +670,26 @@ "dev": true, "license": "MIT" }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -506,6 +819,57 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-to-text": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-8.2.1.tgz", + "integrity": "sha512-aN/3JvAk8qFsWVeE9InWAWueLXrbkoVZy0TkzaGhoRBC2gCFEeRLDDJN3/ijIGHohy6H+SZzUQWN/hcYtaPK8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.6.0", + "deepmerge": "^4.2.2", + "he": "^1.2.0", + "htmlparser2": "^6.1.0", + "minimist": "^1.2.6", + "selderee": "^0.6.0" + }, + "bin": { + "html-to-text": "bin/cli.js" + }, + "engines": { + "node": ">=10.23.2" + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, "node_modules/import-lazy": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", @@ -622,6 +986,16 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -632,6 +1006,36 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "bin": { + "nearley-railroad": "bin/nearley-railroad.js", + "nearley-test": "bin/nearley-test.js", + "nearley-unparse": "bin/nearley-unparse.js", + "nearleyc": "bin/nearleyc.js" + }, + "funding": { + "type": "individual", + "url": "https://nearley.js.org/#give-to-nearley" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -639,6 +1043,20 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/parseley": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.7.0.tgz", + "integrity": "sha512-xyOytsdDu077M3/46Am+2cGXEKM9U9QclBDv7fimY7e+BBlxh2JcBp2mgNsmkyA9uvgyTjVzDi7cP1v4hcFxbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "moo": "^0.5.1", + "nearley": "^2.20.1" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -683,6 +1101,27 @@ "node": ">=6" } }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -714,6 +1153,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12" + } + }, "node_modules/rimraf": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", @@ -734,6 +1183,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/selderee": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.6.0.tgz", + "integrity": "sha512-ibqWGV5aChDvfVdqNYuaJP/HnVBhlRGSRrlbttmlMpHcLuTqqbMH36QkSs9GEgj5M88JDYLI8eyP94JaQ8xRlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "parseley": "^0.7.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -972,6 +1434,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tas-client": { + "version": "0.2.33", + "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.2.33.tgz", + "integrity": "sha512-V+uqV66BOQnWxvI6HjDnE4VkInmYZUQ4dgB7gzaDyFyFSK1i1nF/j7DpS9UbQAgV9NaF1XpcyuavnM1qOeiEIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "peer": true + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -1013,6 +1490,40 @@ "punycode": "^2.1.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vscode-tas-client": { + "version": "0.1.84", + "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.1.84.tgz", + "integrity": "sha512-rUTrUopV+70hvx1hW5ebdw1nd6djxubkLvVxjGdyD/r5v/wcVF41LIfiAtbm5qLZDtQdsMH1IaCuDoluoIa88w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tas-client": "0.2.33" + }, + "engines": { + "vscode": "^1.85.0" + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/api/package.json b/api/package.json index 64e5757d2..6219f7448 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "vscode-documentdb-api-experimental-beta", - "version": "0.2.0", + "version": "0.3.1", "description": "Extension API for VS Code DocumentDB extension (preview)", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -27,6 +27,7 @@ "license": "MIT", "devDependencies": { "@microsoft/api-extractor": "^7.38.0", + "@microsoft/vscode-azext-utils": "~3.3.1", "@types/node": "^18.0.0", "@types/vscode": "^1.90.0", "rimraf": "^6.0.1", diff --git a/api/src/extensionApi.ts b/api/src/extensionApi.ts index 68c0a6585..8b321d53b 100644 --- a/api/src/extensionApi.ts +++ b/api/src/extensionApi.ts @@ -3,19 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type MigrationApi } from './migration/migrationApi'; +import { type AzureExtensionApi } from '@microsoft/vscode-azext-utils'; +import { type MigrationApi, type MigrationApiV030 } from './migration/migrationApi'; /** * The main API interface for the DocumentDB extension */ -export interface DocumentDBExtensionApi { +export interface DocumentDBExtensionApi extends AzureExtensionApi { /** - * API version for compatibility checking + * Migration-related APIs */ - readonly apiVersion: string; + readonly migration: MigrationApi; +} +/** + * The main API interface for the DocumentDB extension (v0.3.0) + */ +export interface DocumentDBExtensionApiV030 extends AzureExtensionApi { /** - * Migration-related APIs + * Migration-related APIs (v0.3.0) */ - readonly migration: MigrationApi; + readonly migration: MigrationApiV030; } diff --git a/api/src/migration/migrationApi.ts b/api/src/migration/migrationApi.ts index 248bc65c6..c648a0b0d 100644 --- a/api/src/migration/migrationApi.ts +++ b/api/src/migration/migrationApi.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type * as vscode from 'vscode'; import { type MigrationProvider } from './migrationProvider'; /** @@ -16,3 +17,18 @@ export interface MigrationApi { */ registerProvider(provider: MigrationProvider): void; } + +/** + * API for migration-related functionality (v0.3.0). + * Supports provider registration with extension context. + */ +export interface MigrationApiV030 { + /** + * Registers a migration provider with extension context validation. + * Each extension can only register one provider. + * @param context The calling extension's context + * @param provider The migration provider to register + * @throws Error if the extension already has a provider registered + */ + registerProvider(context: vscode.ExtensionContext, provider: MigrationProvider): void; +} diff --git a/api/src/utils/getApi.ts b/api/src/utils/getApi.ts index 4c4af96f1..cea11f0e7 100644 --- a/api/src/utils/getApi.ts +++ b/api/src/utils/getApi.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { type apiUtils } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; -import { type DocumentDBExtensionApi } from '../extensionApi'; +import { type DocumentDBExtensionApi, type DocumentDBExtensionApiV030 } from '../extensionApi'; // The actual extension ID based on the package.json const DOCUMENTDB_EXTENSION_ID = 'ms-azuretools.vscode-documentdb'; @@ -33,25 +34,30 @@ function isValidPackageJson(packageJson: unknown): packageJson is DocumentDBApiC * experimental phase ends. Contributors wishing to join in this phase are asked to reach out to us. * * @param context The calling extension context - * @param apiVersionRange The required API version (not checked in this simple implementation) + * @param apiVersionRange The required API version ('0.2.0' or '0.3.0') * @returns The DocumentDB extension API * @throws Error if the extension is not installed or calling extension is not whitelisted * * @example * ```typescript - * const api = await getDocumentDBExtensionApi(context, '0.1.0'); + * // For API v0.2.0 + * const api = await getDocumentDBExtensionApi(context, '0.2.0'); * api.migration.registerProvider(myProvider); + * + * // For API v0.3.0 (requires extension context) + * const api = await getDocumentDBExtensionApi(context, '0.3.0') as DocumentDBExtensionApiV030; + * api.migration.registerProvider(context, myProvider); * ``` */ export async function getDocumentDBExtensionApi( - _context: vscode.ExtensionContext, + context: vscode.ExtensionContext, apiVersionRange: string, -): Promise { +): Promise { // 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(DOCUMENTDB_EXTENSION_ID); + const extension = vscode.extensions.getExtension(DOCUMENTDB_EXTENSION_ID); if (!extension) { throw new Error(`Extension '${DOCUMENTDB_EXTENSION_ID}' is not installed.`); } @@ -78,16 +84,20 @@ export async function getDocumentDBExtensionApi( await extension.activate(); } - const api = extension.exports; + const exportedApis = extension.exports; - if (!api) { - throw new Error(`Extension '${DOCUMENTDB_EXTENSION_ID}' does not export an API.`); + if (!exportedApis) { + throw new Error(`Extension '${DOCUMENTDB_EXTENSION_ID}' does not export any API.`); } + const selectedApi = exportedApis.getApi(apiVersionRange, { + extensionId: callingExtensionId, + }); + // Simple version check (you can enhance this later) - if (api.apiVersion !== apiVersionRange) { - console.warn(`API version mismatch. Expected ${apiVersionRange}, got ${api.apiVersion}`); + if (selectedApi.apiVersion !== apiVersionRange) { + console.warn(`API version mismatch. Expected ${apiVersionRange}, got ${selectedApi.apiVersion}`); } - return api; + return selectedApi; } diff --git a/docs/index.md b/docs/index.md index 9ce8172b2..8a20d0100 100644 --- a/docs/index.md +++ b/docs/index.md @@ -64,9 +64,9 @@ The User Manual provides guidance on using DocumentDB for VS Code. It contains d Explore the history of updates and improvements to the DocumentDB for VS Code extension. Each release brings new features, enhancements, and fixes to improve your experience. -- [0.5](./release-notes/0.5) -- [0.4](./release-notes/0.4) -- [0.3, 0.3.1](./release-notes/0.3) +- [0.5](./release-notes/0.5) | [0.5.1](./release-notes/0.5#patch-release-v051) | [0.5.2](./release-notes/0.5#patch-release-v052) +- [0.4](./release-notes/0.4) | [0.4.1](./release-notes/0.4#patch-release-v041) +- [0.3](./release-notes/0.3) | [0.3.1](./release-notes/0.3#patch-release-v031) - [0.2.4](./release-notes/0.2.4) - [0.2.3](./release-notes/0.2.3) - [0.2.2](./release-notes/0.2.2) diff --git a/docs/release-notes/0.5.md b/docs/release-notes/0.5.md index 487b41ada..d2ebd7eb8 100644 --- a/docs/release-notes/0.5.md +++ b/docs/release-notes/0.5.md @@ -47,7 +47,36 @@ This feature was introduced in PR [#289](https://github.com/microsoft/vscode-doc - **Updating connection authentication from EntraID to UserName/Password fails ([#284](https://github.com/microsoft/vscode-documentdb/issues/284))** - Corrected a failure that occurred when updating a connection's authentication method from Entra ID to a username/password. The connection now updates and connects successfully. -## Changelog +--- + +## Patch Release v0.5.1 + +This patch release addresses a critical issue with connection string parsing, ensuring more reliable connections for Azure Cosmos DB and other services. + +### What's Changed in v0.5.1 + +#### **Improved Connection String Parsing** ([#314](https://github.com/microsoft/vscode-documentdb/issues/314), [#316](https://github.com/microsoft/vscode-documentdb/pull/316)) + +We've resolved an issue where connection strings containing special characters (e.g., `@`) in query parameters, such as those from Azure Cosmos DB (`appName=@myaccount@`), would fail to parse. The connection string parser now properly sanitizes query parameters before parsing, ensuring reliable connections even with complex connection strings. + +### Changelog + +See the full changelog entry for this release: +➡️ [CHANGELOG.md#051](https://github.com/microsoft/vscode-documentdb/blob/main/CHANGELOG.md#051) + +--- + +## Patch Release v0.5.2 + +This patch release introduces API versioning for the DocumentDB extension API and adds support for a new, more robust v0.3.0 API. + +### What's Changed in v0.5.2 + +#### **Updated Migration API for Integrations** ([#321](https://github.com/microsoft/vscode-documentdb/issues/321), [#322](https://github.com/microsoft/vscode-documentdb/pull/322)) + +This release introduces API versioning for the DocumentDB extension API and adds support for a new, more robust v0.3.0 API. The changes update documentation, interfaces, and implementation to reflect the new API version, including stricter provider registration and context validation. The migration provider workflow and usage examples have been clarified, and deprecated API versions are documented. Additional improvements include dependency updates, better credential handling, and minor localization and client registration changes. + +### Changelog See the full changelog entry for this release: -➡️ [CHANGELOG.md#050](https://github.com/microsoft/vscode-documentdb/blob/main/CHANGELOG.md#050) +➡️ [CHANGELOG.md#052](https://github.com/microsoft/vscode-documentdb/blob/main/CHANGELOG.md#052) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 9c0cf9f31..acad8057c 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -45,6 +45,7 @@ "An element with the following id already exists: {id}": "An element with the following id already exists: {id}", "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 v0.3.0: Registered new migration provider: \"{providerId}\" - \"{providerLabel}\" from extension \"{extensionId}\"": "API v0.3.0: Registered new migration provider: \"{providerId}\" - \"{providerLabel}\" from extension \"{extensionId}\"", "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 migration provider: \"{providerId}\" - \"{providerLabel}\"": "API: Registered new migration provider: \"{providerId}\" - \"{providerLabel}\"", "Applying Azure discovery filters…": "Applying Azure discovery filters…", @@ -304,7 +305,6 @@ "Loading Content": "Loading Content", "Loading document {num} of {countUri}": "Loading document {num} of {countUri}", "Loading documents…": "Loading documents…", - "Loading migration actions…": "Loading migration actions…", "Loading resources...": "Loading resources...", "Loading Subscriptions…": "Loading Subscriptions…", "Loading Tenant Filter Options…": "Loading Tenant Filter Options…", diff --git a/package-lock.json b/package-lock.json index 07b58bce8..1838996b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-documentdb", - "version": "0.5.0", + "version": "0.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vscode-documentdb", - "version": "0.5.0", + "version": "0.5.2", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@azure/arm-compute": "^22.4.0", diff --git a/package.json b/package.json index 812a698b1..e08afef7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vscode-documentdb", - "version": "0.5.0", + "version": "0.5.2", "aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255", "publisher": "ms-azuretools", "displayName": "DocumentDB for VS Code", @@ -962,7 +962,8 @@ "x-documentdbApi": { "registeredClients": [ "vscode-cosmosdb", - "vscode-mongo-migration" + "vscode-mongo-migration", + "ms-azurecosmosdbtools.vscode-mongo-migration" ] } } diff --git a/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts b/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts index 469ebecf7..4d90ed401 100644 --- a/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts +++ b/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts @@ -104,66 +104,59 @@ export async function chooseDataMigrationExtension(context: IActionContext, node throw new Error(l10n.t('No credentials found for the selected cluster.')); } - const parsedCS = new DocumentDBConnectionString(credentials.connectionString); - parsedCS.username = CredentialCache.getConnectionUser(node.cluster.id) ?? ''; - parsedCS.password = CredentialCache.getConnectionPassword(node.cluster.id) ?? ''; + // TODO: Include a dialog box for users to approve sharing credentials with a 3rd-party extension + // This should be done when the provider is used, each time the action states it "requiredAuthentication". + // We should allow whitelisting extensions trusted by the user to avoid repeated prompts. + // This could be done on our own but available for the user to edit in settings. + const parsedCS_WithCredentials = new DocumentDBConnectionString(credentials.connectionString); + parsedCS_WithCredentials.username = CredentialCache.getConnectionUser(node.cluster.id) ?? ''; + parsedCS_WithCredentials.password = CredentialCache.getConnectionPassword(node.cluster.id) ?? ''; const options = { - connectionString: parsedCS.toString(), + connectionString: parsedCS_WithCredentials.toString(), extendedProperties: { clusterId: node.cluster.id, }, }; // Get available actions from the provider - const availableActions = await selectedProvider.getAvailableActions(options); + const availableActions: (QuickPickItem & { + id: string; + learnMoreUrl?: string; + requiresAuthentication?: boolean; + })[] = (await selectedProvider.getAvailableActions(options)).map((action) => ({ + id: action.id, + label: action.label, + detail: action.description, + iconPath: action.iconPath, + alwaysShow: action.alwaysShow, + requiresAuthentication: action.requiresAuthentication, + })); if (availableActions.length === 0) { // No actions available, execute default action await selectedProvider.executeAction(options); } else { - // Create async function to provide better loading UX and debugging experience - const getActionQuickPickItems = async (): Promise< - (QuickPickItem & { - id: string; - learnMoreUrl?: string; - requiresAuthentication?: boolean; - })[] - > => { - // Get available actions from the provider - const actions = await selectedProvider.getAvailableActions(options); - - // Extend actions with Learn More option if provider has a learn more URL - const extendedActions: (QuickPickItem & { - id: string; - learnMoreUrl?: string; - requiresAuthentication?: boolean; - })[] = [...actions]; - - const learnMoreUrl = selectedProvider.getLearnMoreUrl?.(); - - if (learnMoreUrl) { - extendedActions.push( - { id: 'separator', label: '', kind: QuickPickItemKind.Separator }, - { - id: 'learnMore', - label: l10n.t('Learn more…'), - detail: l10n.t('Learn more about {0}.', selectedProvider.label), - learnMoreUrl, - alwaysShow: true, - }, - ); - } - - return extendedActions; - }; + const learnMoreUrl = selectedProvider.getLearnMoreUrl?.(); + + if (learnMoreUrl) { + availableActions.push( + { id: 'separator', label: '', kind: QuickPickItemKind.Separator }, + { + id: 'learnMore', + label: l10n.t('Learn more…'), + detail: l10n.t('Learn more about {0}.', selectedProvider.label), + learnMoreUrl, + alwaysShow: true, + }, + ); + } // Show action picker to user - const selectedAction = await context.ui.showQuickPick(getActionQuickPickItems(), { + const selectedAction = await context.ui.showQuickPick(availableActions, { placeHolder: l10n.t('Choose the migration action…'), stepName: 'selectMigrationAction', suppressPersistence: true, - loadingPlaceHolder: l10n.t('Loading migration actions…'), }); if (selectedAction.id === 'learnMore') { diff --git a/src/documentdb/utils/DocumentDBConnectionString.test.ts b/src/documentdb/utils/DocumentDBConnectionString.test.ts index 0d56a79d7..9df860c28 100644 --- a/src/documentdb/utils/DocumentDBConnectionString.test.ts +++ b/src/documentdb/utils/DocumentDBConnectionString.test.ts @@ -163,4 +163,387 @@ describe('DocumentDBConnectionString', () => { expect(baseUrl.search).toBe(documentDBUrl.search); }); }); + + describe('constructor with special characters in query parameters', () => { + it('should parse connection string with @ in appName parameter', () => { + // This is the exact case from the issue - the base class would fail to parse this + const uri = + 'mongodb://myaccount.a-host.local:10255/?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@myaccount@'; + + const connStr = new DocumentDBConnectionString(uri); + + expect(connStr.hosts).toEqual(['myaccount.a-host.local:10255']); + expect(connStr.username).toBe(''); + expect(connStr.password).toBe(''); + expect(connStr.searchParams.get('ssl')).toBe('true'); + expect(connStr.searchParams.get('replicaSet')).toBe('globaldb'); + expect(connStr.searchParams.get('retrywrites')).toBe('false'); + expect(connStr.searchParams.get('maxIdleTimeMS')).toBe('120000'); + // URLSearchParams.get() returns decoded values + expect(connStr.searchParams.get('appName')).toBe('@myaccount@'); + }); + + it('should parse connection string with multiple @ in different parameters', () => { + const uri = 'mongodb://host.example.com:27017/?appName=@user@&tag=@prod@'; + + const connStr = new DocumentDBConnectionString(uri); + + expect(connStr.hosts).toEqual(['host.example.com:27017']); + expect(connStr.username).toBe(''); + expect(connStr.password).toBe(''); + // URLSearchParams.get() returns decoded values + expect(connStr.searchParams.get('appName')).toBe('@user@'); + expect(connStr.searchParams.get('tag')).toBe('@prod@'); + }); + + it('should parse connection string with # in query parameters', () => { + // Note: # is a fragment identifier in URLs, so anything after # is considered a fragment, not a query param + // We encode # to %23 to include it in query parameter values + const uri = 'mongodb://host.example.com:27017/?tag=prod%23123&appName=app%231'; + + const connStr = new DocumentDBConnectionString(uri); + + expect(connStr.hosts).toEqual(['host.example.com:27017']); + // URLSearchParams.get() returns decoded values + expect(connStr.searchParams.get('tag')).toBe('prod#123'); + expect(connStr.searchParams.get('appName')).toBe('app#1'); + }); + + it('should parse connection string with [] in query parameters', () => { + const uri = 'mongodb://host.example.com:27017/?tag=[prod]&filter=[active]'; + + const connStr = new DocumentDBConnectionString(uri); + + expect(connStr.hosts).toEqual(['host.example.com:27017']); + // URLSearchParams.get() returns decoded values + expect(connStr.searchParams.get('tag')).toBe('[prod]'); + expect(connStr.searchParams.get('filter')).toBe('[active]'); + }); + + it('should handle connection string without query parameters', () => { + const uri = 'mongodb://host.example.com:27017/database'; + + const connStr = new DocumentDBConnectionString(uri); + + expect(connStr.hosts).toEqual(['host.example.com:27017']); + expect(connStr.pathname).toBe('/database'); + expect(connStr.username).toBe(''); + expect(connStr.password).toBe(''); + }); + + it('should handle connection string with query parameters but no special characters', () => { + const uri = 'mongodb://host.example.com:27017/?ssl=true&replicaSet=rs0'; + + const connStr = new DocumentDBConnectionString(uri); + + expect(connStr.hosts).toEqual(['host.example.com:27017']); + expect(connStr.searchParams.get('ssl')).toBe('true'); + expect(connStr.searchParams.get('replicaSet')).toBe('rs0'); + }); + + it('should handle normal connection strings without issues', () => { + // Ensure regular, well-formed connection strings work correctly + const uri = 'mongodb://localhost:27017/mydb?ssl=true&authSource=admin'; + + const connStr = new DocumentDBConnectionString(uri); + + expect(connStr.hosts).toEqual(['localhost:27017']); + expect(connStr.pathname).toBe('/mydb'); + expect(connStr.searchParams.get('ssl')).toBe('true'); + expect(connStr.searchParams.get('authSource')).toBe('admin'); + }); + + it('should handle parameters without values', () => { + const uri = 'mongodb://host.example.com:27017/?ssl&replicaSet=rs0'; + + const connStr = new DocumentDBConnectionString(uri); + + expect(connStr.hosts).toEqual(['host.example.com:27017']); + expect(connStr.searchParams.has('ssl')).toBe(true); + expect(connStr.searchParams.get('replicaSet')).toBe('rs0'); + }); + }); + + describe('constructor with credentials and special characters in query parameters', () => { + it('should parse connection string with credentials and @ in query parameters', () => { + const uri = 'mongodb://user:pass@host.example.com:27017/?appName=@myapp@'; + + const connStr = new DocumentDBConnectionString(uri); + + expect(connStr.hosts).toEqual(['host.example.com:27017']); + expect(connStr.username).toBe('user'); + expect(connStr.password).toBe('pass'); + // URLSearchParams.get() returns decoded values + expect(connStr.searchParams.get('appName')).toBe('@myapp@'); + }); + + it('should handle credentials with special characters and @ in query parameters', () => { + const uri = 'mongodb://host.example.com:27017/?appName=@app@'; + const connStr = new DocumentDBConnectionString(uri); + + // Set username and password using setters + connStr.username = 'user@domain'; + connStr.password = 'p@ss!word#123'; + + expect(connStr.username).toBe('user@domain'); + expect(connStr.password).toBe('p@ss!word#123'); + // URLSearchParams.get() returns decoded values + expect(connStr.searchParams.get('appName')).toBe('@app@'); + }); + + it('should encode and decode username with special characters', () => { + const uri = 'mongodb://host.example.com:27017/'; + const connStr = new DocumentDBConnectionString(uri); + + // Test various special characters in username + const testUsername = 'user@domain.com'; + connStr.username = testUsername; + + expect(connStr.username).toBe(testUsername); + }); + + it('should encode and decode password with special characters', () => { + const uri = 'mongodb://host.example.com:27017/'; + const connStr = new DocumentDBConnectionString(uri); + + // Test various special characters in password + const testPassword = 'p@ss:w/ord?#[]'; + connStr.password = testPassword; + + expect(connStr.password).toBe(testPassword); + }); + }); + + describe('username setter and getter', () => { + it('should properly encode and decode username', () => { + const uri = 'mongodb://host.example.com:27017/'; + const connStr = new DocumentDBConnectionString(uri); + + const username = 'user@domain.com'; + connStr.username = username; + + expect(connStr.username).toBe(username); + }); + + it('should handle empty username', () => { + const uri = 'mongodb://host.example.com:27017/'; + const connStr = new DocumentDBConnectionString(uri); + + connStr.username = ''; + + expect(connStr.username).toBe(''); + }); + + it('should handle username with special characters', () => { + const uri = 'mongodb://host.example.com:27017/'; + const connStr = new DocumentDBConnectionString(uri); + + const username = 'user+tag@domain.com'; + connStr.username = username; + + expect(connStr.username).toBe(username); + }); + + it('should preserve username through toString and re-parsing', () => { + const uri = 'mongodb://initialuser@host.example.com:27017/'; + const connStr = new DocumentDBConnectionString(uri); + + const newUsername = 'user@domain.com'; + connStr.username = newUsername; + connStr.password = 'somePassword'; + + const connectionStringText = connStr.toString(); + const reparsed = new DocumentDBConnectionString(connectionStringText); + + expect(reparsed.username).toBe(newUsername); + }); + }); + + describe('validateUsername', () => { + it('should validate normal usernames', () => { + expect(DocumentDBConnectionString.validateUsername('user')).toBe(true); + expect(DocumentDBConnectionString.validateUsername('user123')).toBe(true); + }); + + it('should validate usernames with special characters', () => { + expect(DocumentDBConnectionString.validateUsername('user@domain')).toBe(true); + expect(DocumentDBConnectionString.validateUsername('user+tag')).toBe(true); + }); + + it('should validate empty username', () => { + expect(DocumentDBConnectionString.validateUsername('')).toBe(true); + }); + }); + + describe('real-world Azure Cosmos DB connection strings', () => { + it('should parse Azure Cosmos DB for MongoDB RU connection string', () => { + const uri = + 'mongodb://myaccount.a-host.local:10255/?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@myaccount@'; + + const connStr = new DocumentDBConnectionString(uri); + + expect(connStr.hosts).toEqual(['myaccount.a-host.local:10255']); + expect(connStr.searchParams.get('ssl')).toBe('true'); + expect(connStr.searchParams.get('replicaSet')).toBe('globaldb'); + expect(connStr.searchParams.get('retrywrites')).toBe('false'); + }); + + it('should parse Azure Cosmos DB connection string with credentials', () => { + const uri = 'mongodb://myaccount.a-host.local:10255/?ssl=true&appName=@myaccount@'; + const connStr = new DocumentDBConnectionString(uri); + + // Simulate adding credentials after construction + connStr.username = 'myaccount'; + connStr.password = 'someComplexKey123=='; + + expect(connStr.username).toBe('myaccount'); + expect(connStr.password).toBe('someComplexKey123=='); + expect(connStr.hosts).toEqual(['myaccount.a-host.local:10255']); + expect(connStr.searchParams.get('ssl')).toBe('true'); + }); + + it('should handle MongoDB Atlas-style connection strings', () => { + const uri = 'mongodb://cluster0.mongodb.net:27017/?retryWrites=true&w=majority&appName=myapp'; + + const connStr = new DocumentDBConnectionString(uri); + + expect(connStr.hosts).toEqual(['cluster0.mongodb.net:27017']); + expect(connStr.searchParams.get('retryWrites')).toBe('true'); + expect(connStr.searchParams.get('w')).toBe('majority'); + expect(connStr.searchParams.get('appName')).toBe('myapp'); + }); + + it('should handle connection string with database name and special chars in params', () => { + const uri = 'mongodb://host.example.com:27017/mydb?authSource=admin&appName=@myapp@'; + + const connStr = new DocumentDBConnectionString(uri); + + expect(connStr.hosts).toEqual(['host.example.com:27017']); + expect(connStr.pathname).toBe('/mydb'); + expect(connStr.searchParams.get('authSource')).toBe('admin'); + // URLSearchParams.get() returns decoded values + expect(connStr.searchParams.get('appName')).toBe('@myapp@'); + }); + }); + + describe('duplicate query parameter keys', () => { + it('should preserve duplicate readPreference parameters', () => { + const uri = 'mongodb://host.example.com:27017/?readPreference=secondary&readPreference=primary'; + + const connStr = new DocumentDBConnectionString(uri); + + expect(connStr.hosts).toEqual(['host.example.com:27017']); + // URLSearchParams.getAll() returns all values for a key + const readPreferences = connStr.searchParams.getAll('readPreference'); + expect(readPreferences).toEqual(['secondary', 'primary']); + }); + + it('should preserve duplicate tag parameters', () => { + const uri = 'mongodb://host.example.com:27017/?tag=prod&tag=us-east&tag=critical'; + + const connStr = new DocumentDBConnectionString(uri); + + const tags = connStr.searchParams.getAll('tag'); + expect(tags).toEqual(['prod', 'us-east', 'critical']); + }); + + it('should preserve duplicate parameters with special characters', () => { + const uri = 'mongodb://host.example.com:27017/?appName=@app1@&appName=@app2@&ssl=true'; + + const connStr = new DocumentDBConnectionString(uri); + + const appNames = connStr.searchParams.getAll('appName'); + expect(appNames).toEqual(['@app1@', '@app2@']); + expect(connStr.searchParams.get('ssl')).toBe('true'); + }); + + it('should maintain order of duplicate parameters', () => { + const uri = 'mongodb://host.example.com:27017/?a=1&b=2&a=3&c=4&a=5'; + + const connStr = new DocumentDBConnectionString(uri); + + const aValues = connStr.searchParams.getAll('a'); + expect(aValues).toEqual(['1', '3', '5']); + expect(connStr.searchParams.get('b')).toBe('2'); + expect(connStr.searchParams.get('c')).toBe('4'); + }); + + it('should handle duplicate parameters in toString and re-parsing', () => { + const uri = 'mongodb://user:pass@host.example.com:27017/?tag=prod&tag=critical'; + + const connStr = new DocumentDBConnectionString(uri); + const connStrText = connStr.toString(); + + // Re-parse the connection string + const reparsed = new DocumentDBConnectionString(connStrText); + const tags = reparsed.searchParams.getAll('tag'); + + // Should preserve all tag values + expect(tags).toEqual(['prod', 'critical']); + }); + + it('should handle mixed duplicate and unique parameters', () => { + const uri = 'mongodb://host.example.com:27017/?ssl=true&tag=prod&tag=us-east&replicaSet=rs0&tag=critical'; + + const connStr = new DocumentDBConnectionString(uri); + + expect(connStr.searchParams.get('ssl')).toBe('true'); + expect(connStr.searchParams.get('replicaSet')).toBe('rs0'); + + const tags = connStr.searchParams.getAll('tag'); + expect(tags).toEqual(['prod', 'us-east', 'critical']); + }); + }); + + describe('edge cases with special characters in query parameters', () => { + it('should handle connection string with only @ in one parameter', () => { + const uri = 'mongodb://host.example.com:27017/?tag=@'; + + const connStr = new DocumentDBConnectionString(uri); + + // URLSearchParams.get() returns decoded values + expect(connStr.searchParams.get('tag')).toBe('@'); + }); + + it('should handle connection string with already encoded parameters', () => { + const uri = 'mongodb://host.example.com:27017/?appName=%40user%40'; + + const connStr = new DocumentDBConnectionString(uri); + + // URLSearchParams.get() returns decoded values + expect(connStr.searchParams.get('appName')).toBe('@user@'); + }); + + it('should handle multiple hosts', () => { + const uri = 'mongodb://host1:27017,host2:27017,host3:27017/?appName=@app@'; + + const connStr = new DocumentDBConnectionString(uri); + + expect(connStr.hosts).toEqual(['host1:27017', 'host2:27017', 'host3:27017']); + // URLSearchParams.get() returns decoded values + expect(connStr.searchParams.get('appName')).toBe('@app@'); + }); + + it('should handle SRV connection strings with special chars in params', () => { + const uri = 'mongodb+srv://cluster.mongodb.net/?appName=@myapp@'; + + const connStr = new DocumentDBConnectionString(uri); + + expect(connStr.isSRV).toBe(true); + // URLSearchParams.get() returns decoded values + expect(connStr.searchParams.get('appName')).toBe('@myapp@'); + }); + + it('should handle mixed special characters in parameters', () => { + // Note: # must be encoded as %23, otherwise it's treated as a URL fragment + const uri = 'mongodb://host.example.com:27017/?tag1=@user@&tag2=[prod]&tag3=test%231'; + + const connStr = new DocumentDBConnectionString(uri); + + // URLSearchParams.get() returns decoded values + expect(connStr.searchParams.get('tag1')).toBe('@user@'); + expect(connStr.searchParams.get('tag2')).toBe('[prod]'); + expect(connStr.searchParams.get('tag3')).toBe('test#1'); + }); + }); }); diff --git a/src/documentdb/utils/DocumentDBConnectionString.ts b/src/documentdb/utils/DocumentDBConnectionString.ts index 36a8393da..c644d0064 100644 --- a/src/documentdb/utils/DocumentDBConnectionString.ts +++ b/src/documentdb/utils/DocumentDBConnectionString.ts @@ -3,13 +3,90 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import ConnectionString from 'mongodb-connection-string-url'; +import ConnectionString, { type ConnectionStringParsingOptions } from 'mongodb-connection-string-url'; /** - * Extends the ConnectionString class to properly handle password encoding/decoding. - * The base ConnectionString class has issues with certain special characters in passwords. + * Extends the ConnectionString class to properly handle password encoding/decoding + * and special characters in query parameters. + * + * The base ConnectionString class has two main issues: + * 1. Improper handling of special characters in passwords + * 2. Incorrect parsing when '@' characters appear in query parameters (e.g., appName=@user@) + * because the regex-based parser looks for '@' to separate credentials from the host */ export class DocumentDBConnectionString extends ConnectionString { + /** + * Constructor that pre-processes the connection string to handle special characters + * in query parameters before passing to the base class. + * + * @param uri - The MongoDB connection string + * @param options - Optional parsing options + * + * @example + * // This would fail in the base class due to '@' in appName parameter: + * // mongodb://host:10255/?appName=@user@ + * const connStr = new DocumentDBConnectionString( + * 'mongodb://myaccount.a-host.local:10255/?ssl=true&appName=@myaccount@' + * ); + */ + constructor(uri: string, options?: ConnectionStringParsingOptions) { + const sanitizedUri = DocumentDBConnectionString.sanitizeConnectionString(uri); + super(sanitizedUri, options); + } + + /** + * Pre-processes a connection string to encode special characters in query parameters + * that would otherwise confuse the base ConnectionString parser. + * + * The base parser uses regex to find '@' characters to separate credentials from the host. + * However, '@' characters in query parameters (e.g., appName=@tnaumowicz-ru400@) cause + * incorrect parsing, making the parser think there are credentials when there aren't. + * + * This method: + * 1. Separates the connection string into protocol, authority, and query sections + * 2. Uses URLSearchParams to parse the query string (handles edge cases better) + * 3. Re-encodes all parameter values (not keys) using encodeURIComponent + * 4. Reconstructs the connection string with properly encoded values + * + * @param uri - The original connection string + * @returns A sanitized connection string safe for the base parser + */ + private static sanitizeConnectionString(uri: string): string { + // Find the query string section (everything after the first '?') + const queryStartIndex = uri.indexOf('?'); + + // If there's no query string, return as-is + if (queryStartIndex === -1) { + return uri; + } + + // Split into base URL and query string + const baseUrl = uri.substring(0, queryStartIndex); + const queryString = uri.substring(queryStartIndex + 1); + + // Use URLSearchParams to parse the query string + // This handles edge cases like empty values, multiple values, etc. + const searchParams = new URLSearchParams(queryString); + const encodedParams: string[] = []; + + // Get all unique keys (without duplicates) + const uniqueKeys = [...new Set([...searchParams.keys()])]; + + // Process each key, preserving all values for duplicate keys + for (const key of uniqueKeys) { + // Get all values for this key (supports duplicate parameters) + const values = searchParams.getAll(key); + + // Encode each value and add to the result + for (const value of values) { + const encodedValue = encodeURIComponent(value); + encodedParams.push(`${key}=${encodedValue}`); + } + } + + return `${baseUrl}?${encodedParams.join('&')}`; + } + /** * Override the password setter to properly encode the password before setting it. * This prevents encoding issues in the underlying ConnectionString implementation. @@ -34,6 +111,29 @@ export class DocumentDBConnectionString extends ConnectionString { } } + /** + * Override the username getter to properly decode the username when retrieving it. + * This ensures consistency with password handling. + */ + public get username(): string { + const encodedUsername = super.username; + try { + return encodedUsername ? decodeURIComponent(encodedUsername) : ''; + } catch (err) { + console.warn('Failed to decode connection string username', err); + return encodedUsername; + } + } + + /** + * Override the username setter to properly encode the username before setting it. + * This ensures consistency with password handling. + */ + public set username(value: string) { + const properlyEncodedUsername = encodeURIComponent(value); + super.username = properlyEncodedUsername; + } + /** * Validates that a password can be properly encoded and decoded. * Returns true if the password will be handled correctly, false otherwise. @@ -47,4 +147,18 @@ export class DocumentDBConnectionString extends ConnectionString { return false; } } + + /** + * Validates that a username can be properly encoded and decoded. + * Returns true if the username will be handled correctly, false otherwise. + */ + public static validateUsername(username: string): boolean { + try { + const encoded = encodeURIComponent(username); + const decoded = decodeURIComponent(encoded); + return decoded === username; + } catch { + return false; + } + } } diff --git a/src/extension.ts b/src/extension.ts index 6ceec42cc..1037f4d03 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -14,7 +14,6 @@ import { registerErrorHandler, registerUIExtensionVariables, TreeElementStateManager, - type AzureExtensionApi, type IActionContext, } from '@microsoft/vscode-azext-utils'; import * as l10n from '@vscode/l10n'; @@ -24,7 +23,7 @@ import { ext } from './extensionVariables'; import { globalUriHandler } from './vscodeUriHandler'; // Import the DocumentDB Extension API interfaces import { type AzureResourcesExtensionApi } from '@microsoft/vscode-azureresources-api'; -import { type DocumentDBExtensionApi } from '../api/src'; +import { type DocumentDBExtensionApi, type DocumentDBExtensionApiV030 } from '../api/src'; import { MigrationService } from './services/migrationServices'; export async function activateInternal( @@ -72,8 +71,8 @@ export async function activateInternal( //registerReportIssueCommand('azureDatabases.reportIssue'); }); - // Create the DocumentDB Extension API - const documentDBApi: DocumentDBExtensionApi = { + // Create the DocumentDB Extension API v0.2.0 + const documentDBApiV2: DocumentDBExtensionApi = { apiVersion: '0.2.0', migration: { registerProvider: (provider) => { @@ -89,17 +88,31 @@ export async function activateInternal( }, }; - // Return both the DocumentDB API and Azure Extension API - return { - ...documentDBApi, - ...createApiProvider([ - { - findTreeItem: () => undefined, - pickTreeItem: () => undefined, - revealTreeItem: () => undefined, - apiVersion: '1.2.0', + // Create the DocumentDB Extension API v0.3.0 + const documentDBApiV3: DocumentDBExtensionApiV030 = { + apiVersion: '0.3.0', + migration: { + registerProvider: (context: vscode.ExtensionContext, provider) => { + const extensionId = context.extension.id; + MigrationService.registerProviderWithContext(extensionId, provider); + + ext.outputChannel.appendLine( + vscode.l10n.t( + 'API v0.3.0: Registered new migration provider: "{providerId}" - "{providerLabel}" from extension "{extensionId}"', + { + providerId: provider.id, + providerLabel: provider.label, + extensionId: extensionId, + }, + ), + ); }, - ]), + }, + }; + + // Return DocumentDB Extension API provider supporting multiple versions + return { + ...createApiProvider([documentDBApiV2, documentDBApiV3]), }; } diff --git a/src/services/migrationServices.ts b/src/services/migrationServices.ts index b437b073d..bc93e261d 100644 --- a/src/services/migrationServices.ts +++ b/src/services/migrationServices.ts @@ -67,13 +67,58 @@ export interface ActionsOptions { */ class MigrationServiceImpl { private migrationProviders: Map = new Map(); + private extensionProviders: Map = new Map(); // Maps extension ID to provider ID + /** + * Registers a migration provider (API v0.2.0). + * This method does not track which extension registered the provider. + * Multiple providers can be registered without restrictions. + * + * @param provider The migration provider to register + */ public registerProvider(provider: MigrationProvider): void { this.migrationProviders.set(provider.id, provider); this.updateContext(); } + /** + * Registers a migration provider with extension context validation (API v0.3.0). + * This method enforces that each extension can only register one migration provider. + * If an extension attempts to register a second provider, an error will be thrown. + * + * @param extensionId The ID of the extension registering the provider + * @param provider The migration provider to register + * @throws Error if the extension has already registered a provider + */ + public registerProviderWithContext(extensionId: string, provider: MigrationProvider): void { + // Check if this extension already has a provider registered + const existingProviderId = this.extensionProviders.get(extensionId); + if (existingProviderId) { + throw new Error( + `Extension '${extensionId}' has already registered a migration provider with ID '${existingProviderId}'. ` + + `Each extension can only register one migration provider.`, + ); + } + + // Register the provider + this.migrationProviders.set(provider.id, provider); + this.extensionProviders.set(extensionId, provider.id); + this.updateContext(); + } + public unregisterProvider(id: string): boolean { + // Remove from both maps + const provider = this.migrationProviders.get(id); + if (provider) { + // Find and remove the extension mapping + for (const [extensionId, providerId] of this.extensionProviders.entries()) { + if (providerId === id) { + this.extensionProviders.delete(extensionId); + break; + } + } + } + const result = this.migrationProviders.delete(id); this.updateContext(); return result;