diff --git a/src/decorator/options/ColumnEmbeddedOptions.ts b/src/decorator/options/ColumnEmbeddedOptions.ts index 1256fd1d2c..385278f878 100644 --- a/src/decorator/options/ColumnEmbeddedOptions.ts +++ b/src/decorator/options/ColumnEmbeddedOptions.ts @@ -9,4 +9,10 @@ export interface ColumnEmbeddedOptions { */ prefix?: string | boolean; + /** + * Indicates if this embedded is in array mode. + * + * This option works only in mongodb. + */ + array?: boolean; } diff --git a/src/metadata/ColumnMetadata.ts b/src/metadata/ColumnMetadata.ts index 583c6643c7..ca97c42a8d 100644 --- a/src/metadata/ColumnMetadata.ts +++ b/src/metadata/ColumnMetadata.ts @@ -526,31 +526,40 @@ export class ColumnMetadata { // first step - we extract all parent properties of the entity relative to this column, e.g. [data, information, counters] const propertyNames = [...this.embeddedMetadata.parentPropertyNames]; + const isEmbeddedArray = this.embeddedMetadata.isArray; // now need to access post[data][information][counters] to get column value from the counters // and on each step we need to create complex literal object, e.g. first { data }, // then { data: { information } }, then { data: { information: { counters } } }, // then { data: { information: { counters: [this.propertyName]: entity[data][information][counters][this.propertyName] } } } // this recursive function helps doing that - const extractEmbeddedColumnValue = (propertyNames: string[], value: ObjectLiteral, map: ObjectLiteral): any => { + const extractEmbeddedColumnValue = (propertyNames: string[], value: ObjectLiteral): ObjectLiteral => { + if (value === undefined) { + return {}; + } + const propertyName = propertyNames.shift(); - if (value === undefined) - return map; if (propertyName) { - const submap: ObjectLiteral = {}; - extractEmbeddedColumnValue(propertyNames, value[propertyName], submap); + const submap = extractEmbeddedColumnValue(propertyNames, value[propertyName]); if (Object.keys(submap).length > 0) { - map[propertyName] = submap; + return { [propertyName]: submap }; } - return map; + return {}; } - if (value[this.propertyName] !== undefined && (returnNulls === false || value[this.propertyName] !== null)) - map[this.propertyName] = value[this.propertyName]; - return map; + + if (isEmbeddedArray && Array.isArray(value)) { + return value.map(v => ({ [this.propertyName]: v[this.propertyName] })); + } + + if (value[this.propertyName] !== undefined && (returnNulls === false || value[this.propertyName] !== null)) { + return { [this.propertyName]: value[this.propertyName] }; + } + + return {}; }; - const map: ObjectLiteral = {}; - extractEmbeddedColumnValue(propertyNames, entity, map); + const map = extractEmbeddedColumnValue(propertyNames, entity); + return Object.keys(map).length > 0 ? map : undefined; } else { // no embeds - no problems. Simply return column property name and its value of the entity @@ -589,6 +598,7 @@ export class ColumnMetadata { // first step - we extract all parent properties of the entity relative to this column, e.g. [data, information, counters] const propertyNames = [...this.embeddedMetadata.parentPropertyNames]; + const isEmbeddedArray = this.embeddedMetadata.isArray; // next we need to access post[data][information][counters][this.propertyName] to get column value from the counters // this recursive function takes array of generated property names and gets the post[data][information][counters] embed @@ -616,6 +626,8 @@ export class ColumnMetadata { } else if (this.referencedColumn) { value = this.referencedColumn.getEntityValue(embeddedObject[this.propertyName]); + } else if (isEmbeddedArray && Array.isArray(embeddedObject)) { + value = embeddedObject.map(o => o[this.propertyName]); } else { value = embeddedObject[this.propertyName]; } diff --git a/src/metadata/EntityMetadata.ts b/src/metadata/EntityMetadata.ts index a2246ada94..c2c5c87de0 100644 --- a/src/metadata/EntityMetadata.ts +++ b/src/metadata/EntityMetadata.ts @@ -770,7 +770,7 @@ export class EntityMetadata { if (map === undefined || value === null || value === undefined) return undefined; - return column.isObjectId ? Object.assign(map, value) : OrmUtils.mergeDeep(map, value); + return OrmUtils.mergeDeep(map, value); }, {} as ObjectLiteral|undefined); } diff --git a/src/persistence/SubjectExecutor.ts b/src/persistence/SubjectExecutor.ts index 2345b7bfde..91141fea6f 100644 --- a/src/persistence/SubjectExecutor.ts +++ b/src/persistence/SubjectExecutor.ts @@ -387,7 +387,7 @@ export class SubjectExecutor { // for mongodb we have a bit different updation logic if (this.queryRunner instanceof MongoQueryRunner) { - const partialEntity = OrmUtils.mergeDeep({}, subject.entity!); + const partialEntity = this.cloneMongoSubjectEntity(subject); if (subject.metadata.objectIdColumn && subject.metadata.objectIdColumn.propertyName) { delete partialEntity[subject.metadata.objectIdColumn.propertyName]; } @@ -540,6 +540,18 @@ export class SubjectExecutor { } } + private cloneMongoSubjectEntity(subject: Subject): ObjectLiteral { + const target: ObjectLiteral = {}; + + if (subject.entity) { + for (const column of subject.metadata.columns) { + OrmUtils.mergeDeep(target, column.getEntityValueMap(subject.entity)); + } + } + + return target; + } + /** * Soft-removes all given subjects in the database. */ @@ -551,7 +563,7 @@ export class SubjectExecutor { // for mongodb we have a bit different updation logic if (this.queryRunner instanceof MongoQueryRunner) { - const partialEntity = OrmUtils.mergeDeep({}, subject.entity!); + const partialEntity = this.cloneMongoSubjectEntity(subject); if (subject.metadata.objectIdColumn && subject.metadata.objectIdColumn.propertyName) { delete partialEntity[subject.metadata.objectIdColumn.propertyName]; } @@ -631,7 +643,7 @@ export class SubjectExecutor { // for mongodb we have a bit different updation logic if (this.queryRunner instanceof MongoQueryRunner) { - const partialEntity = OrmUtils.mergeDeep({}, subject.entity!); + const partialEntity = this.cloneMongoSubjectEntity(subject); if (subject.metadata.objectIdColumn && subject.metadata.objectIdColumn.propertyName) { delete partialEntity[subject.metadata.objectIdColumn.propertyName]; } diff --git a/src/util/OrmUtils.ts b/src/util/OrmUtils.ts index 4687b2e963..2aa851ca43 100644 --- a/src/util/OrmUtils.ts +++ b/src/util/OrmUtils.ts @@ -58,41 +58,101 @@ export class OrmUtils { }, [] as T[]); } - 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() + private static isPlainObject(item: any) { + if (item === null || item === undefined) { + return false; + } + + return !item.constructor || item.constructor === Object; + } + + private static mergeArrayKey(target: any, key: number, value: any, memo: Map) { + // Have we seen this before? Prevent infinite recursion. + if (memo.has(value)) { + target[key] = memo.get(value); + return; + } + + if (value instanceof Promise) { + // Skip promises entirely. + // This is a hold-over from the old code & is because we don't want to pull in + // the lazy fields. Ideally we'd remove these promises via another function first + // but for now we have to do it here. + return; + } + + + if (!this.isPlainObject(value) && !Array.isArray(value)) { + target[key] = value; + return; + } + + if (!target[key]) { + target[key] = Array.isArray(value) ? [] : {}; + } + + memo.set(value, target[key]); + this.merge(target[key], value, memo); + memo.delete(value); + } + + private static mergeObjectKey(target: any, key: string, value: any, memo: Map) { + // Have we seen this before? Prevent infinite recursion. + if (memo.has(value)) { + Object.assign(target, { [key]: memo.get(value) }); + return; + } + + if (value instanceof Promise) { + // Skip promises entirely. + // This is a hold-over from the old code & is because we don't want to pull in + // the lazy fields. Ideally we'd remove these promises via another function first + // but for now we have to do it here. + return; + } + + if (!this.isPlainObject(value) && !Array.isArray(value)) { + Object.assign(target, { [key]: value }); + return; + } + + if (!target[key]) { + Object.assign(target, { [key]: Array.isArray(value) ? [] : {} }); + } + + memo.set(value, target[key]); + this.merge(target[key], value, memo); + memo.delete(value); + } + + private static merge(target: any, source: any, memo: Map = new Map()): any { + if (this.isPlainObject(target) && this.isPlainObject(source)) { + for (const key of Object.keys(source)) { + this.mergeObjectKey(target, key, source[key], memo); + } + } + + if (Array.isArray(target) && Array.isArray(source)) { + for (let key = 0; key < source.length; key++) { + this.mergeArrayKey(target, key, source[key], memo); + } + } } /** * Deep Object.assign. - * - * @see http://stackoverflow.com/a/34749873 */ static mergeDeep(target: any, ...sources: any[]): any { - if (!sources.length) return target; - const source = sources.shift(); - - if (this.isObject(target) && this.isObject(source)) { - for (const key in source) { - const value = source[key]; - if (key === "__proto__" || value instanceof Promise) - continue; - - if (this.isObject(value) - && !(value instanceof Map) - && !(value instanceof Set) - && !(value instanceof Date) - && !(value instanceof Buffer) - && !(value instanceof RegExp)) { - if (!target[key]) - Object.assign(target, { [key]: Object.create(Object.getPrototypeOf(value)) }); - this.mergeDeep(target[key], value); - } else { - Object.assign(target, { [key]: value }); - } - } + if (!sources.length) { + return target; + } + + for (const source of sources) { + OrmUtils.merge(target, source); } - return this.mergeDeep(target, ...sources); + return target; } /** diff --git a/test/functional/columns/value-transformer/entity/Post.ts b/test/functional/columns/value-transformer/entity/Post.ts index 5077452d66..3daca45fca 100644 --- a/test/functional/columns/value-transformer/entity/Post.ts +++ b/test/functional/columns/value-transformer/entity/Post.ts @@ -15,6 +15,38 @@ class TagTransformer implements ValueTransformer { } +export class Complex { + x: number; + y: number; + circularReferenceToMySelf: { + complex: Complex + }; + + constructor(from: String) { + this.circularReferenceToMySelf = { complex: this }; + const [x, y] = from.split(" "); + this.x = +x; + this.y = +y; + } + + toString() { + return `${this.x} ${this.y}`; + } +} + +class ComplexTransformer implements ValueTransformer { + + to (value: Complex | null): string | null { + if (value == null) { return value; } + return value.toString(); + } + + from (value: string | null): Complex | null { + if (value == null) { return value; } + return new Complex(value); + } +} + @Entity() export class Post { @@ -27,4 +59,6 @@ export class Post { @Column({ type: String, transformer: new TagTransformer() }) tags: string[]; + @Column({ type: String, transformer: new ComplexTransformer(), nullable: true }) + complex: Complex | null; } diff --git a/test/functional/columns/value-transformer/value-transformer.ts b/test/functional/columns/value-transformer/value-transformer.ts index 02f5d49bc9..9e49b166aa 100644 --- a/test/functional/columns/value-transformer/value-transformer.ts +++ b/test/functional/columns/value-transformer/value-transformer.ts @@ -4,7 +4,7 @@ import {closeTestingConnections, createTestingConnections, reloadTestingDatabase import {Connection} from "../../../../src/connection/Connection"; import {PhoneBook} from "./entity/PhoneBook"; -import {Post} from "./entity/Post"; +import {Complex, Post} from "./entity/Post"; import {User} from "./entity/User"; importĀ {Category} from "./entity/Category"; import {View} from "./entity/View"; @@ -95,4 +95,60 @@ describe("columns > value-transformer functionality", () => { dbView && dbView.title.should.be.eql(title); }))); + + it("should marshal data using a complex value-transformer", () => Promise.all(connections.map(async connection => { + + const postRepository = connection.getRepository(Post); + + // create and save a post first + const post = new Post(); + post.title = "Complex transformers!"; + post.tags = ["complex", "transformer"]; + await postRepository.save(post); + + let loadedPost = await postRepository.findOne(post.id); + expect(loadedPost!.complex).to.eq(null); + + // then update all its properties and save again + post.title = "Complex transformers2!"; + post.tags = ["very", "complex", "actually"]; + post.complex = new Complex("3 2.5"); + await postRepository.save(post); + + // check if all columns are updated except for readonly columns + loadedPost = await postRepository.findOne(post.id); + expect(loadedPost!.title).to.be.equal("Complex transformers2!"); + expect(loadedPost!.tags).to.deep.eq(["very", "complex", "actually"]); + expect(loadedPost!.complex!.x).to.eq(3); + expect(loadedPost!.complex!.y).to.eq(2.5); + + // then update all its properties and save again + post.title = "Complex transformers3!"; + post.tags = ["very", "lacking", "actually"]; + post.complex = null; + await postRepository.save(post); + + loadedPost = await postRepository.findOne(post.id); + expect(loadedPost!.complex).to.eq(null); + + // then update all its properties and save again + post.title = "Complex transformers4!"; + post.tags = ["very", "here", "again!"]; + post.complex = new Complex("0.5 0.5"); + await postRepository.save(post); + + loadedPost = await postRepository.findOne(post.id); + expect(loadedPost!.complex!.x).to.eq(0.5); + expect(loadedPost!.complex!.y).to.eq(0.5); + + // then update all its properties and save again + post.title = "Complex transformers5!"; + post.tags = ["now", "really", "lacking!"]; + post.complex = new Complex("1.05 2.3"); + await postRepository.save(post); + + loadedPost = await postRepository.findOne(post.id); + expect(loadedPost!.complex!.x).to.eq(1.05); + expect(loadedPost!.complex!.y).to.eq(2.3); + }))); }); diff --git a/test/functional/mongodb/basic/array-columns/entity/Post.ts b/test/functional/mongodb/basic/array-columns/entity/Post.ts index 397edc0b71..4fc96c8cbf 100644 --- a/test/functional/mongodb/basic/array-columns/entity/Post.ts +++ b/test/functional/mongodb/basic/array-columns/entity/Post.ts @@ -25,10 +25,10 @@ export class Post { @Column() booleans: boolean[]; - @Column(type => Counters) + @Column(type => Counters, { array: true }) other1: Counters[]; - @Column(type => Counters) + @Column(type => Counters, { array: true }) other2: Counters[]; } \ No newline at end of file diff --git a/test/functional/mongodb/basic/array-columns/mongodb-array-columns.ts b/test/functional/mongodb/basic/array-columns/mongodb-array-columns.ts index fa38407056..1069150659 100644 --- a/test/functional/mongodb/basic/array-columns/mongodb-array-columns.ts +++ b/test/functional/mongodb/basic/array-columns/mongodb-array-columns.ts @@ -118,4 +118,25 @@ describe("mongodb > array columns", () => { }))); + it("should retrieve arrays from the column metadata", () => Promise.all(connections.map(async connection => { + const post = new Post(); + post.title = "Post"; + post.names = ["umed", "dima", "bakhrom"]; + post.numbers = [1, 0, 1]; + post.booleans = [true, false, false]; + post.counters = [ + new Counters(1, "number #1"), + new Counters(2, "number #2"), + new Counters(3, "number #3"), + ]; + post.other1 = []; + + const column = connection.getMetadata(Post) + .columns + .find(c => c.propertyPath === 'counters.text')!; + + const value = column.getEntityValue(post); + + expect(value).to.eql([ "number #1", "number #2", "number #3" ]); + }))); }); diff --git a/test/functional/mongodb/basic/mongo-repository/mongo-repository.ts b/test/functional/mongodb/basic/mongo-repository/mongo-repository.ts index 3d229cd236..11abe9579e 100644 --- a/test/functional/mongodb/basic/mongo-repository/mongo-repository.ts +++ b/test/functional/mongodb/basic/mongo-repository/mongo-repository.ts @@ -1,4 +1,5 @@ import "reflect-metadata"; +import {expect} from "chai"; import {Connection} from "../../../../../src/connection/Connection"; import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../../utils/test-utils"; import {Post} from "./entity/Post"; @@ -87,5 +88,45 @@ describe("mongodb > MongoRepository", () => { }))); // todo: cover other methods as well + it("should be able to save and update mongo entities", () => Promise.all(connections.map(async connection => { + const postRepository = connection.getMongoRepository(Post); + + // save few posts + const firstPost = new Post(); + firstPost.title = "Post #1"; + firstPost.text = "Everything about post #1"; + await postRepository.save(firstPost); + const secondPost = new Post(); + secondPost.title = "Post #2"; + secondPost.text = "Everything about post #2"; + await postRepository.save(secondPost); + + // save few posts + firstPost.text = "Everything and more about post #1"; + await postRepository.save(firstPost); + + const loadedPosts = await postRepository.find(); + + loadedPosts.length.should.be.equal(2); + loadedPosts[0].text.should.be.equal("Everything and more about post #1"); + loadedPosts[1].text.should.be.equal("Everything about post #2"); + + }))); + + it("should ignore non-column properties", () => Promise.all(connections.map(async connection => { + // Github issue #5321 + const postRepository = connection.getMongoRepository(Post); + + await postRepository.save({ + title: "Hello", + text: "World", + unreal: "Not a Column" + }); + + const loadedPosts = await postRepository.find(); + + expect(loadedPosts).to.have.length(1); + expect(loadedPosts[0]).to.not.have.property("unreal"); + }))); }); diff --git a/test/functional/mongodb/basic/repository-actions/entity/Counters.ts b/test/functional/mongodb/basic/repository-actions/entity/Counters.ts new file mode 100644 index 0000000000..90572b9487 --- /dev/null +++ b/test/functional/mongodb/basic/repository-actions/entity/Counters.ts @@ -0,0 +1,19 @@ +import {Column} from "../../../../../../src/decorator/columns/Column"; +import {Entity} from "../../../../../../src/decorator/entity/Entity"; +import {User} from "./User"; + +@Entity() +export class Counters { + + @Column({ name: "_likes" }) + likes: number; + + @Column({ name: "_comments" }) + comments: number; + + @Column({ name: "_favorites" }) + favorites: number; + + @Column(() => User) + viewer: User; +} diff --git a/test/functional/mongodb/basic/repository-actions/entity/Post.ts b/test/functional/mongodb/basic/repository-actions/entity/Post.ts index 281d15c25f..5475cfa5d3 100644 --- a/test/functional/mongodb/basic/repository-actions/entity/Post.ts +++ b/test/functional/mongodb/basic/repository-actions/entity/Post.ts @@ -1,4 +1,5 @@ import {Entity} from "../../../../../../src/decorator/entity/Entity"; +import {Counters} from "./Counters"; import {Column} from "../../../../../../src/decorator/columns/Column"; import {ObjectIdColumn} from "../../../../../../src/decorator/columns/ObjectIdColumn"; import {ObjectID} from "../../../../../../src/driver/mongodb/typings"; @@ -18,7 +19,6 @@ export class Post { @Column() index: number; - // @Column(() => Counters) - // counters: Counters; - + @Column(() => Counters) + counters: Counters; } diff --git a/test/functional/mongodb/basic/repository-actions/entity/User.ts b/test/functional/mongodb/basic/repository-actions/entity/User.ts new file mode 100644 index 0000000000..79290ca977 --- /dev/null +++ b/test/functional/mongodb/basic/repository-actions/entity/User.ts @@ -0,0 +1,9 @@ +import {Column} from "../../../../../../src/decorator/columns/Column"; +import {Entity} from "../../../../../../src/decorator/entity/Entity"; + +@Entity() +export class User { + + @Column() + name: string; +} diff --git a/test/functional/mongodb/basic/repository-actions/mongodb-repository-actions.ts b/test/functional/mongodb/basic/repository-actions/mongodb-repository-actions.ts index 7385ceeaa9..7d42fa036f 100644 --- a/test/functional/mongodb/basic/repository-actions/mongodb-repository-actions.ts +++ b/test/functional/mongodb/basic/repository-actions/mongodb-repository-actions.ts @@ -3,6 +3,8 @@ import {expect} from "chai"; import {Connection} from "../../../../../src/connection/Connection"; import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../../utils/test-utils"; import {Post} from "./entity/Post"; +import {Counters} from "./entity/Counters"; +import {User} from "./entity/User"; describe("mongodb > basic repository actions", () => { @@ -46,6 +48,32 @@ describe("mongodb > basic repository actions", () => { mergedPost.text.should.be.equal("And its text is updated as well"); }))); + it("merge should merge all given recursive partial objects into given source entity", () => Promise.all(connections.map(async connection => { + const postRepository = connection.getRepository(Post); + const counter1 = new Counters(); + counter1.likes = 5; + const counter2 = new Counters(); + counter2.likes = 2; + counter2.viewer = new User(); + counter2.viewer.name = "Hello World"; + const post = postRepository.create({ + title: "This is created post", + text: "All about this post", + counters: counter1 + }); + const mergedPost = postRepository.merge(post, + { title: "This is updated post" }, + { text: "And its text is updated as well" }, + { counters: counter2 } + ); + mergedPost.should.be.instanceOf(Post); + mergedPost.should.be.equal(post); + mergedPost.title.should.be.equal("This is updated post"); + mergedPost.text.should.be.equal("And its text is updated as well"); + mergedPost.counters.likes.should.be.equal(2); + mergedPost.counters.viewer.name.should.be.equal("Hello World"); + }))); + it("target should be valid", () => Promise.all(connections.map(async connection => { const postRepository = connection.getRepository(Post); expect(postRepository.target).not.to.be.undefined; diff --git a/test/functional/util/OrmUtils.ts b/test/functional/util/OrmUtils.ts new file mode 100644 index 0000000000..276b747aff --- /dev/null +++ b/test/functional/util/OrmUtils.ts @@ -0,0 +1,80 @@ +import { expect } from "chai"; +import { OrmUtils } from "../../../src/util/OrmUtils"; + +describe("OrmUtils.mergeDeep", () => { + it("should handle simple values.", () => { + expect(OrmUtils.mergeDeep(1, 2)).to.equal(1); + expect(OrmUtils.mergeDeep(2, 1)).to.equal(2); + expect(OrmUtils.mergeDeep(2, 1, 1)).to.equal(2); + expect(OrmUtils.mergeDeep(1, 2, 1)).to.equal(1); + expect(OrmUtils.mergeDeep(1, 1, 2)).to.equal(1); + expect(OrmUtils.mergeDeep(2, 1, 2)).to.equal(2); + }); + + it("should handle ordering and indempotence.", () => { + const a = { a: 1 }; + const b = { a: 2 }; + expect(OrmUtils.mergeDeep(a, b)).to.deep.equal(b); + expect(OrmUtils.mergeDeep(b, a)).to.deep.equal(a); + expect(OrmUtils.mergeDeep(b, a, a)).to.deep.equal(a); + expect(OrmUtils.mergeDeep(a, b, a)).to.deep.equal(a); + expect(OrmUtils.mergeDeep(a, a, b)).to.deep.equal(b); + expect(OrmUtils.mergeDeep(b, a, b)).to.deep.equal(b); + const c = { a: 3 }; + expect(OrmUtils.mergeDeep(a, b, c)).to.deep.equal(c); + expect(OrmUtils.mergeDeep(b, c, b)).to.deep.equal(b); + expect(OrmUtils.mergeDeep(c, a, a)).to.deep.equal(a); + expect(OrmUtils.mergeDeep(c, b, a)).to.deep.equal(a); + expect(OrmUtils.mergeDeep(a, c, b)).to.deep.equal(b); + expect(OrmUtils.mergeDeep(b, a, c)).to.deep.equal(c); + }); + + it("should skip nested promises in sources.", () => { + expect(OrmUtils.mergeDeep({}, { p: Promise.resolve() })).to.deep.equal({}); + expect(OrmUtils.mergeDeep({}, { p: { p: Promise.resolve() }})).to.deep.equal({ p: {} }); + const a = { p: Promise.resolve(0) }; + const b = { p: Promise.resolve(1) }; + expect(OrmUtils.mergeDeep(a, {})).to.deep.equal(a); + expect(OrmUtils.mergeDeep(a, b)).to.deep.equal(a); + expect(OrmUtils.mergeDeep(b, a)).to.deep.equal(b); + expect(OrmUtils.mergeDeep(b, {})).to.deep.equal(b); + }); + + it("should merge moderately deep objects correctly.", () => { + const a = { a: { b: { c: { d: { e: 123, h: { i: 23 } } } } }, g: 19 }; + const b = { a: { b: { c: { d: { f: 99 } }, f: 31 } } }; + const c = { a: { b: { c: { d: { e: 123, f: 99, h: { i: 23 } } }, f: 31 } }, g: 19 }; + expect(OrmUtils.mergeDeep(a, b)).to.deep.equal(c); + expect(OrmUtils.mergeDeep(b, a)).to.deep.equal(c); + expect(OrmUtils.mergeDeep(b, a, a)).to.deep.equal(c); + expect(OrmUtils.mergeDeep(a, b, a)).to.deep.equal(c); + expect(OrmUtils.mergeDeep(a, a, b)).to.deep.equal(c); + expect(OrmUtils.mergeDeep(b, a, b)).to.deep.equal(c); + }); + + it("should merge recursively deep objects correctly", () => { + let a: Record = {}; + let b: Record = {}; + + a['b'] = b; + a['a'] = a; + b['a'] = a; + + expect(OrmUtils.mergeDeep({}, a)); + }); + + it("should reference copy complex instances of classes.", () => { + class Foo { + recursive: Foo; + + constructor() { + this.recursive = this; + } + } + + const foo = new Foo(); + const result = OrmUtils.mergeDeep({}, { foo }); + expect(result).to.have.property("foo"); + expect(result.foo).to.equal(foo); + }); + }); diff --git a/test/github-issues/5762/entity/User.ts b/test/github-issues/5762/entity/User.ts new file mode 100644 index 0000000000..a4dc4b0bdd --- /dev/null +++ b/test/github-issues/5762/entity/User.ts @@ -0,0 +1,24 @@ +import {Entity} from "../../../../src/decorator/entity/Entity"; +import {PrimaryColumn, Column} from "../../../../src"; +import { URL } from "url"; + +@Entity() +export class User { + + @PrimaryColumn() + id: number; + + @Column("varchar", { + // marshall + transformer: { + from(value: string): URL { + return new URL(value); + }, + to(value: URL): string { + return value.toString(); + }, + }, + }) + url: URL; + +} diff --git a/test/github-issues/5762/issue-5762.ts b/test/github-issues/5762/issue-5762.ts new file mode 100644 index 0000000000..d3ca010f71 --- /dev/null +++ b/test/github-issues/5762/issue-5762.ts @@ -0,0 +1,39 @@ +import "reflect-metadata"; +import {expect} from "chai"; +import {Connection} from "../../../src"; +import {User} from "./entity/User"; +import {createTestingConnections, reloadTestingDatabases, closeTestingConnections} from "../../utils/test-utils"; +import { URL } from "url"; + +describe("github issues > #5762 `Using URL as a rich column type breaks", () => { + + let connections: Connection[]; + + before(async () => { + connections = await createTestingConnections({ + entities: [User], + schemaCreate: true, + dropSchema: true + }); + }); + beforeEach(() => reloadTestingDatabases(connections)); + after(() => closeTestingConnections(connections)); + + it("should allow assigning URL as a field value", () => + Promise.all(connections.map(async (connection) => { + const userRepository = connection.getRepository(User); + + const url = new URL("https://typeorm.io"); + + const user = new User(); + user.id = 1; + user.url = url; + + const promise = userRepository.save(user); + + return expect(promise).to.eventually.be.deep.equal(user) + .and.to.have.property("url").be.instanceOf(URL) + .and.to.have.nested.property("href").equal(url.href); + }))); + +});