diff --git a/src/mongo_types.ts b/src/mongo_types.ts index ff1628157f..cf25f75d66 100644 --- a/src/mongo_types.ts +++ b/src/mongo_types.ts @@ -95,11 +95,11 @@ export interface FilterOperators extends Document { $eq?: TValue; $gt?: TValue; $gte?: TValue; - $in?: TValue[]; + $in?: ReadonlyArray; $lt?: TValue; $lte?: TValue; $ne?: TValue; - $nin?: TValue[]; + $nin?: ReadonlyArray; // Logical $not?: TValue extends string ? FilterOperators | RegExp : FilterOperators; // Element @@ -122,8 +122,8 @@ export interface FilterOperators extends Document { $nearSphere?: Document; $maxDistance?: number; // Array - $all?: TValue extends ReadonlyArray ? any[] : never; - $elemMatch?: TValue extends ReadonlyArray ? Document : never; + $all?: ReadonlyArray; + $elemMatch?: Document; $size?: TValue extends ReadonlyArray ? number : never; // Bitwise $bitsAllClear?: BitwiseFilter; @@ -137,7 +137,7 @@ export interface FilterOperators extends Document { export type BitwiseFilter = | number /** numeric bit mask */ | Binary /** BinData bit mask */ - | number[]; /** `[ , , ... ]` */ + | ReadonlyArray; /** `[ , , ... ]` */ /** @public */ export const BSONType = Object.freeze({ @@ -286,7 +286,7 @@ export type PullAllOperator = ({ readonly [key in KeysOfAType>]?: TSchema[key]; } & NotAcceptedFields>) & { - readonly [key: string]: any[]; + readonly [key: string]: ReadonlyArray; }; /** @public */ @@ -320,7 +320,7 @@ export type UpdateFilter = { export type Nullable = AnyType | null | undefined; /** @public */ -export type OneOrMore = T | T[]; +export type OneOrMore = T | ReadonlyArray; /** @public */ export type GenericListener = (...args: any[]) => void; diff --git a/src/utils.ts b/src/utils.ts index 1af2ac2f00..26600970ee 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -156,9 +156,9 @@ export function parseIndexOptions(indexSpec: IndexSpecification): IndexOptions { } else if (isObject(indexSpec)) { // {location:'2d', type:1} keys = Object.keys(indexSpec); - keys.forEach(key => { - indexes.push(key + '_' + indexSpec[key]); - fieldHash[key] = indexSpec[key]; + Object.entries(indexSpec).forEach(([key, value]) => { + indexes.push(key + '_' + value); + fieldHash[key] = value; }); } diff --git a/test/types/community/collection/findX.test-d.ts b/test/types/community/collection/findX.test-d.ts index 5a4f162030..bc17e3179e 100644 --- a/test/types/community/collection/findX.test-d.ts +++ b/test/types/community/collection/findX.test-d.ts @@ -135,3 +135,58 @@ expectNotType>({ printCar(await car.findOne({}, options)); printCar(await car.findOne({}, optionsWithProjection)); + +// Readonly tests -- NODE-3452 +const colorCollection = client.db('test_db').collection<{ color: string }>('test_collection'); +const colorsFreeze: ReadonlyArray = Object.freeze(['blue', 'red']); +const colorsWritable: Array = ['blue', 'red']; + +// Permitted Readonly fields +expectType>(colorCollection.find({ color: { $in: colorsFreeze } })); +expectType>(colorCollection.find({ color: { $in: colorsWritable } })); +expectType>(colorCollection.find({ color: { $nin: colorsFreeze } })); +expectType>( + colorCollection.find({ color: { $nin: colorsWritable } }) +); +// $all and $elemMatch works against single fields (it's just redundant) +expectType>(colorCollection.find({ color: { $all: colorsFreeze } })); +expectType>( + colorCollection.find({ color: { $all: colorsWritable } }) +); +expectType>( + colorCollection.find({ color: { $elemMatch: colorsFreeze } }) +); +expectType>( + colorCollection.find({ color: { $elemMatch: colorsWritable } }) +); + +const countCollection = client.db('test_db').collection<{ count: number }>('test_collection'); +expectType>( + countCollection.find({ count: { $bitsAnySet: Object.freeze([1, 0, 1]) } }) +); +expectType>( + countCollection.find({ count: { $bitsAnySet: [1, 0, 1] as number[] } }) +); + +const listsCollection = client.db('test_db').collection<{ lists: string[] }>('test_collection'); +await listsCollection.updateOne({}, { list: { $pullAll: Object.freeze(['one', 'two']) } }); +expectType>(listsCollection.find({ lists: { $size: 1 } })); + +const rdOnlyListsCollection = client + .db('test_db') + .collection<{ lists: ReadonlyArray }>('test_collection'); +expectType }>>( + rdOnlyListsCollection.find({ lists: { $size: 1 } }) +); + +// Before NODE-3452's fix we would get this strange result that included the filter shape joined with the actual schema +expectNotType } }>>( + colorCollection.find({ color: { $in: colorsFreeze } }) +); + +// This is related to another bug that will be fixed in NODE-3454 +expectType>(colorCollection.find({ color: { $in: 3 } })); + +// When you use the override, $in doesn't permit readonly +colorCollection.find<{ color: string }>({ color: { $in: colorsFreeze } }); +colorCollection.find<{ color: string }>({ color: { $in: ['regularArray'] } }); diff --git a/test/types/community/collection/insertX.test-d.ts b/test/types/community/collection/insertX.test-d.ts index 8d51559fde..c2cb43ba4c 100644 --- a/test/types/community/collection/insertX.test-d.ts +++ b/test/types/community/collection/insertX.test-d.ts @@ -1,4 +1,4 @@ -import { expectError, expectNotType, expectType } from 'tsd'; +import { expectError, expectNotAssignable, expectNotType, expectType } from 'tsd'; import { MongoClient, ObjectId, OptionalId } from '../../../../src'; import type { PropExists } from '../../utility_types'; @@ -223,3 +223,17 @@ expectType>(false); expectType(indexTypeResult2.insertedId); expectType<{ [key: number]: number }>(indexTypeResultMany2.insertedIds); + +// Readonly Tests -- NODE-3452 +const colorsColl = client.db('test').collection<{ colors: string[] }>('writableColors'); +const colorsFreeze: ReadonlyArray = Object.freeze(['blue', 'red']); +// Users must define their properties as readonly if they want to be able to insert readonly +type InsertOneParam = Parameters[0]; +expectNotAssignable({ colors: colorsFreeze }); +// Correct usage: +const rdOnlyColl = client + .db('test') + .collection<{ colors: ReadonlyArray }>('readonlyColors'); +rdOnlyColl.insertOne({ colors: colorsFreeze }); +const colorsWritable = ['a', 'b']; +rdOnlyColl.insertOne({ colors: colorsWritable }); diff --git a/test/types/helper_types.test-d.ts b/test/types/helper_types.test-d.ts index c71d3c7af2..33253a4e5b 100644 --- a/test/types/helper_types.test-d.ts +++ b/test/types/helper_types.test-d.ts @@ -8,7 +8,8 @@ import type { FilterOperations, OnlyFieldsOfType, IntegerType, - IsAny + IsAny, + OneOrMore } from '../../src/mongo_types'; import { Decimal128, Double, Int32, Long, Document } from '../../src/index'; @@ -97,3 +98,8 @@ interface IndexedSchema { // This means we can't properly enforce the subtype and there doesn't seem to be a way to detect it // and reduce strictness like we can with any, users with indexed schemas will have to use `as any` expectNotAssignable>({ a: 2 }); + +// OneOrMore should accept readonly arrays +expectAssignable>(1); +expectAssignable>([1, 2]); +expectAssignable>(Object.freeze([1, 2]));