-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
When migrating from mongoose 5 to 7, how to get LeanDocument funcitonality? #13424
Comments
Can you please provide an example of how you're defining your schemas and models? We removed |
Thank you for the quick response. For background, we're using nestjs, so schema definition is done via decorators as such: @Schema(/* options */)
export class SomeClass {
id: string;
@Prop({ required: true })
someProp: string;
}
export const SomeClassSchema = SchemaFactory.createForClass(SomeClass);
static createForClass<TClass = any, _TDeprecatedTypeArgument = any>(
target: Type<TClass>,
): mongoose.Schema<TClass> {
const schemaDefinition = DefinitionsFactory.createForClass(target);
const schemaMetadata =
TypeMetadataStorage.getSchemaMetadataByTarget(target);
// HERE:
return new mongoose.Schema<TClass>(
schemaDefinition as SchemaDefinition<SchemaDefinitionType<TClass>>,
schemaMetadata && schemaMetadata.options,
);
} =========== Now that I look at it more closely I'm assuming it's safe to use async function getById(id: string): Promise<SomeClass> {
const result = await this.model.findById(id);
return result.toObject();
} What I noticed is that if I return the document without converting I guess my question is: async function getById(id: string): Promise<???> {
const result = await this.model.findById(id);
return result;
// ^ is a document, use `.toObject()`
} |
Why not just |
export const SomeClassSchema = SchemaFactory.createForClass(SomeClass); My question is how to prevent the types from lying about what there is in runtime: async function getById(id: string): Promise<SomeClass> {
const result = await this.model.findById(id);
console.log(result); // result will have document methods, __v, _id on it.
return result;
async function testFunc(id: string) {
const result = await this.getById(id);
// `typeof result` will be SomeClass, as if there are no additional properties on the result
console.log(result); // but in runtime result still has document methods, __v, _id on it, since we didnt do .toObject()
} |
If you want to add a type check that enforces that import mongoose from 'mongoose';
interface User {
name: string;
age: number;
}
const UserModel = mongoose.model('User', new mongoose.Schema<User>({
name: { type: String, required: true },
age: { type: Number, required: true }
}));
async function getById(id: string): Promise<User & { $locals?: never }> {
const user = await UserModel.findById(id).lean().orFail(); // <-- comment out `lean()` and this fails
return user;
} |
@vkarpov15 Can we recommend a replacement solution in the docs to assist migration?
Frankly it seems correct to add the type back. I still don't understand why it was removed. |
Because seemingly simple tasks like omitting properties are surprisingly nuanced in TypeScript. Features like private fields, generics, etc. make omitting properties tricky, so relying on LeanDocument by default caused a lot of issues. You can always overwrite the return type of Can you provide some examples of how you're using |
We pass lean documents around in our codebase and use TypeScript. Therefore, we need a type that represents what the result of calling We used to define it next to our models like this: const fooSchemaDef = {...};
export type FooFields = ObtainDocumentType<typeof fooSchemaDef>;
const FooModel = model("Foo", new Schema(fooSchemaDef));
export type FooDocument = FooFields & Document;
export type FooLeanDoc = LeanDocument<FooDocument>; I agree it's hard to get the types right in TypeScript and to be honest, I've generally found it quite difficult to work with Mongoose's types. However, regardless of the difficulty of implementation, I think the need is still there. For now I'm trying to just vendor LeanDocument myself and then doing the Mongoose 7 upgrade. |
Instead of doing One thing we're working on is const fooSchema = new Schema(fooSchemaDef);
export type FooDocument = HydratedDocument<FooFields>;
export type FooLeanDoc = inferRawDocType<typeof fooSchema>; Based on our experience so far, it is much easier to infer the raw doc type (what |
@vkarpov15 The problem is that I have no idea how to type Mongoose models correctly in any version, and they are changing in every major version. As an example illustration, I'm using Mongoose 6 and const subDoc = {
name: { type: String, required: true },
controls: { type: String, required: true },
};
const testSchema = {
question: { type: String, required: true },
subDocArray: { type: [subDoc], required: true },
subDoc: { type: subDoc, required: true },
}
type TestFields = ObtainDocumentType<typeof testSchema>;
type TestDoc = HydratedDocument<TestFields>;
const TestModel = model<TestDoc>("TestModel", new Schema(testSchema)); But this does not give the correct types. When I do a lean query, the types of I think the docs need to clearly state how to type 1) the raw schema definition, 2) lean documents (i.e. they include _id fields on subdocuments and subdocument arrays), and 3) the full document. And ideally all this should be derived from a single schema definition source of truth. https://mongoosejs.com/docs/typescript/subdocuments.html makes it sound like this is impossible without manually feeding your own types into Mongoose, is that true? |
@JavaScriptBach try the following, seems to compile fine with Mongoose 6.12: import mongoose from 'mongoose';
const subDoc = {
name: { type: String, required: true },
controls: { type: String, required: true },
};
const testSchema = {
question: { type: String, required: true },
subDocArray: { type: [new mongoose.Schema(subDoc)], required: true },
subDoc: { type: new mongoose.Schema(subDoc), required: true },
}
const TestModel = mongoose.model("TestModel", new mongoose.Schema(testSchema));
const doc = new TestModel({});
doc.subDocArray[0]._id; |
Thanks @vkarpov15 ! I ended up going with a different approach, as all of our schemas are defined using object literals and it would be confusing to tell users to define some fields using schemas, so I ended up writing the following: import type { IfEquals, Types } from "mongoose";
type GetPathType<T> = T extends Array<infer Item>
? // Need to wrap both sides of this `extends` with square brackets to avoid
// distributing unions, which changes the type.
// See https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types
[Item] extends [{ [x: string]: any }]
? Array<Item & { _id: Types.ObjectId }>
: T
: T extends { [x: string]: any }
? T & { _id: Types.ObjectId }
: T;
// Given a Mongoose document interface, hydrate it with additional types so that it's suitable
// to feed as the first type argument into mongoose.Model.
export type HydratedFields<T extends { [x: string]: any }> = {
[K in keyof T]: IfEquals<
T[K],
NonNullable<T[K]>,
GetPathType<T[K]>,
GetPathType<NonNullable<T[K]> | null | undefined>
>;
} & { id: string }; Taking a step back, I think the larger problem here is that Mongoose is trying to provide types for an API that is not statically typeable, so we've had to write a lot of hacks to get things somewhat working. If Mongoose wants to provide first-class support for TypeScript, then I think we should consider writing a statically typeable API in a future major release. |
We will take a look as to why |
types: allow defining document array using `[{ prop: String }]` syntax
Prerequisites
Mongoose version
7.2.0
Node.js version
18.16.0
MongoDB version
6.0
Operating system
macOS
Operating system version (i.e. 20.04, 11.3, 10)
No response
Issue
I'm trying to migrate our nestjs/mongoose codebase from version 5 to 7.
We've been using using
T.lean()
orT.toObject()
and our return types would beLeanDocument<T>
. Mongoose 7 no longer exports LeanDocument, and the existing migration guide suggests using the following setup:But this gives me
HydratedDocument
that I can get byHydratedDocument<T>
, which is not what I want since it has all the document methods on it.As an alternative I can use just
T
as my return type, but then anyDocument<T>
is matchingT
.I'd like to enforce that the result is a POJO,
LeanDocument
or just the documents fields and virtuals, and not aDocument
.What approach can I take?
P.S.
I've asked the same question on stackoverflow, feel free to answer there also, I'd gladly accept the answer.
The text was updated successfully, but these errors were encountered: