Automattic / mongoose

MongoDB object modeling designed to work in an asynchronous environment.
https://mongoosejs.com
MIT License
26.95k stars 3.84k forks source link

Typescript: Can't assign array to DocumentArray field #11754

Closed ajwootto closed 2 years ago

ajwootto commented 2 years ago

Do you want to request a feature or report a bug? bug What is the current behavior? When using a model with a DocumentArray field type representing an array of subdocuments, you are unable to assign a new array to that field. Normally this is valid Mongoose code which simply causes the array contents to be replaced.

If the current behavior is a bug, please provide the steps to reproduce.

import { Schema, model, connect, Types, connection } from 'mongoose'

interface NestedChild {
    name: string
    _id: Types.ObjectId
}
const nestedChildSchema: Schema = new Schema({ name: String })

interface Parent {
    nestedChildren: Types.DocumentArray<NestedChild>
    name?: string
}

const ParentModel = model<Parent>('Parent', new Schema({
    nestedChildren: { type: [nestedChildSchema] },
    name: String
}))

async function run() {
    await connect('mongodb://localhost:27017/')
    await connection.dropDatabase()
    await ParentModel.create({
        nestedChildren: [{ name: 'nestedChild' }],
        name: 'Parent'
    })

    const parent = await ParentModel.findOne().exec()

    if (!parent) {
        throw new Error('Parent not found')
    }

    //works
    parent.nestedChildren.push({ name: 'test' })

    // doesn't work, complains about missing subdocument ORM fields such as "ownerDocument" etc
    parent.nestedChildren = [{name: 'test'}]
}

run()

What is the expected behavior? I would hope that I'd still be able to assign plain arrays to the field like I can with plain Mongoose. This type of operation is useful when I need replace an array field with whatever new data is coming in to be saved.

What are the versions of Node.js, Mongoose and MongoDB you are using? Note that "latest" is not a version. Mongoose 6.3.1

As a side note, the documentation about how to use DocumentArray seems a bit inconsistent. On the main "schema" page it shows that type being used directly in the interface definition: https://mongoosejs.com/docs/typescript/schemas.html#arrays

But in the section dedicated to subdocuments it instead provides it as an "override" to the Model type, and the interface definition is a plain array: https://mongoosejs.com/docs/typescript/subdocuments.html#subdocument-arrays

Which of these approaches is the "correct" way? No matter which one I use the above issue seems to happen anyway.

Uzlopak commented 2 years ago

I cant come with a proper solution, as the issue is because of https://github.com/Automattic/mongoose/blob/45cb5518394cb4a7ac271f8657518713be09368b/types/types.d.ts#L60

It wraps the type in a SubDocument and then you get that result:

A workaround is:

 interface Parent {
    nestedChildren: Types.DocumentArray<NestedChild> | Partial<NestedChild>[];
    name?: string;
  }
Uzlopak commented 2 years ago

@mohammad0-0ahmad @taxilian

Any suggestions?

vkarpov15 commented 2 years ago

Here's a couple of workarounds we recommend:

1) Use set() instead of assigning using =:

parent.set('nestedChildren', [{name: 'test'}])

2) Explicitly instantiate an instance of Types.DocumentArray

parent.nestedChildren = new Types.DocumentArray<NestedChild>([{name: 'test'}]);

I'd love to have a better workaround for this, but without some sort of getters/setters support in TypeScript there isn't much Mongoose can do. To the best of my knowledge, TypeScript assumes that if you set obj.prop = val, then obj.prop === val, which is not necessarily true with Mongoose documents.

mohammad0-0ahmad commented 2 years ago

No, sorry, I've no idea.

ajwootto commented 2 years ago

@vkarpov15 in your first example using .set, it seems that that method doesn't perform any type checking on the value being set, whereas = does. Is that intended? Should the set method be capable of type checking the field being set?

vkarpov15 commented 2 years ago

@ajwootto that's correct, set() explicitly doesn't perform any type checking. That's because Mongoose tries to cast the type at runtime.