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)

Commits related to this issue

Most upvoted comments

@B4nan I have added a check in the code to see whether field existed pre- and post-populate, will update you tomorrow