Skip to content

Commit 4b2e3d1

Browse files
fix(NODE-4031): options parsing for array options (#3193)
1 parent 4e6dccd commit 4b2e3d1

File tree

2 files changed

+165
-32
lines changed

2 files changed

+165
-32
lines changed

src/connection_string.ts

+25-14
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
ServerApi,
2020
ServerApiVersion
2121
} from './mongo_client';
22-
import type { OneOrMore } from './mongo_types';
2322
import { PromiseProvider } from './promise_provider';
2423
import { ReadConcern, ReadConcernLevel } from './read_concern';
2524
import { ReadPreference, ReadPreferenceMode } from './read_preference';
@@ -220,12 +219,7 @@ function getUint(name: string, value: unknown): number {
220219
return parsedValue;
221220
}
222221

223-
/** Wrap a single value in an array if the value is not an array */
224-
function toArray<T>(value: OneOrMore<T>): T[] {
225-
return Array.isArray(value) ? value : [value];
226-
}
227-
228-
function* entriesFromString(value: string) {
222+
function* entriesFromString(value: string): Generator<[string, string]> {
229223
const keyValuePairs = value.split(',');
230224
for (const keyValue of keyValuePairs) {
231225
const [key, value] = keyValue.split(':');
@@ -333,11 +327,19 @@ export function parseOptions(
333327
]);
334328

335329
for (const key of allKeys) {
336-
const values = [objectOptions, urlOptions, DEFAULT_OPTIONS].flatMap(optionsObject => {
337-
const options = optionsObject.get(key) ?? [];
338-
return toArray(options);
339-
});
340-
330+
const values = [];
331+
const objectOptionValue = objectOptions.get(key);
332+
if (objectOptionValue != null) {
333+
values.push(objectOptionValue);
334+
}
335+
const urlValue = urlOptions.get(key);
336+
if (urlValue != null) {
337+
values.push(...urlValue);
338+
}
339+
const defaultOptionsValue = DEFAULT_OPTIONS.get(key);
340+
if (defaultOptionsValue != null) {
341+
values.push(defaultOptionsValue);
342+
}
341343
allOptions.set(key, values);
342344
}
343345

@@ -982,9 +984,18 @@ export const OPTIONS = {
982984
},
983985
readPreferenceTags: {
984986
target: 'readPreference',
985-
transform({ values, options }) {
987+
transform({
988+
values,
989+
options
990+
}: {
991+
values: Array<string | Record<string, string>[]>;
992+
options: MongoClientOptions;
993+
}) {
994+
const tags: Array<string | Record<string, string>> = Array.isArray(values[0])
995+
? values[0]
996+
: (values as Array<string>);
986997
const readPreferenceTags = [];
987-
for (const tag of values) {
998+
for (const tag of tags) {
988999
const readPreferenceTag: TagSet = Object.create(null);
9891000
if (typeof tag === 'string') {
9901001
for (const [k, v] of entriesFromString(tag)) {

test/unit/connection_string.test.ts

+140-18
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,138 @@ describe('Connection String', function () {
4646
expect(options.hosts[0].port).to.equal(27017);
4747
});
4848

49-
context('readPreferenceTags', function () {
50-
it('should parse multiple readPreferenceTags when passed in the uri', () => {
51-
const options = parseOptions(
52-
'mongodb://hostname?readPreferenceTags=bar:foo&readPreferenceTags=baz:bar'
53-
);
54-
expect(options.readPreference.tags).to.deep.equal([{ bar: 'foo' }, { baz: 'bar' }]);
49+
describe('ca option', () => {
50+
context('when set in the options object', () => {
51+
it('should parse a string', () => {
52+
const options = parseOptions('mongodb://localhost', {
53+
ca: 'hello'
54+
});
55+
expect(options).to.have.property('ca').to.equal('hello');
56+
});
57+
58+
it('should parse a NodeJS buffer', () => {
59+
const options = parseOptions('mongodb://localhost', {
60+
ca: Buffer.from([1, 2, 3, 4])
61+
});
62+
63+
expect(options)
64+
.to.have.property('ca')
65+
.to.deep.equal(Buffer.from([1, 2, 3, 4]));
66+
});
67+
68+
it('should parse arrays with a single element', () => {
69+
const options = parseOptions('mongodb://localhost', {
70+
ca: ['hello']
71+
});
72+
expect(options).to.have.property('ca').to.deep.equal(['hello']);
73+
});
74+
75+
it('should parse an empty array', () => {
76+
const options = parseOptions('mongodb://localhost', {
77+
ca: []
78+
});
79+
expect(options).to.have.property('ca').to.deep.equal([]);
80+
});
81+
82+
it('should parse arrays with multiple elements', () => {
83+
const options = parseOptions('mongodb://localhost', {
84+
ca: ['hello', 'world']
85+
});
86+
expect(options).to.have.property('ca').to.deep.equal(['hello', 'world']);
87+
});
88+
});
89+
90+
// TODO(NODE-4172): align uri behavior with object options behavior
91+
context('when set in the uri', () => {
92+
it('should parse a string value', () => {
93+
const options = parseOptions('mongodb://localhost?ca=hello', {});
94+
expect(options).to.have.property('ca').to.equal('hello');
95+
});
96+
97+
it('should throw an error with a buffer value', () => {
98+
const buffer = Buffer.from([1, 2, 3, 4]);
99+
expect(() => {
100+
parseOptions(`mongodb://localhost?ca=${buffer.toString()}`, {});
101+
}).to.throw(MongoAPIError);
102+
});
103+
104+
it('should not parse multiple string values (array of options)', () => {
105+
const options = parseOptions('mongodb://localhost?ca=hello,world', {});
106+
expect(options).to.have.property('ca').to.equal('hello,world');
107+
});
108+
});
109+
110+
it('should prioritize options set in the object over those set in the URI', () => {
111+
const options = parseOptions('mongodb://localhost?ca=hello', {
112+
ca: ['world']
113+
});
114+
expect(options).to.have.property('ca').to.deep.equal(['world']);
55115
});
116+
});
56117

57-
it('should parse multiple readPreferenceTags when passed in options object', () => {
58-
const options = parseOptions('mongodb://hostname?', {
118+
describe('readPreferenceTags option', function () {
119+
context('when the option is passed in the uri', () => {
120+
it('should throw an error if no value is passed for readPreferenceTags', () => {
121+
expect(() => parseOptions('mongodb://hostname?readPreferenceTags=')).to.throw(
122+
MongoAPIError
123+
);
124+
});
125+
it('should parse a single read preference tag', () => {
126+
const options = parseOptions('mongodb://hostname?readPreferenceTags=bar:foo');
127+
expect(options.readPreference.tags).to.deep.equal([{ bar: 'foo' }]);
128+
});
129+
it('should parse multiple readPreferenceTags', () => {
130+
const options = parseOptions(
131+
'mongodb://hostname?readPreferenceTags=bar:foo&readPreferenceTags=baz:bar'
132+
);
133+
expect(options.readPreference.tags).to.deep.equal([{ bar: 'foo' }, { baz: 'bar' }]);
134+
});
135+
it('should parse multiple readPreferenceTags for the same key', () => {
136+
const options = parseOptions(
137+
'mongodb://hostname?readPreferenceTags=bar:foo&readPreferenceTags=bar:banana&readPreferenceTags=baz:bar'
138+
);
139+
expect(options.readPreference.tags).to.deep.equal([
140+
{ bar: 'foo' },
141+
{ bar: 'banana' },
142+
{ baz: 'bar' }
143+
]);
144+
});
145+
});
146+
147+
context('when the option is passed in the options object', () => {
148+
it('should not parse an empty readPreferenceTags object', () => {
149+
const options = parseOptions('mongodb://hostname?', {
150+
readPreferenceTags: []
151+
});
152+
expect(options.readPreference.tags).to.deep.equal([]);
153+
});
154+
it('should parse a single readPreferenceTags object', () => {
155+
const options = parseOptions('mongodb://hostname?', {
156+
readPreferenceTags: [{ bar: 'foo' }]
157+
});
158+
expect(options.readPreference.tags).to.deep.equal([{ bar: 'foo' }]);
159+
});
160+
it('should parse multiple readPreferenceTags', () => {
161+
const options = parseOptions('mongodb://hostname?', {
162+
readPreferenceTags: [{ bar: 'foo' }, { baz: 'bar' }]
163+
});
164+
expect(options.readPreference.tags).to.deep.equal([{ bar: 'foo' }, { baz: 'bar' }]);
165+
});
166+
167+
it('should parse multiple readPreferenceTags for the same key', () => {
168+
const options = parseOptions('mongodb://hostname?', {
169+
readPreferenceTags: [{ bar: 'foo' }, { bar: 'banana' }, { baz: 'bar' }]
170+
});
171+
expect(options.readPreference.tags).to.deep.equal([
172+
{ bar: 'foo' },
173+
{ bar: 'banana' },
174+
{ baz: 'bar' }
175+
]);
176+
});
177+
});
178+
179+
it('should prioritize options from the options object over the uri options', () => {
180+
const options = parseOptions('mongodb://hostname?readPreferenceTags=a:b', {
59181
readPreferenceTags: [{ bar: 'foo' }, { baz: 'bar' }]
60182
});
61183
expect(options.readPreference.tags).to.deep.equal([{ bar: 'foo' }, { baz: 'bar' }]);
@@ -174,25 +296,25 @@ describe('Connection String', function () {
174296
context('when the options are provided in the URI', function () {
175297
context('when the options are equal', function () {
176298
context('when both options are true', function () {
177-
const options = parseOptions('mongodb://localhost/?tls=true&ssl=true');
178-
179299
it('sets the tls option', function () {
300+
const options = parseOptions('mongodb://localhost/?tls=true&ssl=true');
180301
expect(options.tls).to.be.true;
181302
});
182303

183304
it('does not set the ssl option', function () {
305+
const options = parseOptions('mongodb://localhost/?tls=true&ssl=true');
184306
expect(options).to.not.have.property('ssl');
185307
});
186308
});
187309

188310
context('when both options are false', function () {
189-
const options = parseOptions('mongodb://localhost/?tls=false&ssl=false');
190-
191311
it('sets the tls option', function () {
312+
const options = parseOptions('mongodb://localhost/?tls=false&ssl=false');
192313
expect(options.tls).to.be.false;
193314
});
194315

195316
it('does not set the ssl option', function () {
317+
const options = parseOptions('mongodb://localhost/?tls=false&ssl=false');
196318
expect(options).to.not.have.property('ssl');
197319
});
198320
});
@@ -210,38 +332,38 @@ describe('Connection String', function () {
210332
context('when the options are provided in the options', function () {
211333
context('when the options are equal', function () {
212334
context('when both options are true', function () {
213-
const options = parseOptions('mongodb://localhost/', { tls: true, ssl: true });
214-
215335
it('sets the tls option', function () {
336+
const options = parseOptions('mongodb://localhost/', { tls: true, ssl: true });
216337
expect(options.tls).to.be.true;
217338
});
218339

219340
it('does not set the ssl option', function () {
341+
const options = parseOptions('mongodb://localhost/', { tls: true, ssl: true });
220342
expect(options).to.not.have.property('ssl');
221343
});
222344
});
223345

224346
context('when both options are false', function () {
225347
context('when the URI is an SRV URI', function () {
226-
const options = parseOptions('mongodb+srv://localhost/', { tls: false, ssl: false });
227-
228348
it('overrides the tls option', function () {
349+
const options = parseOptions('mongodb+srv://localhost/', { tls: false, ssl: false });
229350
expect(options.tls).to.be.false;
230351
});
231352

232353
it('does not set the ssl option', function () {
354+
const options = parseOptions('mongodb+srv://localhost/', { tls: false, ssl: false });
233355
expect(options).to.not.have.property('ssl');
234356
});
235357
});
236358

237359
context('when the URI is not SRV', function () {
238-
const options = parseOptions('mongodb://localhost/', { tls: false, ssl: false });
239-
240360
it('sets the tls option', function () {
361+
const options = parseOptions('mongodb://localhost/', { tls: false, ssl: false });
241362
expect(options.tls).to.be.false;
242363
});
243364

244365
it('does not set the ssl option', function () {
366+
const options = parseOptions('mongodb://localhost/', { tls: false, ssl: false });
245367
expect(options).to.not.have.property('ssl');
246368
});
247369
});

0 commit comments

Comments
 (0)