Skip to content

Commit ce51e78

Browse files
authored
fix(NODE-3452): readonly filters not permitted by typings (#2927)
1 parent c554a7a commit ce51e78

File tree

5 files changed

+87
-12
lines changed

5 files changed

+87
-12
lines changed

src/mongo_types.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,11 @@ export interface FilterOperators<TValue> extends Document {
9595
$eq?: TValue;
9696
$gt?: TValue;
9797
$gte?: TValue;
98-
$in?: TValue[];
98+
$in?: ReadonlyArray<TValue>;
9999
$lt?: TValue;
100100
$lte?: TValue;
101101
$ne?: TValue;
102-
$nin?: TValue[];
102+
$nin?: ReadonlyArray<TValue>;
103103
// Logical
104104
$not?: TValue extends string ? FilterOperators<TValue> | RegExp : FilterOperators<TValue>;
105105
// Element
@@ -122,8 +122,8 @@ export interface FilterOperators<TValue> extends Document {
122122
$nearSphere?: Document;
123123
$maxDistance?: number;
124124
// Array
125-
$all?: TValue extends ReadonlyArray<any> ? any[] : never;
126-
$elemMatch?: TValue extends ReadonlyArray<any> ? Document : never;
125+
$all?: ReadonlyArray<any>;
126+
$elemMatch?: Document;
127127
$size?: TValue extends ReadonlyArray<any> ? number : never;
128128
// Bitwise
129129
$bitsAllClear?: BitwiseFilter;
@@ -137,7 +137,7 @@ export interface FilterOperators<TValue> extends Document {
137137
export type BitwiseFilter =
138138
| number /** numeric bit mask */
139139
| Binary /** BinData bit mask */
140-
| number[]; /** `[ <position1>, <position2>, ... ]` */
140+
| ReadonlyArray<number>; /** `[ <position1>, <position2>, ... ]` */
141141

142142
/** @public */
143143
export const BSONType = Object.freeze({
@@ -286,7 +286,7 @@ export type PullAllOperator<TSchema> = ({
286286
readonly [key in KeysOfAType<TSchema, ReadonlyArray<any>>]?: TSchema[key];
287287
} &
288288
NotAcceptedFields<TSchema, ReadonlyArray<any>>) & {
289-
readonly [key: string]: any[];
289+
readonly [key: string]: ReadonlyArray<any>;
290290
};
291291

292292
/** @public */
@@ -320,7 +320,7 @@ export type UpdateFilter<TSchema> = {
320320
export type Nullable<AnyType> = AnyType | null | undefined;
321321

322322
/** @public */
323-
export type OneOrMore<T> = T | T[];
323+
export type OneOrMore<T> = T | ReadonlyArray<T>;
324324

325325
/** @public */
326326
export type GenericListener = (...args: any[]) => void;

src/utils.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,9 @@ export function parseIndexOptions(indexSpec: IndexSpecification): IndexOptions {
158158
} else if (isObject(indexSpec)) {
159159
// {location:'2d', type:1}
160160
keys = Object.keys(indexSpec);
161-
keys.forEach(key => {
162-
indexes.push(key + '_' + indexSpec[key]);
163-
fieldHash[key] = indexSpec[key];
161+
Object.entries(indexSpec).forEach(([key, value]) => {
162+
indexes.push(key + '_' + value);
163+
fieldHash[key] = value;
164164
});
165165
}
166166

test/types/community/collection/findX.test-d.ts

+55
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,58 @@ expectNotType<FindOptions<Car>>({
135135

136136
printCar(await car.findOne({}, options));
137137
printCar(await car.findOne({}, optionsWithProjection));
138+
139+
// Readonly tests -- NODE-3452
140+
const colorCollection = client.db('test_db').collection<{ color: string }>('test_collection');
141+
const colorsFreeze: ReadonlyArray<string> = Object.freeze(['blue', 'red']);
142+
const colorsWritable: Array<string> = ['blue', 'red'];
143+
144+
// Permitted Readonly fields
145+
expectType<FindCursor<{ color: string }>>(colorCollection.find({ color: { $in: colorsFreeze } }));
146+
expectType<FindCursor<{ color: string }>>(colorCollection.find({ color: { $in: colorsWritable } }));
147+
expectType<FindCursor<{ color: string }>>(colorCollection.find({ color: { $nin: colorsFreeze } }));
148+
expectType<FindCursor<{ color: string }>>(
149+
colorCollection.find({ color: { $nin: colorsWritable } })
150+
);
151+
// $all and $elemMatch works against single fields (it's just redundant)
152+
expectType<FindCursor<{ color: string }>>(colorCollection.find({ color: { $all: colorsFreeze } }));
153+
expectType<FindCursor<{ color: string }>>(
154+
colorCollection.find({ color: { $all: colorsWritable } })
155+
);
156+
expectType<FindCursor<{ color: string }>>(
157+
colorCollection.find({ color: { $elemMatch: colorsFreeze } })
158+
);
159+
expectType<FindCursor<{ color: string }>>(
160+
colorCollection.find({ color: { $elemMatch: colorsWritable } })
161+
);
162+
163+
const countCollection = client.db('test_db').collection<{ count: number }>('test_collection');
164+
expectType<FindCursor<{ count: number }>>(
165+
countCollection.find({ count: { $bitsAnySet: Object.freeze([1, 0, 1]) } })
166+
);
167+
expectType<FindCursor<{ count: number }>>(
168+
countCollection.find({ count: { $bitsAnySet: [1, 0, 1] as number[] } })
169+
);
170+
171+
const listsCollection = client.db('test_db').collection<{ lists: string[] }>('test_collection');
172+
await listsCollection.updateOne({}, { list: { $pullAll: Object.freeze(['one', 'two']) } });
173+
expectType<FindCursor<{ lists: string[] }>>(listsCollection.find({ lists: { $size: 1 } }));
174+
175+
const rdOnlyListsCollection = client
176+
.db('test_db')
177+
.collection<{ lists: ReadonlyArray<string> }>('test_collection');
178+
expectType<FindCursor<{ lists: ReadonlyArray<string> }>>(
179+
rdOnlyListsCollection.find({ lists: { $size: 1 } })
180+
);
181+
182+
// Before NODE-3452's fix we would get this strange result that included the filter shape joined with the actual schema
183+
expectNotType<FindCursor<{ color: string | { $in: ReadonlyArray<string> } }>>(
184+
colorCollection.find({ color: { $in: colorsFreeze } })
185+
);
186+
187+
// This is related to another bug that will be fixed in NODE-3454
188+
expectType<FindCursor<{ color: { $in: number } }>>(colorCollection.find({ color: { $in: 3 } }));
189+
190+
// When you use the override, $in doesn't permit readonly
191+
colorCollection.find<{ color: string }>({ color: { $in: colorsFreeze } });
192+
colorCollection.find<{ color: string }>({ color: { $in: ['regularArray'] } });

test/types/community/collection/insertX.test-d.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expectError, expectNotType, expectType } from 'tsd';
1+
import { expectError, expectNotAssignable, expectNotType, expectType } from 'tsd';
22
import { MongoClient, ObjectId, OptionalId } from '../../../../src';
33
import type { PropExists } from '../../utility_types';
44

@@ -223,3 +223,17 @@ expectType<PropExists<typeof indexTypeResultMany2, 'ops'>>(false);
223223

224224
expectType<number>(indexTypeResult2.insertedId);
225225
expectType<{ [key: number]: number }>(indexTypeResultMany2.insertedIds);
226+
227+
// Readonly Tests -- NODE-3452
228+
const colorsColl = client.db('test').collection<{ colors: string[] }>('writableColors');
229+
const colorsFreeze: ReadonlyArray<string> = Object.freeze(['blue', 'red']);
230+
// Users must define their properties as readonly if they want to be able to insert readonly
231+
type InsertOneParam = Parameters<typeof colorsColl.insertOne>[0];
232+
expectNotAssignable<InsertOneParam>({ colors: colorsFreeze });
233+
// Correct usage:
234+
const rdOnlyColl = client
235+
.db('test')
236+
.collection<{ colors: ReadonlyArray<string> }>('readonlyColors');
237+
rdOnlyColl.insertOne({ colors: colorsFreeze });
238+
const colorsWritable = ['a', 'b'];
239+
rdOnlyColl.insertOne({ colors: colorsWritable });

test/types/helper_types.test-d.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import type {
88
FilterOperations,
99
OnlyFieldsOfType,
1010
IntegerType,
11-
IsAny
11+
IsAny,
12+
OneOrMore
1213
} from '../../src/mongo_types';
1314
import { Decimal128, Double, Int32, Long, Document } from '../../src/index';
1415

@@ -97,3 +98,8 @@ interface IndexedSchema {
9798
// This means we can't properly enforce the subtype and there doesn't seem to be a way to detect it
9899
// and reduce strictness like we can with any, users with indexed schemas will have to use `as any`
99100
expectNotAssignable<OnlyFieldsOfType<IndexedSchema, NumericType>>({ a: 2 });
101+
102+
// OneOrMore should accept readonly arrays
103+
expectAssignable<OneOrMore<number>>(1);
104+
expectAssignable<OneOrMore<number>>([1, 2]);
105+
expectAssignable<OneOrMore<number>>(Object.freeze([1, 2]));

0 commit comments

Comments
 (0)