Skip to content

fix: fallback to entity property if referencedColumn returns nothing#9572

Closed
akwodkiewicz wants to merge 9 commits intotypeorm:masterfrom
akwodkiewicz:debugging-9565
Closed

fix: fallback to entity property if referencedColumn returns nothing#9572
akwodkiewicz wants to merge 9 commits intotypeorm:masterfrom
akwodkiewicz:debugging-9565

Conversation

@akwodkiewicz
Copy link
Copy Markdown
Contributor

@akwodkiewicz akwodkiewicz commented Nov 23, 2022

Description of change

This PR adds a fallback mechanism to one of ColumnMetadata.getEntityValue conditional branches. It fixes #9565 -- saving an entity with an object literal foreign key.

The root cause of the linked issue is that there is no differentiation between object literal that represents an actual entity and an object literal representing a column value. This PR does not solve this, but it applies a band-aid around one of edge cases that are affected by this behaviour.

Let's assume we're performing an INSERT/save of an entity, where one of its columns is a foreign key column and this column is an object literal (for example, an opaque type boxing a string value). During the query execution, at some point there's a call to ColumnMetadata.getEntityValue. Now see the following snippet (point in history before this PR):

if (this.relationMetadata && this.referencedColumn) {
const relatedEntity =
this.relationMetadata.getEntityValue(entity)
if (
relatedEntity &&
ObjectUtils.isObject(relatedEntity) &&
!InstanceChecker.isFindOperator(relatedEntity) &&
!(typeof relatedEntity === "function") &&
!Buffer.isBuffer(relatedEntity)
) {
value = this.referencedColumn.getEntityValue(relatedEntity)
} else if (
entity[this.propertyName] &&
ObjectUtils.isObject(entity[this.propertyName]) &&
!InstanceChecker.isFindOperator(
entity[this.propertyName],
) &&
!(typeof entity[this.propertyName] === "function") &&
!Buffer.isBuffer(entity[this.propertyName]) &&
!(entity[this.propertyName] instanceof Date)
) {
value = this.referencedColumn.getEntityValue(
entity[this.propertyName],
)
} else {
value = entity[this.propertyName]
}

Eventually, execution falls into if in L806. Because our column is just the foreign key representation, but not the full entity, the relatedEntity in L807 is undefined. This means we'll skip the first nested if and start checking the else if in L809-L815. The problem is that because the column value is an object literal it passes all the predicates and we enter the branch. And in L816 we're going to call getEntityValue on the related column, passing our object literal as the entity. This is not correct, because the object literal only represents the "id", not the full entity. This call will most likely (but not always, more on that in a while) result in value being undefined, because the property to retrieve will not exist in our object literal. And because of that, the value returned eventually by the getEntityValue is going to be undefined, which causes the INSERT/save to fail.

The fix here is to assign raw entity[this.propertyName] to value whenever the this.referencedColumn.getEntityValue returns undefined. This addresses directly the scenario that I described here.

Very similar solution, but structured in a slightly different way, might be to assign entity[this.propertyName] to value almost at the end of the method (before transformation) if it's not assigned yet. I looked at the branching in this method and it looks it should work, but this might have side-effects to cases I did not look into, that's why I decided to put the fallback assignment specifically in the affected case.

There's still risk that our object literal will contain the property of name this.referencedColumn.propertyName, and then my fix will not work -- typeorm will try to assign extracted value from object literal instead of the whole object literal (so most likely the operation will fail with a TypeError during the transformation step or not fail at all, saving wrong data to the database).

To solve this problem completely, the execution should not fall into this 2nd nested branch at all -- it should go directly to the final else and use the entity[this.propertyName] value verbatim. I refactored the method a bit and named the compound predicate to better visualize what decisions are made in the branches:

/**
* Apply some heuristics to be more certain that `value` is an object representing
* another entity.
*
* This is not 100% correct -- `value` might be an object literal
* representing a column value that is **not** an entity.
*/
private isEntityLike(value: unknown): boolean {
return (
ObjectUtils.isObject(value) &&
value &&
!InstanceChecker.isFindOperator(value) &&
!Buffer.isBuffer(value) &&
!(value instanceof Date)
)
}

If there is a way to make sure that the object literal passed to getEntityValue definitely is or is not an entity and replaced isEntityLike with such check, it should solve the issue entirely.

Pull-Request Checklist

  • Code is up-to-date with the master branch
  • npm run format to apply prettier formatting
  • npm run test passes with this change
  • This pull request links relevant issues as Fixes #0000
  • There are new or updated unit tests validating the change
  • Documentation has been updated to reflect this change
  • The new commits follow conventions explained in CONTRIBUTING.md

@akwodkiewicz akwodkiewicz force-pushed the debugging-9565 branch 5 times, most recently from 8353daf to e34b8f2 Compare November 26, 2022 21:21
Both composite boolean expressions check if the value is an object.
This means that value can never be of type "function", so these checks
can be removed.
This predicate occurs a couple of times in getEntityValue method.
I tried to deduce its value (isEntityLike) from its usage.

This refactor is not 100% pure, because I incorporated

    !(val instanceof Date)

expression into the method, even though it was mentioned in 2 out of 4
places that were replaced with the method call.

Again, studying the usage of the predicate, I decided that making sure
if the value is not a Date will only make the assumption more correct in
those 2 other places, because next operations assume that the value is
an entity.
@akwodkiewicz akwodkiewicz marked this pull request as ready for review November 29, 2022 08:51
@akwodkiewicz
Copy link
Copy Markdown
Contributor Author

@pleerock, apologies for reaching to you directly, but I'd like to make sure that the approach I chose here is a correct one. If you believe that the issue should be fixed in a different way, I can try doing it from scratch, just need your guidance here.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 20, 2025

Warning

Rate limit exceeded

@gioboa has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 14 minutes and 30 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between d4f7b44 and c369237.

📒 Files selected for processing (6)
  • src/metadata/ColumnMetadata.ts (4 hunks)
  • test/github-issues/9565/entity/Post.ts (1 hunks)
  • test/github-issues/9565/entity/User.ts (1 hunks)
  • test/github-issues/9565/issue-9565.ts (1 hunks)
  • test/github-issues/9565/model/PostId.ts (1 hunks)
  • test/github-issues/9565/model/UserId.ts (1 hunks)
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Sep 20, 2025

commit: 9637703

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented Dec 1, 2025

PR Code Suggestions ✨

Latest suggestions up to 9637703

CategorySuggestion                                                                                                                                    Impact
Possible issue
Add embedded-path raw fallback

In the embedded-object path, fall back to the raw value if
referencedColumn.getEntityValue returns undefined to ensure consistent behavior
with the non-embedded path.

src/metadata/ColumnMetadata.ts [824-832]

 } else if (
     this.isEntityLike(embeddedObject[this.propertyName])
 ) {
-    value = this.referencedColumn.getEntityValue(
-        embeddedObject[this.propertyName],
-    )
+    const rawEmbeddedValue = embeddedObject[this.propertyName]
+    value = this.referencedColumn.getEntityValue(rawEmbeddedValue) ?? rawEmbeddedValue
 } else {
     value = embeddedObject[this.propertyName]
 }
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly points out that the fix for handling non-entity objects was not applied to the code path for embedded entities, which could lead to the same bug in that scenario.

