mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
feat: nullable embedded entities (#10289)
* feat: nullable embedded entities * fix: ignore embedded columns if not selected * test: update tests with changed behaviour * test: replace integer with varchar for cockroachdb * docs: add documentation * fix: nested embedded entities * test: update nested embedded entities behaviour
This commit is contained in:
parent
5c28154cbe
commit
e67d704138
@ -165,3 +165,26 @@ All columns defined in the `Name` entity will be merged into `user`, `employee`
|
||||
This way code duplication in the entity classes is reduced.
|
||||
You can use as many columns (or relations) in embedded classes as you need.
|
||||
You even can have nested embedded columns inside embedded classes.
|
||||
|
||||
## Nullable embedded entities
|
||||
|
||||
When all its columns values are `null`, the embedded entity itself is considered `null`.
|
||||
Saving the embedded entity as `null` will set all its columns to `null`
|
||||
|
||||
```typescript
|
||||
export class Name {
|
||||
@Column({ nullable: true })
|
||||
first: string | null
|
||||
|
||||
@Column({ nullable: true })
|
||||
last: string | null
|
||||
}
|
||||
|
||||
const student = new Student()
|
||||
student.faculty = 'Faculty'
|
||||
student.name = { first: null, last: null } // same as student.name = null
|
||||
await dataSource.manager.save(student)
|
||||
|
||||
// this will return the student name as `null`
|
||||
await dataSource.getRepository(Student).findOne()
|
||||
```
|
||||
|
||||
@ -800,6 +800,9 @@ export class ColumnMetadata {
|
||||
} else {
|
||||
value = embeddedObject[this.propertyName]
|
||||
}
|
||||
} else if (embeddedObject === null) {
|
||||
// when embedded object is null, set all its properties to null
|
||||
value = null
|
||||
}
|
||||
} else {
|
||||
// no embeds - no problems. Simply return column name by property name of the entity
|
||||
|
||||
@ -757,6 +757,10 @@ export class EntityMetadata {
|
||||
const relation = this.findRelationWithPropertyPath(propertyPath)
|
||||
if (relation && relation.joinColumns) return relation.joinColumns
|
||||
|
||||
// try to find a relation with a property path being an embedded entity
|
||||
const embedded = this.findEmbeddedWithPropertyPath(propertyPath)
|
||||
if (embedded) return embedded.columns
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
@ -251,9 +251,41 @@ export class RawSqlResultsToEntityTransformer {
|
||||
// we don't mark it as has data because if we will have all nulls in our object - we don't need such object
|
||||
hasData = true
|
||||
})
|
||||
// Set all embedded column values to null if all their child columns are null
|
||||
if (entity) {
|
||||
metadata.embeddeds.forEach((embedded) => {
|
||||
if (embedded.propertyName in entity) {
|
||||
entity[embedded.propertyName] = this.deeplyNullify(
|
||||
entity[embedded.propertyName],
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
return hasData
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether an object is an iterrable
|
||||
* This is useful when trying to avoid objects such as Dates
|
||||
*/
|
||||
private isIterrableObject(obj: any): obj is object {
|
||||
const prototype = Object.prototype.toString.call(obj)
|
||||
return prototype === "[object Object]" || prototype === "[object Array]"
|
||||
}
|
||||
|
||||
/**
|
||||
* Deeply nullify an object if all its properties values are null or undefined
|
||||
*/
|
||||
private deeplyNullify<T>(obj: T): T | null {
|
||||
if (!this.isIterrableObject(obj)) return obj
|
||||
|
||||
for (const key in obj) {
|
||||
obj[key] = this.deeplyNullify(obj[key] as any)
|
||||
}
|
||||
const nullify = Object.values(obj).every((value) => value == null)
|
||||
return nullify ? null : obj
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms joined entities in the given raw results by a given alias and stores to the given (parent) entity
|
||||
*/
|
||||
|
||||
@ -33,10 +33,7 @@ describe("github issues > #300 support of embeddeds that are not set", () => {
|
||||
expect(loadedRace).to.exist
|
||||
expect(loadedRace!.id).to.exist
|
||||
loadedRace!.name.should.be.equal("National Race")
|
||||
expect(loadedRace!.duration).to.exist
|
||||
expect(loadedRace!.duration.minutes).to.be.null
|
||||
expect(loadedRace!.duration.hours).to.be.null
|
||||
expect(loadedRace!.duration.days).to.be.null
|
||||
expect(loadedRace!.duration).to.be.null
|
||||
}),
|
||||
))
|
||||
})
|
||||
|
||||
31
test/github-issues/3913/entity/Test.ts
Normal file
31
test/github-issues/3913/entity/Test.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
BaseEntity,
|
||||
} from "../../../../src"
|
||||
|
||||
export class NestedEmbedded {
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
c: string | null
|
||||
}
|
||||
|
||||
export class Embedded {
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
a: string | null
|
||||
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
b: string | null
|
||||
|
||||
@Column(() => NestedEmbedded)
|
||||
nested: NestedEmbedded | null
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class Test extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column(() => Embedded)
|
||||
embedded: Embedded | null
|
||||
}
|
||||
64
test/github-issues/3913/issue-3913.ts
Normal file
64
test/github-issues/3913/issue-3913.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import "reflect-metadata"
|
||||
import { expect } from "chai"
|
||||
import {
|
||||
closeTestingConnections,
|
||||
createTestingConnections,
|
||||
reloadTestingDatabases,
|
||||
} from "../../utils/test-utils"
|
||||
import { DataSource } from "../../../src/data-source/DataSource"
|
||||
import { Test } from "./entity/Test"
|
||||
|
||||
describe("github issues > #3913 Cannot set embedded entity to null", () => {
|
||||
let connections: DataSource[]
|
||||
before(
|
||||
async () =>
|
||||
(connections = await createTestingConnections({
|
||||
entities: [__dirname + "/entity/*{.js,.ts}"],
|
||||
cache: {
|
||||
alwaysEnabled: true,
|
||||
},
|
||||
})),
|
||||
)
|
||||
beforeEach(() => reloadTestingDatabases(connections))
|
||||
after(() => closeTestingConnections(connections))
|
||||
|
||||
it("should set all embedded columns to null when entity is set to null", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const qb = connection.createQueryBuilder(Test, "s")
|
||||
const t = new Test()
|
||||
|
||||
t.embedded = { a: "a", b: "b", nested: { c: "c" } }
|
||||
const { id } = await connection.manager.save(t)
|
||||
const t1 = await qb.getOne()
|
||||
expect(t1!.embedded).to.deep.equal({
|
||||
a: "a",
|
||||
b: "b",
|
||||
nested: { c: "c" },
|
||||
})
|
||||
|
||||
t!.embedded = null
|
||||
t!.id = id
|
||||
await connection.manager.save(t)
|
||||
const t2 = await connection
|
||||
.createQueryBuilder(Test, "s")
|
||||
.getOne()
|
||||
expect(t2!.embedded).to.equal(null)
|
||||
|
||||
await qb
|
||||
.update()
|
||||
.set({ embedded: { a: "a", b: null } })
|
||||
.execute()
|
||||
const t3 = await qb.getOne()
|
||||
expect(t3!.embedded).to.deep.equal({
|
||||
a: "a",
|
||||
b: null,
|
||||
nested: null,
|
||||
})
|
||||
|
||||
await qb.update().set({ embedded: null }).execute()
|
||||
const t4 = await qb.getOne()
|
||||
expect(t4!.embedded).to.equal(null)
|
||||
}),
|
||||
))
|
||||
})
|
||||
@ -108,13 +108,7 @@ describe("github issues > #762 Nullable @Embedded inside @Embedded", () => {
|
||||
loadedFoo3!.should.be.eql({
|
||||
id: 3,
|
||||
name: "Apple3",
|
||||
metadata: {
|
||||
bar: null,
|
||||
child: {
|
||||
something: null,
|
||||
somethingElse: null,
|
||||
},
|
||||
},
|
||||
metadata: null,
|
||||
})
|
||||
}),
|
||||
))
|
||||
@ -132,13 +126,7 @@ describe("github issues > #762 Nullable @Embedded inside @Embedded", () => {
|
||||
loadedFoo!.should.be.eql({
|
||||
id: 1,
|
||||
name: "Orange",
|
||||
metadata: {
|
||||
bar: null,
|
||||
child: {
|
||||
something: null,
|
||||
somethingElse: null,
|
||||
},
|
||||
},
|
||||
metadata: null,
|
||||
})
|
||||
}),
|
||||
))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user