Skip to content

Commit bf8c63e

Browse files
authored
fix(shell-api): align Number* type validation with legacy shell MONGOSH-1073 (#1177)
Extend our type validation helper to allow specifying individual BSON types, and use that to loosen the restrictions on the legacy `Number*()` constructors.
1 parent 0a19ca8 commit bf8c63e

File tree

3 files changed

+41
-15
lines changed

3 files changed

+41
-15
lines changed

packages/shell-api/src/helpers.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,16 @@ export function assertArgsDefinedType(args: any[], expectedTypes: Array<true|str
9494
return;
9595
}
9696

97-
if (((typeof expected === 'string' && typeof arg !== expected) || !expected.includes(typeof arg))) {
98-
const expectedMsg = typeof expected === 'string' ? expected : expected.filter(e => e !== undefined).join(' or ');
97+
const expectedTypesList: Array<string | undefined> =
98+
typeof expected === 'string' ? [expected] : expected;
99+
const isExpectedTypeof = expectedTypesList.includes(typeof arg);
100+
const isExpectedBson = expectedTypesList.includes(`bson:${arg?._bsontype}`);
101+
102+
if (!isExpectedTypeof && !isExpectedBson) {
103+
const expectedMsg = expectedTypesList
104+
.filter(e => e !== undefined)
105+
.map(e => e?.replace(/^bson:/, ''))
106+
.join(' or ');
99107
throw new MongoshInvalidInputError(
100108
`Argument at position ${i} must be of type ${expectedMsg}, got ${typeof arg} instead${getAssertCaller(func)}`,
101109
CommonErrors.InvalidArgument

packages/shell-api/src/shell-bson.spec.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ describe('Shell BSON', () => {
544544
try {
545545
(shellBson.NumberLong as any)({});
546546
} catch (e) {
547-
return expect(e.message).to.match(/string or number, got object.+\(NumberLong\)/);
547+
return expect(e.message).to.match(/string or number or Long or Int32, got object.+\(NumberLong\)/);
548548
}
549549
expect.fail('Expecting error, nothing thrown');
550550
});
@@ -576,7 +576,7 @@ describe('Shell BSON', () => {
576576
try {
577577
(shellBson.NumberDecimal as any)({});
578578
} catch (e) {
579-
return expect(e.message).to.match(/string or number, got object.+\(NumberDecimal\)/);
579+
return expect(e.message).to.match(/string or number or Long or Int32 or Decimal128, got object.+\(NumberDecimal\)/);
580580
}
581581
expect.fail('Expecting error, nothing thrown');
582582
});
@@ -609,12 +609,29 @@ describe('Shell BSON', () => {
609609
try {
610610
(shellBson.NumberInt as any)({});
611611
} catch (e) {
612-
return expect(e.message).to.match(/string or number, got object.+\(NumberInt\)/);
612+
return expect(e.message).to.match(/string or number or Long or Int32, got object.+\(NumberInt\)/);
613613
}
614614
expect.fail('Expecting error, nothing thrown');
615615
});
616616
});
617617

618+
describe('Number type cross-construction', () => {
619+
it('matches the legacy shell', () => {
620+
const { NumberInt, NumberLong, NumberDecimal } = shellBson as any;
621+
expect(NumberInt(null).toString()).to.equal('0');
622+
expect(NumberLong(null).toString()).to.equal('0');
623+
624+
expect(NumberInt(NumberInt(1234)).toString()).to.equal('1234');
625+
expect(NumberInt(NumberLong(1234)).toString()).to.equal('1234');
626+
expect(NumberInt(NumberLong(1234)).toString()).to.equal('1234');
627+
expect(NumberLong(NumberInt(1234)).toString()).to.equal('1234');
628+
expect(NumberLong(NumberLong(1234)).toString()).to.equal('1234');
629+
expect(NumberDecimal(NumberInt(1234)).toString()).to.equal('1234');
630+
expect(NumberDecimal(NumberLong(1234)).toString()).to.equal('1234');
631+
expect(NumberDecimal(NumberDecimal(1234)).toString()).to.equal('1234');
632+
});
633+
});
634+
618635
describe('EJSON', () => {
619636
it('serializes and de-serializes data', () => {
620637
const input = { a: new Date() };

packages/shell-api/src/shell-bson.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -121,24 +121,25 @@ export default function constructShellBson(bson: typeof BSON, printWarning: (msg
121121
return new bson.Code(c, s);
122122
}, { ...bson.Code, prototype: bson.Code.prototype }),
123123
NumberDecimal: Object.assign(function NumberDecimal(s = '0'): typeof bson.Decimal128.prototype {
124-
assertArgsDefinedType([s], [['string', 'number']], 'NumberDecimal');
125-
if (typeof s === 'string') {
126-
return bson.Decimal128.fromString(s);
124+
assertArgsDefinedType([s], [['string', 'number', 'bson:Long', 'bson:Int32', 'bson:Decimal128']], 'NumberDecimal');
125+
if (typeof s === 'number') {
126+
printWarning('NumberDecimal: specifying a number as argument is deprecated and may lead to loss of precision, pass a string instead');
127127
}
128-
printWarning('NumberDecimal: specifying a number as argument is deprecated and may lead to loss of precision, pass a string instead');
129128
return bson.Decimal128.fromString(`${s}`);
130129
}, { prototype: bson.Decimal128.prototype }),
131130
NumberInt: Object.assign(function NumberInt(v = '0'): typeof bson.Int32.prototype {
132-
assertArgsDefinedType([v], [['string', 'number']], 'NumberInt');
131+
v ??= '0';
132+
assertArgsDefinedType([v], [['string', 'number', 'bson:Long', 'bson:Int32']], 'NumberInt');
133133
return new bson.Int32(parseInt(`${v}`, 10));
134134
}, { prototype: bson.Int32.prototype }),
135135
NumberLong: Object.assign(function NumberLong(s: string | number = '0'): typeof bson.Long.prototype {
136-
assertArgsDefinedType([s], [['string', 'number']], 'NumberLong');
137-
if (typeof s === 'string') {
138-
return bson.Long.fromString(s);
136+
s ??= '0';
137+
assertArgsDefinedType([s], [['string', 'number', 'bson:Long', 'bson:Int32']], 'NumberLong');
138+
if (typeof s === 'number') {
139+
printWarning('NumberLong: specifying a number as argument is deprecated and may lead to loss of precision, pass a string instead');
140+
return bson.Long.fromNumber(s);
139141
}
140-
printWarning('NumberLong: specifying a number as argument is deprecated and may lead to loss of precision, pass a string instead');
141-
return bson.Long.fromNumber(s);
142+
return bson.Long.fromString(`${s}`);
142143
}, { prototype: bson.Long.prototype }),
143144
ISODate: function ISODate(input?: string): Date {
144145
if (!input) input = new Date().toISOString();

0 commit comments

Comments
 (0)