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

Mongoose types incorrect for when rawResult: true is set within findOneAndUpdate middleware #13987

Closed
2 tasks done
FullStackSteve opened this issue Oct 17, 2023 · 13 comments
Closed
2 tasks done
Labels
confirmed-bug We've confirmed this is a bug in Mongoose and will fix it. typescript Types or Types-test related issue / Pull Request
Milestone

Comments

@FullStackSteve
Copy link

FullStackSteve commented Oct 17, 2023

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

Mongoose version

~7.4.4

Node.js version

v16.19.1

MongoDB server version

4.0

Typescript version (if applicable)

~11.4.1

Description

Initially reported here typegoose/typegoose#886 and recommended to post here re: comment:

Incorrect types for the doc in the post findOneAndUpdate middleware when rawResult: true is set as option to the model.findOneAndUpdate(...) method

Steps to Reproduce

In typegoose:

// MongoDB: 5.0 (Docker)
// Typescript 4.9.5
import { getModelForClass, prop, post } from '@typegoose/typegoose'; // @typegoose/typegoose@11.5.1
import * as mongoose from 'mongoose'; // mongoose@7.5.4

@post<User>('findOneAndUpdate', function (doc, next) {
  // @ts-ignore
  doc.value.id = doc.value._id;  
  // @ts-ignore
  doc.value.save();
  next();
})
class User {
  @prop()
  public id: string;

  @prop()
  public username?: string;
}

const UserModel = getModelForClass(User);

async function main() {
  await mongoose.connect(`mongodb://localhost:27017/`, {
    dbName: 'verifyMASTER',
  });

  const doc = new UserModel({ username: 'user1' });

  const upsertResult = await UserModel
    .findOneAndUpdate({}, { username: 'user1' }, {
      upsert: true,
      new: true,
      rawResult: true
    })
    .exec();

  console.log(doc);

  await mongoose.disconnect();
}

main();

Roughly the mongoose equivalent:

const UserModel = mongoose.model('User', new mongoose.Schema({
  id: String,
  username: Number
}));

UserModel.post('findOneAndUpdate', function(doc, next) {
   // TYPES indicate doc.id but actual passed is doc.value.id
   doc.value.id = doc.value._id;  
   // TYPES indicate doc.save() but actual passed is doc.value.save()
   doc.value.save();
   next();
});


async function main() {
  await mongoose.connect(`mongodb://localhost:27017/`, {
    dbName: 'verifyMASTER',
  });

  const doc = new UserModel({ username: 'user1' });

  const upsertResult = await UserModel
    .findOneAndUpdate({}, { username: 'user1' }, {
      upsert: true,
      new: true,
      rawResult: true
    })
    .exec();

  console.log(doc);

  await mongoose.disconnect();
}

main();

Expected Behavior

The middleware should return the correct types for the doc parameter when the rawResult: true parameter is passed so that this change can be reflected in the typegoose library

@hasezoey
Copy link
Collaborator

just as a note, the mongoose code example is wrong (you try to call schema function post on the model), the corrected example would be:

const UserSchema = new mongoose.Schema({
  id: String,
  username: Number,
});

UserSchema.post('findOneAndUpdate', function (doc, next) {
  // TYPES indicate doc.id but actual passed is doc.value.id
  doc.value.id = doc.value._id;
  // TYPES indicate doc.save() but actual passed is doc.value.save()
  doc.value.save();
  next();
});

const UserModel = mongoose.model('User', UserSchema);

@hasezoey hasezoey added the typescript Types or Types-test related issue / Pull Request label Oct 17, 2023
@IslandRhythms
Copy link
Collaborator

import * as mongoose from 'mongoose';

const UserSchema = new mongoose.Schema({
  id: String,
  username: String,
});

UserSchema.post('findOneAndUpdate', function (doc) {
  console.log('what is doc', doc);
  // TYPES indicate doc.id but actual passed is doc.value.id
  doc.value.id = doc.value._id.toString();
  // TYPES indicate doc.save() but actual passed is doc.value.save()
  doc.value.save();
});

const UserModel = mongoose.model('User', UserSchema);

async function main() {
  await mongoose.connect(`mongodb://localhost:27017/`);
  await mongoose.connection.dropDatabase();

  const doc = new UserModel({ username: 'user1' });

  await doc.save();
  const upsertResult = await UserModel
    .findOneAndUpdate({}, { username: 'user1' }, {
      upsert: true,
      new: true,
      rawResult: true
    })
    .exec();

  console.log(doc);
}

main();

@IslandRhythms IslandRhythms added confirmed-bug We've confirmed this is a bug in Mongoose and will fix it. and removed typescript Types or Types-test related issue / Pull Request labels Oct 18, 2023
@hasezoey
Copy link
Collaborator

@IslandRhythms why remove the typescript label? to my knowledge this is a issue about types

@IslandRhythms
Copy link
Collaborator

@hasezoey Val has told me that he only wants one label per issue so confirmed-bug overrides typescript. I do agree with you though.

@vkarpov15
Copy link
Collaborator

I think it is appropriate to have TypeScript label even if we also have "confirmed-bug" just to see what is a purely TypeScript issue.

