From 852734662854a4997f88613d4ab913cb65480ae3 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 22 Oct 2025 12:11:47 +0200 Subject: [PATCH 01/17] fix: added support for unescaped parameters in connection strings Update src/documentdb/utils/DocumentDBConnectionString.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> fix: updated comments --- .../utils/DocumentDBConnectionString.test.ts | 313 ++++++++++++++++++ .../utils/DocumentDBConnectionString.ts | 113 ++++++- 2 files changed, 423 insertions(+), 3 deletions(-) diff --git a/src/documentdb/utils/DocumentDBConnectionString.test.ts b/src/documentdb/utils/DocumentDBConnectionString.test.ts index 0d56a79d7..145eb82b1 100644 --- a/src/documentdb/utils/DocumentDBConnectionString.test.ts +++ b/src/documentdb/utils/DocumentDBConnectionString.test.ts @@ -163,4 +163,317 @@ 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('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', () => { + const uri = 'mongodb://host.example.com:27017/?tag1=@user@&tag2=[prod]&tag3=test#1'; + + 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..b58ad2264 100644 --- a/src/documentdb/utils/DocumentDBConnectionString.ts +++ b/src/documentdb/utils/DocumentDBConnectionString.ts @@ -3,13 +3,83 @@ * 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[] = []; + + // Re-encode all parameter values consistently + for (const [key, value] of searchParams.entries()) { + // URLSearchParams already decodes values when iterating + // So we just need to re-encode them properly + 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 +104,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.error('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 +140,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; + } + } } From 8776c936cc7f1febe1d39d91b657f72f8fdc88ef Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 22 Oct 2025 12:34:17 +0200 Subject: [PATCH 02/17] chore: updated console error levels --- src/documentdb/utils/DocumentDBConnectionString.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/documentdb/utils/DocumentDBConnectionString.ts b/src/documentdb/utils/DocumentDBConnectionString.ts index b58ad2264..dfd054e8f 100644 --- a/src/documentdb/utils/DocumentDBConnectionString.ts +++ b/src/documentdb/utils/DocumentDBConnectionString.ts @@ -113,7 +113,7 @@ export class DocumentDBConnectionString extends ConnectionString { try { return encodedUsername ? decodeURIComponent(encodedUsername) : ''; } catch (err) { - console.error('Failed to decode connection string username', err); + console.warn('Failed to decode connection string username', err); return encodedUsername; } } From b499cd4e495db1bda259304b62e9453f54daf4ee Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 22 Oct 2025 12:36:30 +0200 Subject: [PATCH 03/17] fix: test updated based on a copilot review recommendation --- src/documentdb/utils/DocumentDBConnectionString.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/documentdb/utils/DocumentDBConnectionString.test.ts b/src/documentdb/utils/DocumentDBConnectionString.test.ts index 145eb82b1..880f071c4 100644 --- a/src/documentdb/utils/DocumentDBConnectionString.test.ts +++ b/src/documentdb/utils/DocumentDBConnectionString.test.ts @@ -466,7 +466,8 @@ describe('DocumentDBConnectionString', () => { }); it('should handle mixed special characters in parameters', () => { - const uri = 'mongodb://host.example.com:27017/?tag1=@user@&tag2=[prod]&tag3=test#1'; + // 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); From af20c1552ff662fccb856b17ab7176bcaef91709 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 22 Oct 2025 12:47:55 +0200 Subject: [PATCH 04/17] doc: release notes + change log --- CHANGELOG.md | 6 ++++++ docs/index.md | 6 +++--- docs/release-notes/0.5.md | 16 ++++++++++++++-- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7a2d8213..1228d91ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 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/docs/index.md b/docs/index.md index 9ce8172b2..7b55d3abb 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.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..c986dfd30 100644 --- a/docs/release-notes/0.5.md +++ b/docs/release-notes/0.5.md @@ -47,7 +47,19 @@ 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#050](https://github.com/microsoft/vscode-documentdb/blob/main/CHANGELOG.md#050) +➡️ [CHANGELOG.md#051](https://github.com/microsoft/vscode-documentdb/blob/main/CHANGELOG.md#051) From 82c54198af158ced1bb3ff8d153649ba14ef9988 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 22 Oct 2025 12:49:06 +0200 Subject: [PATCH 05/17] version bump to `0.5.1` --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 07b58bce8..de614a08a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-documentdb", - "version": "0.5.0", + "version": "0.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vscode-documentdb", - "version": "0.5.0", + "version": "0.5.1", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@azure/arm-compute": "^22.4.0", diff --git a/package.json b/package.json index 812a698b1..efc07e329 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vscode-documentdb", - "version": "0.5.0", + "version": "0.5.1", "aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255", "publisher": "ms-azuretools", "displayName": "DocumentDB for VS Code", From 8e57e97e31f1652af7dfb6375b7925bcfe630499 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 22 Oct 2025 12:58:11 +0200 Subject: [PATCH 06/17] fix: added support for duplicate keys in connection strings (rare edge case) --- .../utils/DocumentDBConnectionString.test.ts | 69 +++++++++++++++++++ .../utils/DocumentDBConnectionString.ts | 19 +++-- 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/src/documentdb/utils/DocumentDBConnectionString.test.ts b/src/documentdb/utils/DocumentDBConnectionString.test.ts index 880f071c4..9df860c28 100644 --- a/src/documentdb/utils/DocumentDBConnectionString.test.ts +++ b/src/documentdb/utils/DocumentDBConnectionString.test.ts @@ -426,6 +426,75 @@ describe('DocumentDBConnectionString', () => { }); }); + 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=@'; diff --git a/src/documentdb/utils/DocumentDBConnectionString.ts b/src/documentdb/utils/DocumentDBConnectionString.ts index dfd054e8f..c644d0064 100644 --- a/src/documentdb/utils/DocumentDBConnectionString.ts +++ b/src/documentdb/utils/DocumentDBConnectionString.ts @@ -69,12 +69,19 @@ export class DocumentDBConnectionString extends ConnectionString { const searchParams = new URLSearchParams(queryString); const encodedParams: string[] = []; - // Re-encode all parameter values consistently - for (const [key, value] of searchParams.entries()) { - // URLSearchParams already decodes values when iterating - // So we just need to re-encode them properly - const encodedValue = encodeURIComponent(value); - encodedParams.push(`${key}=${encodedValue}`); + // 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('&')}`; From ba816de7e216606ede4c2a05a90b4550f2df1f47 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 29 Oct 2025 13:36:29 +0100 Subject: [PATCH 07/17] feat: migration API 0.3.0 with a limit of number of registered migration providers --- api/src/extensionApi.ts | 18 ++++++++----- api/src/migration/migrationApi.ts | 16 +++++++++++ api/src/utils/getApi.ts | 21 ++++++++++----- l10n/bundle.l10n.json | 1 + src/extension.ts | 41 ++++++++++++++++++---------- src/services/migrationServices.ts | 45 +++++++++++++++++++++++++++++++ 6 files changed, 115 insertions(+), 27 deletions(-) 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..ca7881b0d 100644 --- a/api/src/utils/getApi.ts +++ b/api/src/utils/getApi.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 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 +33,32 @@ 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.`); } diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 9c0cf9f31..098187cec 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…", 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; From da129a2a34885618a42bf7f6842f9164c2605c67 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 29 Oct 2025 13:47:50 +0100 Subject: [PATCH 08/17] Improvement Note, Create an Issue out of it --- .../chooseDataMigrationExtension.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts b/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts index 469ebecf7..121b7b4d9 100644 --- a/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts +++ b/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts @@ -104,12 +104,16 @@ 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 approove 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, }, From 42fcd3088c369ccee7abf23a586d5ea60847d427 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 29 Oct 2025 14:19:52 +0100 Subject: [PATCH 09/17] feat: publishing 0.3.0 of the migration api --- api/package-lock.json | 4 ++-- api/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index b6e214131..a1866d67d 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-documentdb-api-experimental-beta", - "version": "0.2.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vscode-documentdb-api-experimental-beta", - "version": "0.2.0", + "version": "0.3.0", "license": "MIT", "devDependencies": { "@microsoft/api-extractor": "^7.38.0", diff --git a/api/package.json b/api/package.json index 64e5757d2..3404c5984 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.0", "description": "Extension API for VS Code DocumentDB extension (preview)", "main": "dist/index.js", "types": "dist/index.d.ts", From a73586e6ea9cfac4df277cca7be986860434498b Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 29 Oct 2025 16:43:11 +0100 Subject: [PATCH 10/17] feat: new api client added --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index efc07e329..a8f1c130a 100644 --- a/package.json +++ b/package.json @@ -962,7 +962,8 @@ "x-documentdbApi": { "registeredClients": [ "vscode-cosmosdb", - "vscode-mongo-migration" + "vscode-mongo-migration", + "ms-azurecosmosdbtools.vscode-mongo-migration" ] } } From 33d7a63c35c168a9fca755825d0e3fbb36283066 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 29 Oct 2025 17:02:12 +0100 Subject: [PATCH 11/17] feat: added support for more api versions in the getApi code --- api/package.json | 2 +- api/src/utils/getApi.ts | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/api/package.json b/api/package.json index 3404c5984..5da8fde10 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "vscode-documentdb-api-experimental-beta", - "version": "0.3.0", + "version": "0.3.1", "description": "Extension API for VS Code DocumentDB extension (preview)", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/api/src/utils/getApi.ts b/api/src/utils/getApi.ts index ca7881b0d..cea11f0e7 100644 --- a/api/src/utils/getApi.ts +++ b/api/src/utils/getApi.ts @@ -3,6 +3,7 @@ * 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, type DocumentDBExtensionApiV030 } from '../extensionApi'; @@ -56,9 +57,7 @@ export async function getDocumentDBExtensionApi( 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.`); } @@ -85,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; } From 2b8c3fd15e3c863f359d53ab1eab9b3027508037 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 29 Oct 2025 17:45:19 +0100 Subject: [PATCH 12/17] feat: better UI for the migration integration --- l10n/bundle.l10n.json | 1 - .../chooseDataMigrationExtension.ts | 65 ++++++++----------- 2 files changed, 27 insertions(+), 39 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 098187cec..acad8057c 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -305,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/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts b/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts index 121b7b4d9..c8ddb357a 100644 --- a/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts +++ b/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts @@ -120,54 +120,43 @@ export async function chooseDataMigrationExtension(context: IActionContext, node }; // 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') { From 562f790be1cd4019d4b44b172f507a77daca0d5f Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 29 Oct 2025 18:20:34 +0100 Subject: [PATCH 13/17] feat: api readme update --- api/README.md | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) 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 From edaef83525368e43134b9ddfcace11b1bcde8868 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 29 Oct 2025 18:26:40 +0100 Subject: [PATCH 14/17] fix: added missing dependency, resolving ci build errors --- api/package-lock.json | 515 +++++++++++++++++++++++++++++++++++++++++- api/package.json | 1 + 2 files changed, 514 insertions(+), 2 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index a1866d67d..c0b9a9de9 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -1,15 +1,16 @@ { "name": "vscode-documentdb-api-experimental-beta", - "version": "0.3.0", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vscode-documentdb-api-experimental-beta", - "version": "0.3.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 5da8fde10..6219f7448 100644 --- a/api/package.json +++ b/api/package.json @@ -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", From f49a6ba1e201d837322f7039c67d079241352fb1 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 29 Oct 2025 18:27:21 +0100 Subject: [PATCH 15/17] chore: typo in comments Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../chooseDataMigrationExtension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts b/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts index c8ddb357a..4d90ed401 100644 --- a/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts +++ b/src/commands/chooseDataMigrationExtension/chooseDataMigrationExtension.ts @@ -104,7 +104,7 @@ export async function chooseDataMigrationExtension(context: IActionContext, node throw new Error(l10n.t('No credentials found for the selected cluster.')); } - // TODO: Include a dialog box for users to approove sharing credentials with a 3rd-party extension + // 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. From b28772a2bac87f26a393748cda33d53548589aac Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 29 Oct 2025 18:36:12 +0100 Subject: [PATCH 16/17] Updated changelog and release notes --- CHANGELOG.md | 6 ++++++ docs/index.md | 2 +- docs/release-notes/0.5.md | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1228d91ab..41eb91ed4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 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 diff --git a/docs/index.md b/docs/index.md index 7b55d3abb..8a20d0100 100644 --- a/docs/index.md +++ b/docs/index.md @@ -64,7 +64,7 @@ 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.5.1](./release-notes/0.5#patch-release-v051) +- [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) diff --git a/docs/release-notes/0.5.md b/docs/release-notes/0.5.md index c986dfd30..d2ebd7eb8 100644 --- a/docs/release-notes/0.5.md +++ b/docs/release-notes/0.5.md @@ -63,3 +63,20 @@ We've resolved an issue where connection strings containing special characters ( 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#052](https://github.com/microsoft/vscode-documentdb/blob/main/CHANGELOG.md#052) From 36847c5f37d362b2e59286fdb9e29ea0572af950 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 29 Oct 2025 18:20:47 +0100 Subject: [PATCH 17/17] Version bump to `0.5.2` --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index de614a08a..1838996b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-documentdb", - "version": "0.5.1", + "version": "0.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vscode-documentdb", - "version": "0.5.1", + "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 a8f1c130a..e08afef7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vscode-documentdb", - "version": "0.5.1", + "version": "0.5.2", "aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255", "publisher": "ms-azuretools", "displayName": "DocumentDB for VS Code",