Medium
Prevent functions treated as entities

Add a typeof value !== "function" check to the isEntityLike method to prevent
functions from being incorrectly treated as entities.

src/metadata/ColumnMetadata.ts [1044-1052]

 private isEntityLike(value: unknown): boolean {
     return (
         ObjectUtils.isObject(value) &&
         value &&
+        typeof value !== "function" &&
         !InstanceChecker.isFindOperator(value) &&
         !Buffer.isBuffer(value) &&
         !(value instanceof Date)
     )
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the refactoring to isEntityLike dropped a check for functions that existed in the original code, which could lead to regressions.

Medium
  • More

Previous suggestions

Suggestions up to commit c02fe94
CategorySuggestion                                                                                                                                    Impact
Possible issue
Handle falsy values correctly in fallback

Replace the nullish coalescing operator (??) with a strict check for undefined
to correctly handle null return values from getEntityValue.

src/metadata/ColumnMetadata.ts [822-845]

 if (this.isEntityLike(relatedEntityValue)) {
     value =
         this.referencedColumn.getEntityValue(relatedEntityValue)
 } else if (this.isEntityLike(rawValue)) {
-    // If `rawValue` is actually not an entity, we should assign it to `value`.
-    // FIXME
-    // There is no differentiation between a literal object representing an entity
-    // and a literal object which is the column value verbatim.
-    // This means that we will know that `rawValue` is definitely not an entity if
-    // `referencedColumn.getEntityValue` returns undefined. And that's when we should assign
-    // `rawValue` to `value.
-    // However, if `referencedColumn.getEntityValue` returns some defined value we cannot be certain
-    // that `rawValue` actually was an entity, because it might have been the case where `rawValue`
-    // object literal contained a property with exactly the same name as `referencedColumn.propertyName`.
-    // In such cases we will incorrectly assign some arbitrary object property to `value` variable,
-    // where the correct behaviour would be to assign whole `rawValue`.
-    // To resolve this bug, we need to be able to differentiate an object literal representing an entity
-    // from an object literal representing arbitrary data.
-    value =
-        this.referencedColumn.getEntityValue(rawValue) ??
-        rawValue
+    const extractedValue = this.referencedColumn.getEntityValue(rawValue)
+    value = extractedValue !== undefined ? extractedValue : rawValue
 } else {
     value = rawValue
 }
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that if getEntityValue returns null, the nullish coalescing operator (??) would incorrectly fall back to rawValue, whereas the intended behavior is to use rawValue only when getEntityValue returns undefined. The proposed change to explicitly check for undefined fixes this subtle but important bug, improving the correctness of value extraction.

Low
Suggestions up to commit bfce334
CategorySuggestion                                                                                                                                    Impact
Possible issue
Prevent incorrect property extraction from non-entities

Refine the fallback logic for rawValue to prevent incorrectly extracting a
property from a non-entity object. Add a check to ensure the value is an entity
before attempting to get the referenced column's value.

src/metadata/ColumnMetadata.ts [818-845]

 if (this.relationMetadata && this.referencedColumn) {
     const relatedEntityValue =
         this.relationMetadata.getEntityValue(entity)
 
     if (this.isEntityLike(relatedEntityValue)) {
         value =
             this.referencedColumn.getEntityValue(relatedEntityValue)
     } else if (this.isEntityLike(rawValue)) {
-        ...
-        value =
-            this.referencedColumn.getEntityValue(rawValue) ??
-            rawValue
+        const extractedValue = this.referencedColumn.getEntityValue(rawValue)
+        // Only use extracted value if rawValue has the expected entity structure
+        value = extractedValue !== undefined && rawValue.hasOwnProperty(this.referencedColumn.propertyName)
+            ? extractedValue
+            : rawValue
     } else {
         value = rawValue
     }
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a potential bug that the PR author also noted with a FIXME comment. It proposes a valid improvement to the logic, making it more robust by adding a check to prevent incorrect property extraction from non-entity objects.

Medium
General
Remove redundant null check

In the isEntityLike method, remove the redundant value && check, as
ObjectUtils.isObject(value) already handles null and undefined cases.

src/metadata/ColumnMetadata.ts [1011-1019]

 private isEntityLike(value: unknown): boolean {
     return (
         ObjectUtils.isObject(value) &&
-        value &&
         !InstanceChecker.isFindOperator(value) &&
         !Buffer.isBuffer(value) &&
         !(value instanceof Date)
     )
 }
Suggestion importance[1-10]: 4

__

Why: The suggestion correctly points out that the value && check is redundant since ObjectUtils.isObject(value) already handles null. Removing it simplifies the code and improves readability.

Low
Suggestions up to commit d314518
CategorySuggestion                                                                                                                                    Impact
Possible issue
Exclude function types from entities

Add a check for function types to the isEntityLike method to prevent functions
from being incorrectly treated as entity-like objects.

src/metadata/ColumnMetadata.ts [1011-1019]

 private isEntityLike(value: unknown): boolean {
     return (
         ObjectUtils.isObject(value) &&
         value &&
+        typeof value !== "function" &&
         !InstanceChecker.isFindOperator(value) &&
         !Buffer.isBuffer(value) &&
         !(value instanceof Date)
     )
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the refactoring to isEntityLike dropped a check for function types that was present in the original code, which could lead to regressions.

Medium

@coveralls
Copy link
Copy Markdown

coveralls commented Dec 1, 2025

Coverage Status

coverage: 81.418% (-0.001%) from 81.419%
when pulling 9637703 on akwodkiewicz:debugging-9565
into f47246c on typeorm:master.

@akwodkiewicz
Copy link
Copy Markdown
Contributor Author

I got notified about some commotion in this PR so I looked at it again, and I'm wondering if anybody with better understanding of the codebase can propose a better solution than my "isEntityLike". Or at least run it against Opus 4.5 or GPT 5.2 to point out the flaws of the approach I proposed.

It was a long time ago when I wrote this and I did not continue development of TypeORM at all, so I understand this PR much less than I did when I first wrote it.

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented Feb 24, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (2) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Test added under github-issues 📘 Rule violation ⛯ Reliability
Description
This issue fix adds a new test under test/github-issues/9565 instead of the functional test suite.
This violates the guideline to keep issue fixes in test/functional unless there is a clear reason.
Code

test/github-issues/9565/issue-9565.ts[R15-45]

+describe("github issues > #9565 Foreign key property transformation processes undefined value upon save", () => {
+    let dataSources: DataSource[]
+    before(async () => {
+        dataSources = await createTestingConnections({
+            entities: [path.join(__dirname, "entity", "*.{ts,js}")],
+            schemaCreate: true,
+            dropSchema: true,
+        })
+    })
+    beforeEach(() => reloadTestingDatabases(dataSources))
+    after(() => closeTestingConnections(dataSources))
+
+    it("should save entity constructed with object foreign key property", () =>
+        Promise.all(
+            dataSources.map(async (dataSource) => {
+                const user = dataSource.manager.create(User, {
+                    id: new UserId("1234"),
+                    name: "Tom",
+                })
+                await dataSource.manager.save(user)
+
+                const post = dataSource.manager.create(Post, {
+                    id: new PostId("first"),
+                    authorId: new UserId("1234"), // specifying `authorId` but not `author`
+                })
+
+                return expect(dataSource.manager.save(post)).to.eventually.be
+                    .fulfilled
+            }),
+        ))
+})
Evidence
PR Compliance ID 3 requires issue fixes to live in the functional test suite; the PR introduces a
new issue-specific test file under test/github-issues/9565 with no justification indicated in the
diff.

Rule 3: Prefer functional tests over per-issue tests
test/github-issues/9565/issue-9565.ts[15-45]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new regression test for an issue fix was added under `test/github-issues/9565`, but the compliance requirement prefers adding/updating tests under `test/functional` for issue fixes.
## Issue Context
The new test file is currently located in the per-issue test folder and should be migrated into the functional test suite (or provide a clear reason if it must remain in `test/github-issues`).
## Fix Focus Areas
- test/github-issues/9565/issue-9565.ts[15-45]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Branded ID runtime ReferenceError 🐞 Bug ✓ Correctness
Description
PostId/UserId declare an ambient unique symbol but also use it as a computed class field key
with an initializer; the identifier has no runtime value, so constructing these classes will throw
(tests will fail before exercising the ORM change).
Code

test/github-issues/9565/model/UserId.ts[R1-5]

+declare const type: unique symbol
+
+export class UserId {
+    [type]: true
+    constructor(public readonly value: string) {}
Evidence
Both ID classes declare type with declare (no runtime value) and then use [type]: true (a
runtime computed key assignment). With useDefineForClassFields: false, class field initializers
are emitted as constructor assignments, requiring type to exist at runtime.

test/github-issues/9565/model/UserId.ts[1-6]
test/github-issues/9565/model/PostId.ts[1-6]
tsconfig.json[1-27]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`PostId` / `UserId` use an ambient `unique symbol` as a computed property key with a runtime initializer (`[type]: true`). Since `declare const` is erased, `type` has no runtime value and object construction can throw.
### Issue Context
These classes are instantiated in the new regression test, so this can fail the test suite before the ORM fix is validated.
### Fix Focus Areas
- test/github-issues/9565/model/UserId.ts[1-6]
- test/github-issues/9565/model/PostId.ts[1-6]
- tsconfig.json[1-27]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Verbose FIXME comment block 📘 Rule violation ⛯ Reliability
Description
The change introduces a long FIXME/explanatory comment block inside getEntityValue, which adds
noise and is inconsistent with keeping changes minimal. This may reduce maintainability and
reviewability in a core metadata path.
Code

src/metadata/ColumnMetadata.ts[R855-871]

+                    // If `rawValue` is actually not an entity, we should assign it to `value`.
+                    // FIXME
+                    // There is no differentiation between a literal object representing an entity
+                    // and a literal object which is the column value verbatim.
+                    // This means that we will know that `rawValue` is definitely not an entity if
+                    // `referencedColumn.getEntityValue` returns undefined. And that's when we should assign
+                    // `rawValue` to `value.
+                    // However, if `referencedColumn.getEntityValue` returns some defined value we cannot be certain
+                    // that `rawValue` actually was an entity, because it might have been the case where `rawValue`
+                    // object literal contained a property with exactly the same name as `referencedColumn.propertyName`.
+                    // In such cases we will incorrectly assign some arbitrary object property to `value` variable,
+                    // where the correct behaviour would be to assign whole `rawValue`.
+                    // To resolve this bug, we need to be able to differentiate an object literal representing an entity
+                    // from an object literal representing arbitrary data.
+                    value =
+                        this.referencedColumn.getEntityValue(rawValue) ??
+                        rawValue
Evidence
PR Compliance ID 4 requires avoiding AI-generated noise such as extra comments and abnormal
defensive additions; the PR adds an extended in-code narrative and FIXME block in a hot path
rather than a concise comment or an issue reference.

Rule 4: Remove AI-generated noise
src/metadata/ColumnMetadata.ts[855-871]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A long `FIXME` and multi-line narrative comment block was added in `getEntityValue`, which violates the "no AI-generated noise" guideline and increases code clutter in a core path.
## Issue Context
The logic change may be valid, but the commentary should be concise and consistent with the surrounding file style (prefer a short note and/or an issue reference).
## Fix Focus Areas
- src/metadata/ColumnMetadata.ts[855-871]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. No fallback in embedded branch 🐞 Bug ✓ Correctness
Description
The new FK-value fallback (referencedColumn.getEntityValue(rawValue) ?? rawValue) was only added
for the non-embedded path. For embedded columns, value can still become undefined when the
embedded FK value is an object-like wrapper that lacks the referenced property, keeping the original
failure mode for embedded join columns.
Code

src/metadata/ColumnMetadata.ts[R825-828]

+                        this.isEntityLike(embeddedObject[this.propertyName])
                   ) {
                       value = this.referencedColumn.getEntityValue(
                           embeddedObject[this.propertyName],
Evidence
In the embedded branch, when embeddedObject[this.propertyName] is considered “entity-like”, the
code assigns only referencedColumn.getEntityValue(...) without a nullish fallback. In the
non-embedded branch, the exact same pattern now includes ?? rawValue, so embedded and non-embedded
behavior diverge for the same input shape.

src/metadata/ColumnMetadata.ts[815-832]
src/metadata/ColumnMetadata.ts[845-878]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Embedded columns with relations can still return `undefined` when the embedded join-column value is an object-like wrapper (same scenario as #9565), because the embedded branch lacks the newly-added nullish fallback.
### Issue Context
Non-embedded path now does:
`referencedColumn.getEntityValue(rawValue) ?? rawValue`
But embedded path still does:
`value = referencedColumn.getEntityValue(embeddedObject[this.propertyName])`
### Fix Focus Areas
- src/metadata/ColumnMetadata.ts[815-832]
- src/metadata/ColumnMetadata.ts[845-878]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Copy link
Copy Markdown
Collaborator

@gioboa gioboa left a comment

Choose a reason for hiding this comment

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

@akwodkiewicz thanks for opening this.
Based on your message, I'm going on with the fix in this PR #12042 💪

@gioboa gioboa closed this Feb 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Foreign key property transformation processes undefined value upon save

4 participants