Skip to content

Commit

Permalink
feat: support nullable embedded entities
Browse files Browse the repository at this point in the history
Support nullable embedded fields such that embedded documents in MongoDB can correctly be returned as null for a subdocument that is explicitly nullable.

Closes: typeorm#3913
  • Loading branch information
test137E29B committed Apr 14, 2024
1 parent e7649d2 commit 2b6aeb2
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 3 deletions.
2 changes: 2 additions & 0 deletions src/decorator/columns/Column.ts
Expand Up @@ -182,6 +182,8 @@ export function Column(
reflectMetadataType === Array || options.array === true,
prefix:
options.prefix !== undefined ? options.prefix : undefined,
nullable:
options.nullable !== undefined ? options.nullable : undefined,
type: typeOrOptions as (type?: any) => Function,
} as EmbeddedMetadataArgs)
} else {
Expand Down
5 changes: 5 additions & 0 deletions src/decorator/options/ColumnEmbeddedOptions.ts
Expand Up @@ -14,4 +14,9 @@ export interface ColumnEmbeddedOptions {
* This option works only in mongodb.
*/
array?: boolean

/**
* Indicates if this embedded is nullable.
*/
nullable?: boolean
}
5 changes: 5 additions & 0 deletions src/entity-schema/EntitySchemaEmbeddedColumnOptions.ts
Expand Up @@ -18,4 +18,9 @@ export class EntitySchemaEmbeddedColumnOptions {
* This option works only in mongodb.
*/
array?: boolean

/**
* Indicates if this embedded is nullable.
*/
nullable?: boolean
}
1 change: 1 addition & 0 deletions src/entity-schema/EntitySchemaTransformer.ts
Expand Up @@ -348,6 +348,7 @@ export class EntitySchemaTransformer {
target: options.target || options.name,
propertyName: columnName,
isArray: embeddedOptions.array === true,
nullable: embeddedOptions.nullable === true,
prefix:
embeddedOptions.prefix !== undefined
? embeddedOptions.prefix
Expand Down
5 changes: 5 additions & 0 deletions src/metadata-args/EmbeddedMetadataArgs.ts
Expand Up @@ -23,6 +23,11 @@ export interface EmbeddedMetadataArgs {
*/
prefix?: string | boolean

/**
* Indicates if this embedded is nullable.
*/
nullable: boolean

/**
* Type of the class to be embedded.
*/
Expand Down
6 changes: 6 additions & 0 deletions src/metadata/EmbeddedMetadata.ts
Expand Up @@ -98,6 +98,11 @@ export class EmbeddedMetadata {
*/
isArray: boolean = false

/**
* Indicates if this embedded is nullable.
*/
nullable: boolean = false

/**
* Prefix of the embedded, used instead of propertyName.
* If set to empty string or false, then prefix is not set at all.
Expand Down Expand Up @@ -185,6 +190,7 @@ export class EmbeddedMetadata {
this.propertyName = options.args.propertyName
this.customPrefix = options.args.prefix
this.isArray = options.args.isArray
this.nullable = options.args.nullable
}

// ---------------------------------------------------------------------
Expand Down
11 changes: 8 additions & 3 deletions src/query-builder/transformer/DocumentToEntityTransformer.ts
Expand Up @@ -107,9 +107,14 @@ export class DocumentToEntityTransformer {
embeddeds: EmbeddedMetadata[],
) => {
embeddeds.forEach((embedded) => {
if (!document[embedded.prefix]) return

if (embedded.isArray) {
if (document[embedded.prefix] === undefined) return
if (document[embedded.prefix] === null && !embedded.nullable) return

if (embedded.nullable && document[embedded.prefix] === null) {
// We allow this to be set to null in the case it's null in the database response
// It's processed first to ensure other embedded options can still remain nullable, like a nullable array
entity[embedded.prefix] = document[embedded.prefix]
} else if (embedded.isArray) {
entity[embedded.propertyName] = (
document[embedded.prefix] as any[]
).map((subValue: any, index: number) => {
Expand Down
20 changes: 20 additions & 0 deletions test/github-issues/3913/entity/TestMongo.ts
@@ -0,0 +1,20 @@
import { ObjectId } from "mongodb"
import {
BaseEntity,
Column,
Entity,
ObjectIdColumn
} from "../../../../src"

export class Embedded {
a: string
}

@Entity()
export class TestMongo extends BaseEntity {
@ObjectIdColumn()
_id: ObjectId

@Column(() => Embedded, { nullable: true })
embedded: Embedded | null
}
19 changes: 19 additions & 0 deletions test/github-issues/3913/entity/TestSQL.ts
@@ -0,0 +1,19 @@
import {
BaseEntity,
Column,
Entity,
PrimaryGeneratedColumn
} from "../../../../src"

export class Embedded {
a: string
}

@Entity()
export class TestSQL extends BaseEntity {
@PrimaryGeneratedColumn()
id: number

@Column(() => Embedded, { nullable: true })
embedded: Embedded | null
}
64 changes: 64 additions & 0 deletions test/github-issues/3913/issue-3913.ts
@@ -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 { TestMongo } from "./entity/TestMongo"
import { TestSQL } from "./entity/TestSQL"
import { MongoDriver } from "../../../src/driver/mongodb/MongoDriver"

describe("github issues > #3913 Cannnot 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 the embedded entity to null in the database for mongodb", () =>
Promise.all(
connections.map(async (connection) => {
if (!(connection.driver instanceof MongoDriver)) return // Only run this test for mongodb
const test = new TestMongo()
test.embedded = null

await connection.manager.save(test)

const loadedTest = await connection.manager.findOne(TestMongo, {
where: { _id: test._id },
})
expect(loadedTest).to.be.eql({
_id: test._id,
embedded: null,
})
}),
))

it("should set the embedded entity to null in the database for non mongodb", () =>
Promise.all(
connections.map(async (connection) => {
if (connection.driver instanceof MongoDriver) return // Don't run this test for mongodb
const test = new TestSQL()
test.embedded = null

await connection.manager.save(test)

const loadedTest = await connection.manager.findOne(TestSQL, {
where: { id: test.id },
})
expect(loadedTest).to.be.eql({
id: 1,
embedded: null,
})
}),
))
})

0 comments on commit 2b6aeb2

Please sign in to comment.