mikro-orm: Sometimes calling em.populate does not init a relation
Describe the bug
Sometimes calling em.populate does not init a relation
Stack trace
Cannot read properties of undefined (reading 'getItems') <- happens to 1:M / M:N relations
or
Cannot return null from non-nullable field <- GraphQL server throws it, happens to 1:1 relations
To Reproduce Steps to reproduce the behavior: Unfortunately I was not able to reliably reproduce this behaviour. Mostly it works for us but typically a few times per day we get this error in the same parts of the code. Given its unstable nature I would suspect that there is a race condition somewhere. If needed, I’ll be happy to add some debugging info into our code to get more information from runtime exceptions.
We operate a GraphQL API and relevant entities look like the following:
1:N / M:N
abstract class BaseEntity {
@PrimaryKey({ length: 25, type: 'string' })
id = cuid()
@Property({
type: 'Date',
fieldName: 'createdAt',
})
createdAt = new Date()
@Property({
type: 'Date',
fieldName: 'updatedAt',
onUpdate: () => new Date(),
})
updatedAt = new Date()
}
@Entity({ collection: 'Trait' })
class Trait extends BaseEntity {
[OptionalProps]?: 'createdAt' | 'updatedAt'
@OneToMany({ entity: 'Translation', mappedBy: 'trait' })
translations = new Collection<Translation>(this)
@OneToMany({ entity: 'Translation', mappedBy: 'traitDescription' })
descriptions = new Collection<Translation>(this)
}
enum LOCALE {
DE = 'DE',
EN = 'EN',
FR = 'FR',
ES = 'ES',
PT = 'PT',
}
@Entity({ collection: 'Translation' })
class Translation extends BaseEntity {
[OptionalProps]?: 'createdAt' | 'updatedAt'
@Enum({ items: () => LOCALE, type: 'LOCALE' })
locale!: LOCALE
@Property({ columnType: 'text', type: 'string' })
text!: string
@ManyToOne({
entity: () => Trait,
fieldName: 'trait',
onDelete: 'cascade',
nullable: true,
})
trait?: Trait
@ManyToOne({
entity: () => Trait,
fieldName: 'traitDescription',
onDelete: 'cascade',
nullable: true,
})
traitDescription?: Trait
}
async function pseudoGraphQL() {
const traits = await em.find(Trait, {})
const getTranslation = async (parent: Trait) => {
await em.populate(parent, ['translations'])
const translation = parent.translations
.getItems()
.find(({ locale }) => locale === 'EN')
return translation?.text
}
const getDescription = async (parent: Trait) => {
await em.populate(parent, ['descriptions'])
const description = parent.descriptions
.getItems()
.find(({ locale }) => locale === 'EN')
return description?.text
}
await Promise.all(
traits.map(traits => {
return Promise.all([getTranslation(traits), getDescription(traits)])
})
)
}
1:1
@Entity({ collection: 'Company' })
class Company extends BaseEntity {
[OptionalProps]?: 'createdAt' | 'updatedAt'
@OneToOne({ entity: 'Image', mappedBy: 'companyLogo' })
logo!: Image
@OneToOne({ entity: 'Image', mappedBy: 'companyImage' })
image!: Image
}
@Entity({ collection: 'Image' })
class Image extends BaseEntity {
[OptionalProps]?: 'createdAt' | 'updatedAt'
@Property({ columnType: 'text', type: 'string' })
url!: string
@OneToOne({ entity: 'Company', nullable: true, onDelete: 'cascade' })
companyLogo?: Company
@OneToOne({ entity: 'Company', nullable: true, onDelete: 'cascade' })
companyImage?: Company
}
async function pseudoGraphQL() {
const companies = await em.find(Company, {})
const getLogo = async (parent: Company) => {
await em.populate(parent, ['logo'])
if (!parent.logo) {
throw new Error(
'Cannot return null for non-nullable field Company.logo'
)
}
return parent.logo
}
const getImage = async (parent: Company) => {
await em.populate(parent, ['image'])
if (!parent.image) {
throw new Error(
'Cannot return null for non-nullable field Company.image'
)
}
return parent.image
}
await Promise.all(
companies.map(company => {
return Promise.all([getLogo(company), getImage(company)])
})
)
}
Expected behavior
Calling em.populate does not result in a property being undefined
Additional context One thing that comes to my mind is that for both these cases entities are connected twice through different fields:
<--(logo)--> Image
Company
<--(image)--> Image
<--(translations)--> Translation[]
Trait
<--(descriptions)--> Translation[]
Versions
| Dependency | Version |
|---|---|
| node | 16.16.0 |
| typescript | 4.4.4 |
| mikro-orm | 5.3.1 |
| postgresql | 5.3.1 |
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Comments: 37 (20 by maintainers)
@B4nan I have added a check in the code to see whether field existed pre- and post-populate, will update you tomorrow