UNPKG

mongoose-lean-extension

Version:

This plugin extends the functionality of mongoose .lean() to avoid the extra work brought by the default behavior...

386 lines (283 loc) β€’ 12.9 kB
# [MONGOOSE LEAN EXTENSION](https://github.com/SsekandiRaymond/mongoose-lean-extension) A powerful Mongoose plugin that **extends the functionality of `.lean()`** queries to eliminate common post-processing steps β€” like stringifying ObjectIds, renaming `_id` key to something else, and removing internal Mongoose metadata like `__v`. --- > `mongoose-lean-extension@1.0.0` is here. Welcome this major version update. 0 -> 1 ### Breaking Changes: > Read through this README for complete overview > Visit my GitHub repo for the source code > Typescript support > My contact are everywhere in this source code --- ## Overview Mongoose’s `.lean()` is a great performance optimization, returning plain JavaScript objects instead of full Mongoose documents. But it comes with annoyances: - ObjectId keys are still BSON types (`ObjectId`) that require manual conversion. - The `__v` version key is often unnecessarily present in results. - Deeply nested ObjectIds in subdocuments aren't stringified without additional effort. **`mongoose-lean-extension` solves these problems** by automating the transformation of `.lean()` results. It enables: - Auto-stringification of top-level `_id` fields - Auto-removal of the `__v` field - Optional stringification of nested ObjectId fields via path expressions (e.g. `contributors._id`) --- ## Installation ```bash npm i mongoose-lean-extension ``` ## πŸ”§ Usage ### Global Plugin (Recommended) ```javascript const mongooseLeanExtension = require("mongoose-lean-extension"); const mongoose = require("mongoose"); // Apply plugin globally to all schemas mongoose.plugin(mongooseLeanExtension); ``` ### Per-Schema Usage ```javascript const mongooseLeanExtension = require("mongoose-lean-extension"); const schema = new mongoose.Schema({ /* ... */ }); schema.plugin(mongooseLeanExtension); ``` ### `IMPORTANT NOTICE` - All plugins are attached to the schema before it is compiled to create a model > ⚑ `mongoose-lean-extension` introduces negligible latency (<2ms per 100 documents) on average `.lean()` queries. - For how utility function in stringifyKeys > See [`util/stringifyPaths.js`](./util/stringifyPaths.js) ## Configuration Options The plugin is configured via the .lean() query method. ```javascript Model.find().lean({ stringifyId: true, // (default: true) β€” Convert `_id` to string showVersion: false, // (default: false) β€” Remove `__v` from results stringifyKeys: ["nested._id"], // (optional) β€” Array of mongo-like paths [dot-paths] to nested ObjectId fields to stringify rename: "key", // Renames `_id` to `key` }); ``` ## Option Details | Option | Type | Default | Description | | --------------- | -------- | --------- | --------------------------------------------------------------------------------------- | | `stringifyId` | boolean | true | If true, stringifies the document `_id`. If false, leaves as ObjectId. | | `showVersion` | boolean | false | If false, removes the `__v` field from results if they exist otherwise, leaves them. | | `stringifyKeys` | string[] | [] | Mongo-like (Dot-separated) paths to any nested ObjectId fields to convert into strings. | | `rename` | string | undefined | Renames `_id` to provided string value. | ## Example ```javascript const mongooseLeanExtension = require("mongoose-lean-extension"); const mongoose = require("mongoose"); mongoose.plugin(mongooseLeanExtension); const ContributorSchema = new mongoose.Schema({ username: { type: String, required: true }, languages: [String], }); const PackageSchema = new mongoose.Schema( { name: { type: String, required: true }, contributors: [ContributorSchema], // This will have ObjectId _id for each of the nested document, Perfect for nested stringification }, { timestamps: true } ); const Package = mongoose.model("Package", PackageSchema); /** * await Package.insertMany([ * { * name: "express", * contributors: [ * { * username: "tjholowaychuk", * languages: ["JavaScript", "TypeScript"], * }, * { * username: "dougwilson", * languages: ["JavaScript"], * }, * ], * }, * { * name: "react", * contributors: [ * { * username: "gaearon", * languages: ["JavaScript", "TypeScript"], * }, * { * username: "bvaughn", * languages: ["JavaScript"], * }, * ], * } * ]) */ const packages = await Package.find().lean({ stringifyKeys: ["contributors._id"], }); console.dir(packages, { depth: null }); ``` **Output** ```json [ { "_id": "683a2480b53cfe150c05c5a8", "name": "express", "contributors": [ { "username": "tjholowaychuk", "languages": ["JavaScript", "TypeScript"], "_id": "683a2480b53cfe150c05c5a9" }, ... ], "createdAt": "2025-05-30T21:34:56.981Z", "updatedAt": "2025-05-30T21:34:56.981Z" }, ... ] ``` ## How It Works - Under the hood, the plugin attaches a post-query middleware on find and findOne: - If the query uses .lean(), it inspects custom plugin options from lean({...}). - `_id` fields are converted to .toString() if enabled. - `__v` is stripped if disabled. Nested ObjectIds are stringified recursively using a safe traversal method. ##### Custom Path Stringification The helper function `applyStringifyAtPath(object, path)` walks through nested objects and arrays to convert specific paths like: ```js stringifyKeys: ["contributors._id", "authors.contact._id"]; ``` ## Plugin Architecture This package consists of a modular design that allows importing specific plugins if needed. #### Included Plugins | Plugin | File | Purpose | | ------------- | ---------- | ------------------------------------------------ | | stringifyId | plugins.js | Converts top-level `_id` fields | | rename | plugins.js | Renames top-level `_id` to provided string value | | deversion | plugins.js | Removes `__v` fields | | stringifyKeys | plugins.js | Stringifies custom ObjectId paths | | main | index.js | All-in-one plugin (recommended) | `Example: Using Only deversion` ```js const { deversion } = require("mongoose-lean-extension/plugins"); mongoose.plugin(deversion); ``` `Example: Selective Composition` ```js const { stringifyId } = require("mongoose-lean-extension/plugins.js"); const { stringifyKeys } = require("mongoose-lean-extension/plugins.js"); const { deversion } = require("mongoose-lean-extension/plugins.js"); const { rename } = require("mongoose-lean-extension/plugins.js"); const schema = new mongoose.Schema({...}); schema.plugin(stringifyKeys); schema.plugin(stringifyId); schema.plugin(deversion); schema.plugin(rename); ``` ### Testing Unit tests are written using Jest. ```bash $ npm run test > mongoose-lean-extension@1.0.0 test > jest --coverage PASS __tests__/index.test.js PASS __tests__/plugins.test.js (5.995 s) ------------------------------|---------|----------|---------|---------|------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s ------------------------------|---------|----------|---------|---------|------------------- All files | 95.91 | 86.66 | 100 | 97.64 | Mongoose Lean Extension | 95.23 | 84.81 | 100 | 97.22 | index.js | 93.33 | 87.09 | 100 | 96.29 | 61 plugins.js | 96.29 | 83.33 | 100 | 97.77 | 59 Mongoose Lean Extension/util | 100 | 100 | 100 | 100 | stringifyPaths.js | 100 | 100 | 100 | 100 | ------------------------------|---------|----------|---------|---------|------------------- Test Suites: 2 passed, 2 total Tests: 29 passed, 29 total Snapshots: 0 total Time: 6.591 s Ran all test suites. ``` Includes tests using `mongodb-memory-server` for in-memory database mocking. ### Advanced #### How to Preserve `_id` as ObjectId? ```js Model.find().lean({ stringifyId: false }); // Disables stringification of `_id` ``` Or; do not use the all-in-one plugin at "global level" rather use the purpose-specific at "schema level" #### How to Keep `__v` mongoose metadata? ```js Model.find().lean({ showVersion: true }); // Keeps __v field in results ``` Or; do not use the all-in-one plugin at "global level" rather use the purpose-specific at "schema level" #### How to Keep nested ObjectId fields? ```js Model.find().lean(); // Keeps nested ObjectIds ``` Or; do not use the plugin ## Extras ```js async function __run_() { await mongoose.connect("mongodb://localhost:27017/trials"); const User = mongoose.model("User", new Schema({ name: String, age: Number })); const Car = mongoose.model( "Car", new Schema({ model: String, owner: { type: mongoose.Schema.Types.ObjectId, ref: "User" }, }) ); const ContributorSchema = new Schema({ username: { type: String, required: true }, languages: [String], }); const PackageSchema = new Schema( { name: { type: String, required: true }, contributors: [ContributorSchema], }, { timestamps: true } ); /** First time? Un-comment the lines below and run the code. * After initial run comment them again (or delete) */ // await Car.deleteMany({}); // await User.deleteMany({}); // await Promise.allSettled([ // User.create({ name: "Ariella", age: 2 }), // User.create({ name: "Arianna", age: 33 }), // ]); // await Promise.allSettled([ // Car.create({ // model: "BMW", // owner: Types.ObjectId.createFromHexString("6839968512bb0d7b0d62e23e"), // }), // Car.create({ // model: "Rover", // owner: Types.ObjectId.createFromHexString("6839968512bb0d7b0d62e23e"), // }), // Car.create({ // model: "Toyota", // owner: Types.ObjectId.createFromHexString("6839968512bb0d7b0d62e23f"), // }), // ]); const users = await User.find().lean(); console.log(users); const cars = await Car.find().populate("owner").lean(); // if "stringifyId" plugin is used on mongoose "global level" that is, mongoose.plugin(stringifyId) and .populate("field") is on the query, both car._id and car.owner._id will be hex strings however, if it is used on schema level, for example, CarSchema.plugin(stringifyId) only: car._id will be hex string but car.owner._id will be ObjectId... if UserSchema.plugin(stringifyId): car.owner._id will be hex string but car._id will be ObjectId. console.log(cars); /** cars.owner might be null in case you do not assign the right object ids as new ones are created everytime a document is recorded. Check "owner" property assigned value for await Promise.allSettled([ ... ]) for Car.create({}) */ await mongoose.disconnect(); } __run_().catch(console.error); ``` ## βœ… Compatibility - βœ… Mongoose 8.x - βœ… MongoDB >= 4.x ### Contributing Pull requests are welcome! Feel free to open issues or suggestions on improvements, use cases, or performance enhancements. ### Feedback If this plugin saves you time or solves your pain, please consider starring the repo and sharing it with the Mongoose community! `Let me know if you'd like this split into multiple files (e.g. for GitHub pages) or want me to generate a live demo repo or documentation website from it.` ### License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ## Contributors 1. [Ssekandi](https://github.com/SsekandiRaymond) - Author ### 😏 Gotcha Section {Ignore if you are in a bad mood} - Aaah nothing much, just weekly downloads reduce when I do not update this package. - Anyways, I added the rename functionality because I found it a little awkward to use `_id` in my data for React Native's `FlatList` keyExtractor prop.