Skip to content

Commit d90196e

Browse files
base implementation
1 parent 9b18859 commit d90196e

File tree

4 files changed

+220
-36
lines changed

4 files changed

+220
-36
lines changed

src/cmap/handshake/client_metadata.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@ import * as os from 'os';
22
import * as process from 'process';
33

44
import { BSON, type Document, Int32 } from '../../bson';
5-
import { MongoInvalidArgumentError, MongoRuntimeError } from '../../error';
6-
import type { MongoOptions } from '../../mongo_client';
5+
import { MongoInvalidArgumentError } from '../../error';
6+
import type { DriverInfo, MongoOptions } from '../../mongo_client';
77
import { fileIsAccessible } from '../../utils';
88

99
// eslint-disable-next-line @typescript-eslint/no-require-imports
1010
const NODE_DRIVER_VERSION = require('../../../package.json').version;
1111

12+
/** @internal */
13+
export function isDriverInfoEqual(info1: DriverInfo, info2: DriverInfo): boolean {
14+
return (
15+
info1.name === info2.name &&
16+
info1.platform === info2.platform &&
17+
info1.version === info2.version
18+
);
19+
}
20+
1221
/**
1322
* @public
1423
* @deprecated This interface will be made internal in the next major release.
@@ -115,19 +124,11 @@ export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMe
115124
metadataDocument.ifItFitsItSits('application', { name });
116125
}
117126

118-
const { name = '', version = '', platform = '' } = options.driverInfo;
119-
120127
const driverInfo = {
121-
name: name.length > 0 ? `nodejs|${name}` : 'nodejs',
122-
version: version.length > 0 ? `${NODE_DRIVER_VERSION}|${version}` : NODE_DRIVER_VERSION
128+
name: 'nodejs',
129+
version: NODE_DRIVER_VERSION
123130
};
124131

125-
if (options.additionalDriverInfo == null) {
126-
throw new MongoRuntimeError(
127-
'Client options `additionalDriverInfo` must always default to an empty array'
128-
);
129-
}
130-
131132
// This is where we handle additional driver info added after client construction.
132133
for (const { name: n = '', version: v = '' } of options.additionalDriverInfo) {
133134
if (n.length > 0) {
@@ -145,13 +146,10 @@ export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMe
145146
}
146147

147148
let runtimeInfo = getRuntimeInfo();
148-
if (platform.length > 0) {
149-
runtimeInfo = `${runtimeInfo}|${platform}`;
150-
}
151-
152-
for (const { platform: p = '' } of options.additionalDriverInfo) {
153-
if (p.length > 0) {
154-
runtimeInfo = `${runtimeInfo}|${p}`;
149+
// This is where we handle additional driver info added after client construction.
150+
for (const { platform = '' } of options.additionalDriverInfo) {
151+
if (platform.length > 0) {
152+
runtimeInfo = `${runtimeInfo}|${platform}`;
155153
}
156154
}
157155

@@ -210,7 +208,9 @@ async function getContainerMetadata() {
210208
* Re-add each metadata value.
211209
* Attempt to add new env container metadata, but keep old data if it does not fit.
212210
*/
213-
export async function addContainerMetadata(originalMetadata: ClientMetadata) {
211+
export async function addContainerMetadata(
212+
originalMetadata: ClientMetadata
213+
): Promise<ClientMetadata> {
214214
const containerMetadata = await getContainerMetadata();
215215
if (Object.keys(containerMetadata).length === 0) return originalMetadata;
216216

@@ -233,7 +233,7 @@ export async function addContainerMetadata(originalMetadata: ClientMetadata) {
233233
extendedMetadata.ifItFitsItSits('env', extendedEnvMetadata);
234234
}
235235

236-
return extendedMetadata.toObject();
236+
return extendedMetadata.toObject() as ClientMetadata;
237237
}
238238

239239
/**

src/connection_string.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -535,16 +535,6 @@ export function parseOptions(
535535
}
536536
);
537537

538-
// Set the default for the additional driver info.
539-
mongoOptions.additionalDriverInfo = [];
540-
541-
mongoOptions.metadata = makeClientMetadata(mongoOptions);
542-
543-
mongoOptions.extendedMetadata = addContainerMetadata(mongoOptions.metadata).then(
544-
undefined,
545-
squashError
546-
); // rejections will be handled later
547-
548538
return mongoOptions;
549539
}
550540

src/mongo_client.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { Connection } from './cmap/connection';
1818
import {
1919
addContainerMetadata,
2020
type ClientMetadata,
21+
isDriverInfoEqual,
2122
makeClientMetadata
2223
} from './cmap/handshake/client_metadata';
2324
import type { CompressorName } from './cmap/wire_protocol/compression';
@@ -436,6 +437,9 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> implements
436437

437438
this.options = parseOptions(url, this, options);
438439

440+
this.options.additionalDriverInfo = [];
441+
this.appendMetadata(this.options.driverInfo);
442+
439443
const shouldSetLogger = Object.values(this.options.mongoLoggerOptions.componentSeverities).some(
440444
value => value !== SeverityLevel.OFF
441445
);
@@ -492,11 +496,11 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> implements
492496
* @param driverInfo - Information about the application or library.
493497
*/
494498
appendMetadata(driverInfo: DriverInfo) {
495-
for (const info of this.options.additionalDriverInfo) {
496-
if (info.name === driverInfo.name) {
497-
return;
498-
}
499-
}
499+
const isDuplicateDriverInfo = this.options.additionalDriverInfo.some(info =>
500+
isDriverInfoEqual(info, driverInfo)
501+
);
502+
if (isDuplicateDriverInfo) return;
503+
500504
this.options.additionalDriverInfo.push(driverInfo);
501505
this.options.metadata = makeClientMetadata(this.options);
502506
this.options.extendedMetadata = addContainerMetadata(this.options.metadata)

test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { expect } from 'chai';
22
import * as sinon from 'sinon';
33

4+
import { type ClientMetadata } from '../../../mongodb';
5+
import { MongoClient as RawMongoClient } from '../../../src';
46
import {
57
Connection,
68
getFAASEnv,
79
Int32,
10+
isDriverInfoEqual,
811
LEGACY_HELLO_COMMAND,
912
type MongoClient
1013
} from '../../mongodb';
@@ -365,4 +368,191 @@ describe('Client Metadata Update Prose Tests', function () {
365368
});
366369
}
367370
});
371+
372+
describe('Test 3: Multiple Successive Metadata Updates with Identical/Partially Identical `DriverInfo`', function () {
373+
const originalDriverInfo = { name: 'library', version: '1.2', platform: 'Library Platform' };
374+
let initialClientMetadata: ClientMetadata;
375+
let updatedClientMetadata: ClientMetadata;
376+
let client: RawMongoClient;
377+
378+
// | Case | Name | Version | Platform |
379+
// | ---- | --------- | ------- | ------------------ |
380+
// | 1 | library | 1.2 | Library Platform |
381+
// | 2 | framework | 1.2 | Library Platform |
382+
// | 3 | library | 2.0 | Library Platform |
383+
// | 4 | library | 1.2 | Framework Platform |
384+
// | 5 | framework | 2.0 | Library Platform |
385+
// | 6 | framework | 1.2 | Framework Platform |
386+
// | 7 | library | 2.0 | Framework Platform |
387+
// | 8 | framework | 2.0 | Framework Platform |
388+
const tests = [
389+
{ testCase: 1, name: 'library', version: '1.2', platform: 'Library Platform' },
390+
{ testCase: 2, name: 'framework', version: '1.2', platform: 'Library Platform' },
391+
{ testCase: 3, name: 'library', version: '2.0', platform: 'Library Platform' },
392+
{ testCase: 4, name: 'library', version: '1.2', platform: 'Framework Platform' },
393+
{ testCase: 5, name: 'framework', version: '2.0', platform: 'Library Platform' },
394+
{ testCase: 6, name: 'framework', version: '1.2', platform: 'Framework Platform' },
395+
{ testCase: 7, name: 'library', version: '2.0', platform: 'Framework Platform' },
396+
{ testCase: 8, name: 'framework', version: '2.0', platform: 'Framework Platform' }
397+
];
398+
399+
for (const { testCase, ...driverInfo } of tests) {
400+
context(`Case ${testCase}: ${JSON.stringify(driverInfo)}`, function () {
401+
// 1. Create a `MongoClient` instance with:
402+
// - `maxIdleTimeMS` set to `1ms`
403+
// 2. Append the following `DriverInfoOptions` to the `MongoClient` metadata:
404+
// | Field | Value |
405+
// | -------- | ---------------- |
406+
// | name | library |
407+
// | version | 1.2 |
408+
// | platform | Library Platform |
409+
// 3. Send a `ping` command to the server and verify that the command succeeds.
410+
// 4. Save intercepted `client` document as `updatedClientMetadata`.
411+
// 5. Wait 5ms for the connection to become idle.
412+
beforeEach(async function () {
413+
client = new RawMongoClient(this.configuration.url(), { maxIdleTimeMS: 1 });
414+
client.appendMetadata(originalDriverInfo);
415+
416+
sinon
417+
.stub(Connection.prototype, 'command')
418+
.callsFake(async function (ns, cmd, options, responseType) {
419+
// @ts-expect-error: sinon will place wrappedMethod on the command method.
420+
const command = Connection.prototype.command.wrappedMethod.bind(this);
421+
422+
if (cmd.hello || cmd[LEGACY_HELLO_COMMAND]) {
423+
if (!initialClientMetadata) {
424+
initialClientMetadata = cmd.client;
425+
} else {
426+
updatedClientMetadata = cmd.client;
427+
}
428+
}
429+
return command(ns, cmd, options, responseType);
430+
});
431+
432+
await client.db('test').command({ ping: 1 });
433+
await sleep(5);
434+
});
435+
436+
afterEach(async function () {
437+
await client.close();
438+
});
439+
440+
it('metadata is updated correctly, if necessary', async function () {
441+
// 1. Append the `DriverInfoOptions` from the selected test case to the `MongoClient` metadata.
442+
client.appendMetadata(driverInfo);
443+
444+
// 2. Send a `ping` command to the server and verify:
445+
// - The command succeeds.
446+
await client.db('test').command({ ping: 1 });
447+
448+
// - The framework metadata is appended to the existing `DriverInfoOptions` in the `client.driver` fields of the `hello`
449+
// command, with values separated by a pipe `|`. To simplify assertions in these tests, strip out the default driver info
450+
// that is automatically added by the driver (ex: `metadata.name.split('|').slice(1).join('|')`).
451+
452+
// - If the test case's DriverInfo is identical to the driver info from setup step 2 (test case 1):
453+
// - Assert metadata.name is equal to `library`
454+
// - Assert metadata.version is equal to `1.2`
455+
// - Assert metadata.platform is equal to `LibraryPlatform`
456+
// - Otherwise:
457+
// - Assert metadata.name is equal to `library|<name>`
458+
// - Assert metadata.version is equal to `1.2|<version>`
459+
// - Assert metadata.platform is equal to `LibraryPlatform|<platform>`
460+
const { driver, platform, ...updatedRest } = updatedClientMetadata;
461+
const { driver: _driver, platform: _platform, ...originalRest } = initialClientMetadata;
462+
463+
const extractParts = (s: string) => s.split('|').slice(1).join('|');
464+
465+
const actual = {
466+
name: extractParts(driver.name),
467+
version: extractParts(driver.version),
468+
platform: extractParts(platform)
469+
};
470+
471+
const expected = isDriverInfoEqual(driverInfo, originalDriverInfo)
472+
? originalDriverInfo
473+
: {
474+
name: `library|${driverInfo.name}`,
475+
platform: `Library Platform|${driverInfo.platform}`,
476+
version: `1.2|${driverInfo.version}`
477+
};
478+
479+
expect(actual).to.deep.equal(expected);
480+
481+
// All other subfields in the `client` document remain unchanged from `updatedClientMetadata`.
482+
expect(updatedRest).to.deep.equal(originalRest);
483+
});
484+
});
485+
}
486+
});
487+
488+
describe('Test 4: Metadata is not appended if identical to initial metadata', function () {
489+
let initialClientMetadata;
490+
let updatedClientMetadata;
491+
492+
// 1. Create a `MongoClient` instance with the following:
493+
// - `maxIdleTimeMS` set to `1ms`
494+
// - Wrapping library metadata:
495+
// | Field | Value |
496+
// | -------- | ---------------- |
497+
// | name | library |
498+
// | version | 1.2 |
499+
// | platform | Library Platform |
500+
// 2. Send a `ping` command to the server and verify that the command succeeds.
501+
// 3. Save intercepted `client` document as `initialClientMetadata`.
502+
// 4. Wait 5ms for the connection to become idle.
503+
beforeEach(async function () {
504+
// 1. Create a `MongoClient` instance with:
505+
// - `maxIdleTimeMS` set to `1ms`
506+
// - `driverInfo` set to the following:
507+
// | Field | Value |
508+
// | -------- | ---------------- |
509+
// | name | library |
510+
// | version | 1.2 |
511+
// | platform | Library Platform |
512+
client = new RawMongoClient(this.configuration.url(), {
513+
maxIdleTimeMS: 1,
514+
driverInfo: { name: 'library', version: '1.2', platform: 'Library Platform' }
515+
});
516+
517+
// 2. Send a `ping` command to the server and verify that the command succeeds.
518+
// 3. Save intercepted `client` document as `clientMetadata`.
519+
sinon
520+
.stub(Connection.prototype, 'command')
521+
.callsFake(async function (ns, cmd, options, responseType) {
522+
// @ts-expect-error: sinon will place wrappedMethod on the command method.
523+
const command = Connection.prototype.command.wrappedMethod.bind(this);
524+
525+
if (cmd.hello || cmd[LEGACY_HELLO_COMMAND]) {
526+
if (!initialClientMetadata) {
527+
initialClientMetadata = cmd.client;
528+
} else {
529+
updatedClientMetadata = cmd.client;
530+
}
531+
}
532+
return command(ns, cmd, options, responseType);
533+
});
534+
535+
await client.db('test').command({ ping: 1 });
536+
537+
// 4. Wait 5ms for the connection to become idle.
538+
await sleep(5);
539+
});
540+
541+
it('appends the metadata', async function () {
542+
// 5. Append the following `DriverInfoOptions` to the `MongoClient` metadata:
543+
// | Field | Value |
544+
// | -------- | ---------------- |
545+
// | name | library |
546+
// | version | 1.2 |
547+
// | platform | Library Platform |
548+
client.appendMetadata({ name: 'library', version: '1.2', platform: 'Library Platform' });
549+
550+
// 6. Send a `ping` command to the server and verify that the command succeeds.
551+
// 7. Save intercepted `client` document as `updatedClientMetadata`.
552+
await client.db('test').command({ ping: 1 });
553+
554+
// 8. Assert that `clientMetadata` is identical to `updatedClientMetadata`.
555+
expect(initialClientMetadata).to.deep.equal(updatedClientMetadata);
556+
});
557+
});
368558
});

0 commit comments

Comments
 (0)