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

docs(documents): add section on setting deeply nested properties, including warning about nullish coalescing assignment #14972

Merged
merged 1 commit into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/documents.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ to documents as stored in MongoDB. Each document is an instance of its
<li><a href="#documents-vs-models">Documents vs Models</a></li>
<li><a href="#retrieving">Retrieving</a></li>
<li><a href="#updating-using-save">Updating Using <code>save()</code></a></li>
<li><a href="#setting-nested-properties">Setting Nested Properties</a></li>
<li><a href="#updating-using-queries">Updating Using Queries</a></li>
<li><a href="#validating">Validating</a></li>
<li><a href="#overwriting">Overwriting</a></li>
Expand Down Expand Up @@ -81,6 +82,54 @@ doc.name = 'foo';
await doc.save(); // Throws DocumentNotFoundError
```

## Setting Nested Properties

Mongoose documents have a `set()` function that you can use to safely set deeply nested properties.

```javascript
const schema = new Schema({
nested: {
subdoc: new Schema({
name: String
})
}
});
const TestModel = mongoose.model('Test', schema);

const doc = new TestModel();
doc.set('nested.subdoc.name', 'John Smith');
doc.nested.subdoc.name; // 'John Smith'
```

Mongoose documents also have a `get()` function that lets you safely read deeply nested properties. `get()` lets you avoid having to explicitly check for nullish values, similar to JavaScript's [optional chaining operator `?.`](https://masteringjs.io/tutorials/fundamentals/optional-chaining-array).

```javascript
const doc2 = new TestModel();

doc2.get('nested.subdoc.name'); // undefined
doc2.nested?.subdoc?.name; // undefined

doc2.set('nested.subdoc.name', 'Will Smith');
doc2.get('nested.subdoc.name'); // 'Will Smith'
```

You can use optional chaining `?.` and nullish coalescing `??` with Mongoose documents.
However, be careful when using [nullish coalescing assignments `??=`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_assignment) to create nested paths with Mongoose documents.

```javascript
// The following works fine
const doc3 = new TestModel();
doc3.nested.subdoc ??= {};
doc3.nested.subdoc.name = 'John Smythe';

// The following does **NOT** work.
// Do not use the following pattern with Mongoose documents.
const doc4 = new TestModel();
(doc4.nested.subdoc ??= {}).name = 'Charlie Smith';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this not work because mongoose is cloning on assignment? (i have tried in a node REPL)

ret = (doc4.nested.subdoc ??= {})
ret === doc4.nested.subdoc // eval "false"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The problem is the following:

const x = {};
(doc4.nested.subdoc ??= x) === x; // true
doc4.nested.subdoc === x; // false

Basically a.b ??= x evaluates to x, not the value of a.b after the assignment. Which isn't a big deal in most cases, but with Mongoose the distinction matters.

We need to clone because doc4.nested.subdoc needs change tracking. This may be a case where using proxies instead of Object.defineProperty() for change tracking would help.

doc.nested.subdoc; // Empty object
doc.nested.subdoc.name; // undefined.
```

## Updating Using Queries {#updating-using-queries}

The [`save()`](api/model.html#model_Model-save) function is generally the right
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"lint": "eslint .",
"lint-js": "eslint . --ext .js --ext .cjs",
"lint-ts": "eslint . --ext .ts",
"lint-md": "markdownlint-cli2 \"**/*.md\"",
"lint-md": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#benchmarks\"",
"build-browser": "(rm ./dist/* || true) && node ./scripts/build-browser.js",
"prepublishOnly": "npm run build-browser",
"release": "git pull && git push origin master --tags && npm publish",
Expand Down