diff --git a/src/index.ts b/src/index.ts index eb7cc75f971..76f9a599716 100644 --- a/src/index.ts +++ b/src/index.ts @@ -284,6 +284,7 @@ export type { InferIdType, IntegerType, IsAny, + IsInUnion, Join, KeysOfAType, KeysOfOtherType, @@ -306,6 +307,7 @@ export type { RootFilterOperators, SchemaMember, SetFields, + TypeEquals, UpdateFilter, WithId, WithoutId diff --git a/src/mongo_types.ts b/src/mongo_types.ts index 83bf11ad6a5..df52267e98b 100644 --- a/src/mongo_types.ts +++ b/src/mongo_types.ts @@ -66,7 +66,9 @@ export type WithoutId = Omit; /** A MongoDB filter can be some portion of the schema or a set of operators @public */ export type Filter = - | Partial + | Partial<{ + [Property in keyof TSchema]: Condition; + }> | ({ [Property in Join>, '.'>]?: Condition< PropertyType, Property> @@ -89,8 +91,23 @@ export type AlternativeType = T extends ReadonlyArray /** @public */ export type RegExpOrString = T extends string ? BSONRegExp | RegExp | T : T; +/** + * This is a type that allows any `$` prefixed keys and makes no assumptions + * about their value types. This stems from a design decision that newly added + * filter operators should be accepted without needing to upgrade this package. + * + * This has the unfortunate side effect of preventing type errors on unknown + * operator keys, so we should prefer not to extend this type whenever possible. + * + * @see https://github.com/mongodb/node-mongodb-native/pull/3115#issuecomment-1021303302 + * @public + */ +export type OpenOperatorQuery = { + [key: `$${string}`]: unknown; +}; + /** @public */ -export interface RootFilterOperators extends Document { +export interface RootFilterOperators extends OpenOperatorQuery { $and?: Filter[]; $nor?: Filter[]; $or?: Filter[]; @@ -479,6 +496,24 @@ export type PropertyType = string extends Propert : unknown : unknown; +/** + * Check if two types are exactly equal + * + * From https://github.com/microsoft/TypeScript/issues/27024#issuecomment-421529650, + * credit to https://github.com/mattmccutchen. + * @public + */ +// prettier-ignore +export type TypeEquals = + (() => T extends X ? 1 : 2) extends + (() => T extends Y ? 1 : 2) ? true : false + +/** + * Check if A is a union type that includes B + * @public + */ +export type IsInUnion = TypeEquals, B>; + /** * @public * returns tuple of strings (keys to be joined on '.') that represent every path into a schema @@ -504,9 +539,7 @@ export type NestedPaths = Type extends ? { [Key in Extract]: Type[Key] extends Type // type of value extends the parent ? [Key] - : // for a recursive union type, the child will never extend the parent type. - // but the parent will still extend the child - Type extends Type[Key] + : IsInUnion extends true ? [Key] : Type[Key] extends ReadonlyArray // handling recursive types with arrays ? Type extends ArrayType // is the type of the parent the same as the type of the array? diff --git a/test/types/community/collection/filterQuery.test-d.ts b/test/types/community/collection/filterQuery.test-d.ts index 53c83f586fd..9adbe07977d 100644 --- a/test/types/community/collection/filterQuery.test-d.ts +++ b/test/types/community/collection/filterQuery.test-d.ts @@ -126,6 +126,11 @@ expectType[]>( expectType[]>( await collectionT.find({ name: new BSONRegExp('MrMeow', 'i') }).toArray() ); + +/// it should not accept fields that are not in the schema +type FT = Filter; +expectNotType({ missing: true }); + /// it should not accept wrong types for string fields expectNotType>({ name: 23 }); expectNotType>({ name: { suffix: 'Jr' } }); @@ -235,7 +240,7 @@ await collectionT.find({ name: { $eq: /Spot/ } }).toArray(); await collectionT.find({ type: { $eq: 'dog' } }).toArray(); await collectionT.find({ age: { $gt: 12, $lt: 13 } }).toArray(); await collectionT.find({ treats: { $eq: 'kibble' } }).toArray(); -await collectionT.find({ scores: { $gte: 23 } }).toArray(); +await collectionT.find({ age: { $gte: 23 } }).toArray(); await collectionT.find({ createdAt: { $lte: new Date() } }).toArray(); await collectionT.find({ friends: { $ne: spot } }).toArray(); /// it should not accept wrong queries @@ -283,6 +288,9 @@ expectNotType>({ name: { $or: ['Spot', 'Bubbles'] } }); /// it should not accept single objects for __$and, $or, $nor operator__ query expectNotType>({ $and: { name: 'Spot' } }); +/// it allows using unknown root operators with any value +expectAssignable>({ $fakeOp: 123 }); + /** * test 'element' query operators */ @@ -361,9 +369,11 @@ expectError( otherField: new ObjectId() }) ); -nonObjectIdCollection.find({ - fieldThatDoesNotExistOnSchema: new ObjectId() -}); +expectError( + nonObjectIdCollection.find({ + fieldThatDoesNotExistOnSchema: new ObjectId() + }) +); // we only forbid objects that "look like" object ids, so other random objects are permitted nonObjectIdCollection.find({ diff --git a/test/types/community/collection/findX-recursive-types.test-d.ts b/test/types/community/collection/findX-recursive-types.test-d.ts index 96c4e095cce..86fc54127e8 100644 --- a/test/types/community/collection/findX-recursive-types.test-d.ts +++ b/test/types/community/collection/findX-recursive-types.test-d.ts @@ -87,6 +87,7 @@ recursiveOptionalCollection.find({ * recursive union types are supported */ interface Node { + value?: string; next: Node | null; } @@ -103,10 +104,12 @@ expectError( ); nodeCollection.find({ - 'next.next': 'asdf' + 'next.value': 'asdf' }); -nodeCollection.find({ 'next.next.next': 'yoohoo' }); +// type safety is lost through recursive relations; callers will +// need to annotate queries with `ts-expect-error` comments +expectError(nodeCollection.find({ 'next.next.value': 'yoohoo' })); /** * Recursive schemas with arrays are also supported @@ -149,9 +152,11 @@ expectError( // type safety breaks after the first // level of nested types -recursiveSchemaWithArray.findOne({ - 'branches.0.directories.0.files.0.id': 'hello' -}); +expectError( + recursiveSchemaWithArray.findOne({ + 'branches.0.directories.0.files.0.id': 'hello' + }) +); recursiveSchemaWithArray.findOne({ branches: [ diff --git a/test/types/community/transaction.test-d.ts b/test/types/community/transaction.test-d.ts index 259d76114db..f09b5e4bc13 100644 --- a/test/types/community/transaction.test-d.ts +++ b/test/types/community/transaction.test-d.ts @@ -8,6 +8,7 @@ const client = new MongoClient(''); const session = client.startSession(); interface Account { + name: string; balance: number; }