Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(NODE-3895): Emit TypeScript errors when filtering on fields not in collection schema #3115

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ export type {
InferIdType,
IntegerType,
IsAny,
IsInUnion,
Join,
KeysOfAType,
KeysOfOtherType,
Expand All @@ -306,6 +307,7 @@ export type {
RootFilterOperators,
SchemaMember,
SetFields,
TypeEquals,
UpdateFilter,
WithId,
WithoutId
Expand Down
43 changes: 38 additions & 5 deletions src/mongo_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ export type WithoutId<TSchema> = Omit<TSchema, '_id'>;

/** A MongoDB filter can be some portion of the schema or a set of operators @public */
export type Filter<TSchema> =
| Partial<TSchema>
| Partial<{
[Property in keyof TSchema]: Condition<TSchema[Property]>;
}>
| ({
[Property in Join<NestedPaths<WithId<TSchema>>, '.'>]?: Condition<
PropertyType<WithId<TSchema>, Property>
Expand All @@ -89,8 +91,23 @@ export type AlternativeType<T> = T extends ReadonlyArray<infer U>
/** @public */
export type RegExpOrString<T> = 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<TSchema> extends Document {
export interface RootFilterOperators<TSchema> extends OpenOperatorQuery {
$and?: Filter<TSchema>[];
$nor?: Filter<TSchema>[];
$or?: Filter<TSchema>[];
Expand Down Expand Up @@ -479,6 +496,24 @@ export type PropertyType<Type, Property extends string> = 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<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false

/**
* Check if A is a union type that includes B
* @public
*/
export type IsInUnion<A, B> = TypeEquals<Extract<A, B>, B>;

/**
* @public
* returns tuple of strings (keys to be joined on '.') that represent every path into a schema
Expand All @@ -504,9 +539,7 @@ export type NestedPaths<Type> = Type extends
? {
[Key in Extract<keyof Type, string>]: 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<Type[Key], Type> extends true
? [Key]
: Type[Key] extends ReadonlyArray<infer ArrayType> // handling recursive types with arrays
? Type extends ArrayType // is the type of the parent the same as the type of the array?
Expand Down
18 changes: 14 additions & 4 deletions test/types/community/collection/filterQuery.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ expectType<WithId<PetModel>[]>(
expectType<WithId<PetModel>[]>(
await collectionT.find({ name: new BSONRegExp('MrMeow', 'i') }).toArray()
);

/// it should not accept fields that are not in the schema
type FT = Filter<PetModel>;
expectNotType<FT>({ missing: true });

/// it should not accept wrong types for string fields
expectNotType<Filter<PetModel>>({ name: 23 });
expectNotType<Filter<PetModel>>({ name: { suffix: 'Jr' } });
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -283,6 +288,9 @@ expectNotType<Filter<PetModel>>({ name: { $or: ['Spot', 'Bubbles'] } });
/// it should not accept single objects for __$and, $or, $nor operator__ query
expectNotType<Filter<PetModel>>({ $and: { name: 'Spot' } });

/// it allows using unknown root operators with any value
expectAssignable<Filter<PetModel>>({ $fakeOp: 123 });

/**
* test 'element' query operators
*/
Expand Down Expand Up @@ -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({
Expand Down
15 changes: 10 additions & 5 deletions test/types/community/collection/findX-recursive-types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ recursiveOptionalCollection.find({
* recursive union types are supported
*/
interface Node {
value?: string;
next: Node | null;
}

Expand All @@ -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
Expand Down Expand Up @@ -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: [
Expand Down
1 change: 1 addition & 0 deletions test/types/community/transaction.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const client = new MongoClient('');
const session = client.startSession();

interface Account {
name: string;
balance: number;
}

Expand Down