Skip to content

Conversation

@ferk6a
Copy link

@ferk6a ferk6a commented Nov 17, 2019

The current mergeDeep implementation tries to recursively copy complex objects like class instances. So, a new implementation of mergeDeep and isPlainObject (previously isObject) is given. This fixes #5096, which demonstrates the problem.

As a bonus, this creates tests for these methods which weren't tested before.

@ferk6a ferk6a closed this Nov 17, 2019
@ferk6a ferk6a reopened this Nov 17, 2019
@ferk6a
Copy link
Author

ferk6a commented Nov 17, 2019

I guess the tests pass, but I'm a bit worried about edge cases, maybe something like passing a class instance to an insert, or some kind of recursive scenario?

@pleerock
Copy link
Member

Tests can easily pass because we don't have a large test coverage for monogdb driver.

I think your changes won't work properly because in the cases when embedded objects (nested objects) are used, we actually need class instances, we can't skip them.

Solution to this issue is to rewrite current way of "copying object" - traverse over defined columns in the class and all its embeddeds and copy only object properties that are defined in these columns. Columns definition tells us what schema should be used for saving and this schema can't be recursive.

@ferk6a
Copy link
Author

ferk6a commented Nov 23, 2019

@pleerock Ok, thanks! I will work some more on this then. I will add more tests regarding having nested embedded objects and objects with more information than the schema allows (should we throw an error or something if we detect that, or just ignore?).

I will work on making a new function to transform nested entities into plain objects using schema (probably using getMetadata and .prototype, I still don't know exactly how and how well it will scale), and then apply that instead of { ... }, and I think then we will be ready!

@ferk6a
Copy link
Author

ferk6a commented Nov 25, 2019

Tests can easily pass because we don't have a large test coverage for monogdb driver.

I think your changes won't work properly because in the cases when embedded objects (nested objects) are used, we actually need class instances, we can't skip them.

@pleerock I was looking into this again, and it looks like there is a test for this, and it passed:

it("should transform entity with nested embedded columns correctly", () => Promise.all(connections.map(async connection => {
const postRepository = connection.getMongoRepository(Post);
// save few posts
const post = new Post();
post.title = "Post #1";
post.text = "Everything about post";
post.counters = new Counters();
post.counters.likes = 5;
post.counters.comments = 0;
post.counters.favorites = 1;
post.counters.information = new Information();
post.counters.information.description = "Hello post";
post.counters.extraInformation = new ExtraInformation();
post.counters.extraInformation.lastEdit = new EditHistory();
post.counters.extraInformation.lastEdit.title = "Old Post Title";
post.counters.extraInformation.lastEdit.text = "Not everything about post";
await postRepository.save(post);
const [loadedPost] = await postRepository.find();
loadedPost.counters.comments.should.be.equal(0);
loadedPost.counters.favorites.should.be.equal(1);
loadedPost.counters.extraInformation.lastEdit.title.should.be.eql("Old Post Title");
loadedPost.counters.extraInformation.lastEdit.text.should.be.eql("Not everything about post");

So I don't know exactly how I could break this, since I saw that it passed that, it looks to me to be pretty solid (even though it really doesn't look like it).

@papaiatis
Copy link

@pleerock is this ever going to get merged any time soon?

Copy link
Contributor

@imnotjames imnotjames left a comment

Choose a reason for hiding this comment

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

Can you rebase & fix the conflict?

// for mongodb we have a bit different updation logic
if (this.queryRunner instanceof MongoQueryRunner) {
const partialEntity = OrmUtils.mergeDeep({}, subject.entity!);
const partialEntity = OrmUtils.mergeDeep({}, { ...subject.entity! });
Copy link
Contributor

Choose a reason for hiding this comment

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

Why shallow cloning the object here? Doesn't this code that already?

Comment on lines -61 to +62
static isObject(item: any) {
return (item && typeof item === "object" && !Array.isArray(item));
// Checks if it's an object made by Object.create(null), {} or new Object()
static isPlainObject(item: any) {
Copy link
Contributor

@imnotjames imnotjames Oct 17, 2020

Choose a reason for hiding this comment

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

If you're dropping isObject can you make this private? To reduce the public API footprint.

Comment on lines +62 to +63
@Column({ type: String, transformer: new ComplexTransformer(), nullable: true })
complex: Complex | null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to add this to a different entity in a different entity file?

That way we're not muddying up the original tests

Comment on lines +4 to +37
describe("OrmUtils.isPlainObject", () => {
const isPlainObject = OrmUtils.isPlainObject.bind(OrmUtils);

it("should return `true` if the Object is made by Object.create(null), {} or new Object().", () => {
class Foo {}

expect(isPlainObject(new Object())).to.be.true;
expect(isPlainObject(Object.create(null))).to.be.true;
expect(isPlainObject(Object.create(Object.prototype))).to.be.true;
expect(isPlainObject({ foo: "bar" })).to.be.true;
expect(isPlainObject({ constructor: "foo" })).to.be.true;
expect(isPlainObject({ constructor: Foo })).to.be.true;
expect(isPlainObject({}));
});

it("should return `false` if the Object is not made by Object.create(null), {} or new Object().", () => {
class Foo {}

expect(isPlainObject(Object.create({}))).to.be.false;
expect(isPlainObject(Object.create(Object))).to.be.false;
expect(isPlainObject(/foo/)).to.be.false;
expect(isPlainObject(function() {})).to.be.false;
expect(isPlainObject(() => {})).to.be.false;
expect(isPlainObject(["foo", "bar"])).to.be.false;
expect(isPlainObject([])).to.be.false;
expect(isPlainObject(new Foo())).to.be.false;
expect(isPlainObject(null)).to.be.false;
expect(isPlainObject(Math)).to.be.false;
expect(isPlainObject(Error)).to.be.false;
expect(isPlainObject(0)).to.be.false;
expect(isPlainObject(false)).to.be.false;
expect(isPlainObject(NaN)).to.be.false;
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

since we're doing all the tests below - do we need to test isPlainObject if it's just an internal helper? Do the below tests not go through all the possible permutations?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inserting entities with complex objects with circular references results in stack overflow

4 participants