@vkarpov15 vkarpov15 added the typescript Types or Types-test related issue / Pull Request label Oct 24, 2023
@vkarpov15 vkarpov15 added this to the 7.6.5 milestone Oct 24, 2023
@prathamVaidya
Copy link
Contributor

Can I work on this issue?

@hasezoey
Copy link
Collaborator

you can put in a PR if you want to, i am not aware of anyone actively working on this yet

probably the easiest would be to change the hooks for which functions return a ModifyResult to be a OR of that and the original value

@prathamVaidya
Copy link
Contributor

Okay! I'll check the code.

@prathamVaidya
Copy link
Contributor

prathamVaidya commented Nov 2, 2023

@hasezoey, after reviewing the Mongoose hook TypeScript declaration, I've noticed that Mongoose returns the document in hooks as the any type. (Mongoose 7.5)

The problem mentioned by @FullStackSteve is related to the types defined in the Typegoose library, specifically at this line. To resolve this issue, you have a few possible solutions:

  1. You can infer the document as any in your callback function.
  2. Consider updating Typegoose to return the document as any.
  3. Alternatively, you can update Typegoose to return Document | ModifyResult<Document. However, if you choose this option, you'll need to check every time in your callback whether it's the original document or the modified document.

Now after the checking out the Typegoose Issue , I know you already knew about the above details. Should I implement 3rd solution in typegoose library and create a PR?

@hasezoey
Copy link
Collaborator

hasezoey commented Nov 2, 2023

The problem mentioned by @FullStackSteve is related to the types defined in the Typegoose library, specifically at this line. To resolve this issue, you have a few possible solutions:

from what i can tell, yes this could be fixed by typegoose, but mongoose also has some types which could be changed, for example (for 7.5 & 8.0):

post<T = Query<any, any>>(method: MongooseDistinctQueryMiddleware|MongooseDistinctQueryMiddleware[], options: SchemaPostOptions, fn: PostMiddlewareFunction<T, QueryResultType<T>>): this;

could be changed to something like

post<T = Query<any, any>>(method: MongooseDistinctQueryMiddleware|MongooseDistinctQueryMiddleware[], options: SchemaPostOptions, fn: PostMiddlewareFunction<T, QueryResultType<T> | ModifyResult<QueryResultType<T>>>): this;

(would require separating out the function that return ModifyResult from MongooseDistinctQueryMiddleware

though that would likely not change the default type from "any" because of the default Query<any, any> and typescripts tendency to simplify to any instead of the types if available like any | ModifyResult<any>, but this would improve if the user provides a custom type (which is what typegoose does by default) like

UserSchema.post<
  mongoose.Query<mongoose.HydratedDocument<mongoose.InferSchemaType<typeof UserSchema>>, mongoose.InferSchemaType<typeof UserSchema>>
>

would then give the doc parameter the following type:

doc: (mongoose.Document<unknown, {}, {
    id?: string | null | undefined;
    username?: string | null | undefined;
}> & {
    id?: string | null | undefined;
    username?: string | null | undefined;
} & {
    ...;
}) | mongoose.ModifyResult<...>

(and according errors)

@prathamVaidya
Copy link
Contributor

@hasezoey I got your point. This can be useful if user provides a custom type.
However, I'd like you to clarify the part where you mentioned:

(would require separating out the function that return ModifyResult from MongooseDistinctQueryMiddleware

Is this related to separating the ModifyResult<> type if rawResult is found in query options because query options are not accessible in hooks.

Also, anyways user will have to add a manual check for "value" to differentiate between the ModifyResult<> and Document in the callback function. Is there anyway that can be avoided or improved?

From what I gather, my current understanding is that I need to add ModifyResult in the post hooks that supports rawResult and create a PR for the master branch.

@hasezoey
Copy link
Collaborator

hasezoey commented Nov 3, 2023

Is this related to separating the ModifyResult<> type if rawResult is found in query options because query options are not accessible in hooks.

i dont quite know what you are asking, i mean separating out things like find*And* which could return a ModifyResult from functions that do not (like count), i am not sure exactly which function do and dont return that and may require double-checking with the runtime code to verify

Is there anyway that can be avoided or improved?

to my knowledge this cannot be avoided, and the only known ways of dealing with this are:

  • cast the value to what you want (if you are sure you wont ever use the other version)
  • assert the value (assuming the assert function used actually has the proper types, see assertion in typegoose's utils)
  • use if-blocks like if ("value" in doc) { ModifyResult }

the last 2 options (assert and if-blocks) could be slightly improved by adding a helper function to detect if it is actually a ModifyResult instead of something else, that would need to be a bit more involved, because the passed-in object (read document) could also have its own value defined in its schema

@prathamVaidya
Copy link
Contributor

@hasezoey Apologies for the delayed response; I was occupied for a while. I've identified the middlewares that support includeResultMetadata and added separate types to fix the Issue in v8.0.

Check the PR here.

Let me know if its acceptable or I need to fix something else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
confirmed-bug We've confirmed this is a bug in Mongoose and will fix it. typescript Types or Types-test related issue / Pull Request
Projects
None yet
Development

No branches or pull requests

5 participants