-
-
Notifications
You must be signed in to change notification settings - Fork 612
Description
Describe the bug
Hi, and thanks again for maintaining this excellent repo!
I'm back with another bug report regarding JSON properties, sorry in advance 😆
This is a regression that happened somewhere between 5.5.3 and 6.3.10.
After upgrading, I noticed that some JSON properties were not properly persisted in the database.
TLDR: This happens when the JSON object is polluted with a prototype containing keys equal to the actual object keys.
More context
Using the debugger, I found out that the following condition was reached when computing the ChangeSet and comparing values:
mikro-orm/packages/core/src/utils/clone.ts
Lines 136 to 138 in 4b263f3
| if (attrs && attrs.set == null) { | |
| continue; | |
| } |
When inspecting the prototype of these JSON properties, I discovered that it was "polluted" with some "default values" copied from their JSON schema. This was due to the usage of the Yup library (v0.32.11): transforming user inputs to schema-validated DTO with the cast() function creates objects with a prototype that contains default values from the yup schema.
Workaround
I'm not sure if MikroORM should fix this issue, as it happens in a very specific scenario. Ideally, JSON properties should not be polluted with prototypes. Feel free to close it or convert it into a discussion. I'm mostly posting for reference and to help others if anyone encounters the same problem.
What I did is that I overwrote Yup's cast() method to strip the resulting objects' prototypes, like so:
/**
* Objects created by Yup are decorated with prototypes to store
* metadata like default schema values, etc., which can have an impact on
* downstream logic. For instance, it prevents MikroORM from comparing
* diffs on managed entities.
*/
yup.addMethod(yup.object, "cast", function (value, options) {
// @ts-expect-error use the original cast function, otherwise we get an infinite recursive loop
const castedObject = this._cast(value, options);
return stripPrototype(castedObject);
});With the following code for the stripPrototype() method:
import { ObjectId } from "mongodb";
const ignoredClasses = [Date, ObjectId];
export function stripPrototype(obj: unknown) {
// If the input is not an object or is null, return it as is (base case for recursion)
if (typeof obj !== "object" || obj === null) {
return obj;
}
for (const ignoredClass of ignoredClasses) {
if (obj instanceof ignoredClass) return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => stripPrototype(item));
}
// Create a new object without prototype
const cleanObj = Object.create(null);
// Iterate over the object's own properties
for (const key of Object.getOwnPropertyNames(obj)) {
const value = obj[key];
// Recursively strip prototype from sub-properties
cleanObj[key] = stripPrototype(value);
}
return cleanObj;
}Reproduction
Here is a simple test case to reproduce the issue: https://github.com/alexandre-abrioux/mikro-orm-issues/tree/iss-6050 ; this does not use the Yup library; I kept it simple.
What driver are you using?
@mikro-orm/mongodb
MikroORM version
6.3.10
Node.js version
18.20.4
Operating system
No response
Validations
- Read the Contributing Guidelines.
- Read the docs.
- Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- Check that this is a concrete bug. For Q&A open a GitHub Discussion or join our Discord.
- The provided reproduction is a minimal reproducible example of the bug.