-
Notifications
You must be signed in to change notification settings - Fork 112
feat: update connectionString appName param - [MCP-68] #406
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 28 commits
c5c91e9
4cf78c2
026b91a
680e1e1
92aab61
eca40e1
6cf0ae6
524d965
72f1ab8
c3f0928
f8de877
8048cf6
0c620b2
d148376
13a0349
bb97041
5fb0284
a690850
a2e1091
f964e12
6e922e6
4ba3a16
78d430a
ae1d6d0
68a462e
720be15
565ca2b
69609b4
bdf1272
658cdc8
dae4f06
858ce1e
0bc1a9f
b0972bd
e570562
8e62d2e
0ed8d23
abf285a
e7727ee
f9c22d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,59 @@ | ||
import { MongoClientOptions } from "mongodb"; | ||
import ConnectionString from "mongodb-connection-string-url"; | ||
|
||
export function setAppNameParamIfMissing({ | ||
export interface AppNameComponents { | ||
appName: string; | ||
deviceId?: Promise<string>; | ||
clientName?: string; | ||
} | ||
|
||
/** | ||
* Sets the appName parameter with the extended format: appName--deviceId--clientName | ||
* Only sets the appName if it's not already present in the connection string | ||
* @param connectionString - The connection string to modify | ||
* @param components - The components to build the appName from | ||
* @returns The modified connection string | ||
*/ | ||
export async function setAppNameParamIfMissing({ | ||
connectionString, | ||
defaultAppName, | ||
components, | ||
}: { | ||
connectionString: string; | ||
defaultAppName?: string; | ||
}): string { | ||
components: AppNameComponents; | ||
}): Promise<string> { | ||
const connectionStringUrl = new ConnectionString(connectionString); | ||
|
||
const searchParams = connectionStringUrl.typedSearchParams<MongoClientOptions>(); | ||
|
||
if (!searchParams.has("appName") && defaultAppName !== undefined) { | ||
searchParams.set("appName", defaultAppName); | ||
// Only set appName if it's not already present | ||
if (searchParams.has("appName")) { | ||
gagik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return connectionStringUrl.toString(); | ||
} | ||
|
||
const appName = components.appName || "unknown"; | ||
const deviceId = components.deviceId ? await components.deviceId : "unknown"; | ||
const clientName = components.clientName || "unknown"; | ||
|
||
// Build the extended appName format: appName--deviceId--clientName | ||
const extendedAppName = `${appName}--${deviceId}--${clientName}`; | ||
|
||
searchParams.set("appName", extendedAppName); | ||
|
||
return connectionStringUrl.toString(); | ||
} | ||
|
||
/** | ||
* Validates the connection string | ||
* @param connectionString - The connection string to validate | ||
* @param looseValidation - Whether to allow loose validation | ||
* @returns void | ||
* @throws Error if the connection string is invalid | ||
*/ | ||
export function validateConnectionString(connectionString: string, looseValidation: boolean): void { | ||
try { | ||
new ConnectionString(connectionString, { looseValidation }); | ||
} catch (error) { | ||
throw new Error( | ||
`Invalid connection string with error: ${error instanceof Error ? error.message : String(error)}` | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
import { getDeviceId } from "@mongodb-js/device-id"; | ||
import nodeMachineId from "node-machine-id"; | ||
import { LogId, LoggerBase } from "../common/logger.js"; | ||
|
||
export const DEVICE_ID_TIMEOUT = 3000; | ||
gagik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* Singleton class for managing device ID retrieval and caching. | ||
* Starts device ID calculation early and is shared across all services. | ||
*/ | ||
export class DeviceIdService { | ||
private static instance: DeviceIdService | undefined = undefined; | ||
private deviceId: string | undefined = undefined; | ||
private deviceIdPromise: Promise<string> | undefined = undefined; | ||
private abortController: AbortController | undefined = undefined; | ||
private logger: LoggerBase; | ||
private readonly getMachineId: () => Promise<string>; | ||
private timeout: number; | ||
|
||
private constructor(logger: LoggerBase, timeout: number) { | ||
this.logger = logger; | ||
this.timeout = timeout; | ||
this.getMachineId = (): Promise<string> => nodeMachineId.machineId(true); | ||
// Start device ID calculation immediately | ||
this.startDeviceIdCalculation(); | ||
} | ||
|
||
/** | ||
* Initializes the DeviceIdService singleton with a logger. | ||
* A separated init method is used to use a single instance of the logger. | ||
* @param logger - The logger instance to use | ||
* @returns The DeviceIdService instance | ||
*/ | ||
public static init(logger: LoggerBase, timeout?: number): DeviceIdService { | ||
if (DeviceIdService.instance) { | ||
return DeviceIdService.instance; | ||
} | ||
DeviceIdService.instance = new DeviceIdService(logger, timeout ?? DEVICE_ID_TIMEOUT); | ||
return DeviceIdService.instance; | ||
} | ||
blva marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* Checks if the DeviceIdService is initialized. | ||
* @returns True if the DeviceIdService is initialized, false otherwise | ||
*/ | ||
public static isInitialized(): boolean { | ||
return DeviceIdService.instance !== undefined; | ||
} | ||
|
||
/** | ||
* Gets the singleton instance of DeviceIdService. | ||
* @returns The DeviceIdService instance | ||
*/ | ||
public static getInstance(): DeviceIdService { | ||
if (!DeviceIdService.instance) { | ||
throw new Error("DeviceIdService not initialized"); | ||
} | ||
return DeviceIdService.instance; | ||
} | ||
blva marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* Starts the device ID calculation process. | ||
* This method is called automatically in the constructor. | ||
*/ | ||
private startDeviceIdCalculation(): void { | ||
if (this.deviceIdPromise) { | ||
return; | ||
} | ||
|
||
this.abortController = new AbortController(); | ||
this.deviceIdPromise = this.calculateDeviceId(); | ||
} | ||
|
||
/** | ||
* Gets the device ID, waiting for the calculation to complete if necessary. | ||
* @returns Promise that resolves to the device ID string | ||
*/ | ||
public async getDeviceId(): Promise<string> { | ||
if (this.deviceId !== undefined) { | ||
return this.deviceId; | ||
} | ||
|
||
if (!this.deviceIdPromise) { | ||
throw new Error("DeviceIdService calculation not started"); | ||
} | ||
|
||
return this.deviceIdPromise; | ||
} | ||
/** | ||
* Aborts any ongoing device ID calculation. | ||
*/ | ||
public close(): void { | ||
if (this.abortController) { | ||
this.abortController.abort(); | ||
this.abortController = undefined; | ||
} | ||
this.deviceId = undefined; | ||
this.deviceIdPromise = undefined; | ||
DeviceIdService.instance = undefined; | ||
} | ||
|
||
/** | ||
* Internal method that performs the actual device ID calculation. | ||
*/ | ||
private async calculateDeviceId(): Promise<string> { | ||
if (!this.abortController) { | ||
throw new Error("Device ID calculation not started"); | ||
} | ||
|
||
try { | ||
const deviceId = await getDeviceId({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does it automatically assign unknown upon error? If so we can remove |
||
getMachineId: this.getMachineId, | ||
onError: (reason, error) => { | ||
this.handleDeviceIdError(reason, String(error)); | ||
}, | ||
timeout: this.timeout, | ||
abortSignal: this.abortController.signal, | ||
}); | ||
|
||
// Cache the result | ||
this.deviceId = deviceId; | ||
return deviceId; | ||
} catch (error) { | ||
// Check if this was an abort error | ||
if (error instanceof Error && error.name === "AbortError") { | ||
throw error; // Re-throw abort errors | ||
} | ||
|
||
this.logger.debug({ | ||
id: LogId.deviceIdResolutionError, | ||
context: "deviceId", | ||
message: `Failed to get device ID: ${String(error)}`, | ||
}); | ||
|
||
// Cache the fallback value | ||
this.deviceId = "unknown"; | ||
return "unknown"; | ||
} finally { | ||
this.abortController = undefined; | ||
} | ||
} | ||
|
||
/** | ||
* Handles device ID error. | ||
* @param reason - The reason for the error | ||
* @param error - The error object | ||
*/ | ||
private handleDeviceIdError(reason: string, error: string): void { | ||
switch (reason) { | ||
case "resolutionError": | ||
this.logger.debug({ | ||
id: LogId.deviceIdResolutionError, | ||
context: "deviceId", | ||
message: `Resolution error: ${String(error)}`, | ||
}); | ||
break; | ||
case "timeout": | ||
this.logger.debug({ | ||
id: LogId.deviceIdTimeout, | ||
context: "deviceId", | ||
message: "Device ID retrieval timed out", | ||
noRedaction: true, | ||
}); | ||
break; | ||
case "abort": | ||
// No need to log in the case of aborts | ||
break; | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.