Skip to content

Instantly share code, notes, and snippets.

@loucadufault
Last active August 28, 2024 23:59
Show Gist options
  • Save loucadufault/1ab0397362f34f742d3f329e9396c959 to your computer and use it in GitHub Desktop.
Save loucadufault/1ab0397362f34f742d3f329e9396c959 to your computer and use it in GitHub Desktop.
Mongoose model plugin to convert documents to a JSON or DTO representation (e.g. for a ReST API), according to a customizable DTO definition that enables mapping from other properties and transforming property values.
// DTO assembler
export const toDTO = (DtoDef) => (schema) => {
let transform;
// preserve the previously defined transform, if any
if (schema.options.toJSON && schema.options.toJSON.transform) {
transform = schema.options.toJSON.transform;
}
const assembleDTO = (doc) =>
Object.entries(DtoDef).reduce((acc, [currKey, currVal]) => {
if (!currVal) {
// property should not be included in the assembled DTO (explicitly declared falsy value for this property)
return acc;
}
const source = currVal.hasOwnProperty("fromProp")
? doc[currVal.fromProp]
: doc[currKey];
const result = currVal.hasOwnProperty("transform")
? currVal.transform(source, doc)
: source;
acc[currKey] = result;
return acc;
}, {});
schema.options.toJSON = Object.assign(schema.options.toJSON || {}, {
transform(doc, ret, options) {
const dto = assembleDTO(doc);
// apply the previously defined transform, if any
if (transform) {
return transform(doc, dto, options);
}
return dto;
},
});
// can opt to skip this aliasing if you would prefer to call the plugin using the more typical `doc.toJSON()`.
schema.method("toDTO", function () {
return this.toJSON(); // this = doc
});
};
import mongoose, { Schema } from "mongoose";
import { toDTO } from "./plugins/to_dto.plugin";
const productSchema = new Schema(
{
name: { type: String, unique: true, required: true, maxLength: 100 },
count: { type: Number, required: true, min: 0 },
description: { type: String, required: true },
privateInfo: { type: String },
},
{ timestamps: true }
);
productSchema.plugin(
toDTO({
id: {
fromProp: "_id",
},
created_at: {
fromProp: "createdAt"
},
updated_at: {
fromProp: "updatedAt"
},
name: true,
count: true,
// limit returned description text to 1000 chars, for example
description: {
transform: (value) => value.slice(0, 1000),
},
// a more complex transform, where the produced value for the "summary" field in the returned DTO is generated from two unrelated fields in the given doc, which is passed as the second argument to the `transform` callback
summary: {
transform: (value, doc) => `${doc.name}: ${doc.description.split('.')[0]}`
// note that `value` will be undefined, since docs do not have a property "summary"
// a value could be supplied in this case by adding to the object a "fromProp" entry whose value is the key of a property that exists on docs
},
// can opt to explicitly declare that a document prop is not to be included in the assembled DTO, although props not included in the DTO definition object are omitted by default
// for example, the "sensitiveInfo" property will not be produced in the DTOs assembled for all docs, since it does not have an entry in the DTO definition object
__v: false,
})
);
export default mongoose.model("Product", productSchema);
// example usage
async function get(req, res, next) {
const product = Product.findById(id).exec();
return res.status(200).json({
resource: product.toDTO(),
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment