vendure
vendure copied to clipboard
[Regression] v1.6.3 introduces entity__customFields error with custom field relations
Describe the bug
In v1.6.3 I made commit https://github.com/vendure-ecommerce/vendure/commit/9834225 which was an attempt to solve #1636. Unfortunately that commit introduced a regression when defining custom field relations on certain ChannelAware entities:
TypeORMError: "entity__customFields" alias was not found. Maybe you forgot to join it?
To Reproduce
Repro instructions courtesy of @vrosa:
I'm getting the same entity__customFields error after upgrading to v1.6.3. It fails when trying to edit a product in the Admin UI - GetProductWithVariants query. I've added 2 custom fields to Product, one of type "relation", which is public, and a non-public "number".
config.customFields.Product.push({
name: 'vendor',
label: [{ languageCode: LanguageCode.en_AU, value: 'Vendor' }],
type: 'relation',
entity: Vendor,
eager: true,
nullable: false,
defaultValue: null,
ui: {
component: 'cp-product-vendor-selector',
},
});
config.customFields.Product.push({
name: 'shopifyId',
type: 'float',
public: false,
});
Vendor is a custom entity that has a one-to-many relationship with Product:
@OneToMany(() => Product, (product) => product.customFields.vendor)
products: Product[];
query
query GetProductWithVariants(
$id: ID!
$variantListOptions: ProductVariantListOptions
) {
product(id: $id) {
...ProductDetail
variantList(options: $variantListOptions) {
items {
...ProductVariant
__typename
}
totalItems
__typename
}
__typename
}
}
fragment ProductDetail on Product {
id
createdAt
updatedAt
enabled
languageCode
name
slug
description
featuredAsset {
...Asset
__typename
}
assets {
...Asset
__typename
}
translations {
id
languageCode
name
slug
description
__typename
}
optionGroups {
...ProductOptionGroup
__typename
}
facetValues {
id
code
name
facet {
id
name
__typename
}
__typename
}
channels {
id
code
__typename
}
customFields {
vendor {
id
createdAt
updatedAt
name
__typename
}
__typename
}
__typename
}
fragment ProductOptionGroup on ProductOptionGroup {
id
createdAt
updatedAt
code
languageCode
name
translations {
id
languageCode
name
__typename
}
__typename
}
fragment Asset on Asset {
id
createdAt
updatedAt
name
fileSize
mimeType
type
preview
source
width
height
focalPoint {
x
y
__typename
}
__typename
}
fragment ProductVariant on ProductVariant {
id
createdAt
updatedAt
enabled
languageCode
name
price
currencyCode
priceWithTax
stockOnHand
stockAllocated
trackInventory
outOfStockThreshold
useGlobalOutOfStockThreshold
taxRateApplied {
id
name
value
__typename
}
taxCategory {
id
name
__typename
}
sku
options {
...ProductOption
__typename
}
facetValues {
id
code
name
facet {
id
name
__typename
}
__typename
}
featuredAsset {
...Asset
__typename
}
assets {
...Asset
__typename
}
translations {
id
languageCode
name
__typename
}
channels {
id
code
__typename
}
customFields {
__typename
}
__typename
}
fragment ProductOption on ProductOption {
id
createdAt
updatedAt
code
languageCode
name
groupId
translations {
id
languageCode
name
__typename
}
__typename
}
{
"id": "5",
"variantListOptions": {
"take": 10
}
}
Expected behavior No error
Environment (please complete the following information):
- @vendure/core version: 1.6.3
- Nodejs version: any
- Database (mysql/postgres etc): any?
I attempted to reproduce based on Vinicius' repro steps, resulting in the following plugin:
import { LanguageCode, PluginCommonModule, Product, VendureEntity, VendurePlugin } from '@vendure/core';
import gql from 'graphql-tag';
import { Column, Entity, OneToMany } from 'typeorm';
@Entity()
class Vendor extends VendureEntity {
constructor(input: Partial<Vendor>) {
super(input);
}
@Column()
name: string;
@OneToMany(() => Product, product => (product.customFields as any).vendor)
products: Product[];
}
const schema = gql`
type Vendor implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
name: String!
}
`;
@VendurePlugin({
imports: [PluginCommonModule],
entities: [Vendor],
shopApiExtensions: { schema, resolvers: [] },
adminApiExtensions: { schema, resolvers: [] },
configuration: config => {
config.customFields.Product.push({
name: 'vendor',
label: [{ languageCode: LanguageCode.en_AU, value: 'Vendor' }],
type: 'relation',
entity: Vendor,
eager: true,
nullable: false,
defaultValue: null,
ui: {
component: 'cp-product-vendor-selector',
},
});
config.customFields.Product.push({
name: 'shopifyId',
type: 'float',
public: false,
});
return config;
},
})
export class Test1664Plugin {}
However, when using this plugin and opening the product detail page in the Admin UI, I do not see the error. Tried without and with a related Vendor assigned to the Product. Also tried in MariaDB and Postgres.
@vrosa, @Draykee are either of you able to modify this plugin to cause the error to show?
@michaelbromley It took me a while but I found the trigger:
- import { LanguageCode, PluginCommonModule, Product, VendureEntity, VendurePlugin } from '@vendure/core';
+ import { LanguageCode, PluginCommonModule, Product, Translation, VendureEntity, VendurePlugin } from '@vendure/core';
import gql from 'graphql-tag';
import { Column, Entity, OneToMany } from 'typeorm';
@Entity()
class Vendor extends VendureEntity {
constructor(input: Partial<Vendor>) {
super(input);
}
+
+ description: LocaleString;
@Column()
name: string;
@OneToMany(() => Product, product => (product.customFields as any).vendor)
products: Product[];
+
+ @OneToMany(() => VendorTranslation, (translation) => translation.base, { eager: true })
+ translations: Array<Translation<Vendor>>;
}
+
+ @Entity()
+ export class VendorTranslation extends VendureEntity implements Translation<Vendor> {
+ constructor(input?: Partial<Translation<Vendor>>) {
+ super(input);
+ }
+
+ @Column('varchar')
+ languageCode: LanguageCode;
+
+ @Column('text')
+ description: string;
+
+ @ManyToOne(() => Vendor, (vendor) => vendor.translations, { onDelete: 'CASCADE' })
+ base: Vendor;
+ }
const schema = gql`
type Vendor implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
name: String!
+ description: String!
}
`;
@VendurePlugin({
imports: [PluginCommonModule],
- entities: [Vendor],
+ entities: [Vendor, VendorTranslation],
shopApiExtensions: { schema, resolvers: [] },
adminApiExtensions: { schema, resolvers: [] },
configuration: config => {
config.customFields.Product.push({
name: 'vendor',
label: [{ languageCode: LanguageCode.en_AU, value: 'Vendor' }],
type: 'relation',
entity: Vendor,
eager: true,
nullable: false,
defaultValue: null,
ui: {
component: 'cp-product-vendor-selector',
},
});
config.customFields.Product.push({
name: 'shopifyId',
type: 'float',
public: false,
});
return config;
},
})
export class Test1664Plugin {}
Seems like the problem happens when the entity associated to a custom field has relationships of its own and loads those relations eagerly. Unfortunately I haven't found a fix but FindOptionsUtils.joinEagerRelations
is what throws the error.
Oh my that was a tough one. I've implemented a "fix" - more of a work-around of a TypeORM issue for now. Will release a patch with this in the coming day or so.
Awesome! Even though I discovered this bug I wasn't really able to pinpoint the exact reason. Thanks, I'm already looking forward for the next patch!
There is still an error for nested relations.
Hi @Draykee, are you able to modify the test plugin above (source here) to reproduce that error? And also include the minimal query that reproduces it?
I think you just need to add
config.customFields.User.push({ // and then resolve customer -> user -> customFields -> vendor
name: 'vendor',
label: [{ languageCode: LanguageCode.en_AU, value: 'Vendor' }],
type: 'relation',
entity: Vendor,
eager: true,
nullable: false,
defaultValue: null
});
But I'm not too sure on how to use this test plugin within the dev-server directory
I finally found one of the errors. I have the relation Product -> User -> Profile (-> Asset) and I need it to get eagerly loaded to access the properties for the elasticsearch indexing.
// Product
config.customFields.Product.push({
name: 'owner',
nullable: true,
type: 'relation',
entity: User,
public: false,
eager: true, // needs to be eager to enable indexing of user->profile attributes like name, etc.
readonly: true,
});
// User
config.customFields.User.push({
name: 'profile',
type: 'relation',
entity: Profile,
nullable: true,
public: false,
internal: false,
readonly: true,
eager: true, // needs to be eager to enable indexing of profile attributes like name, etc.
});
With profile:
@Entity('profile')
export class Profile extends VendureEntity {
constructor(input?: DeepPartial<Profile>) {
super(input);
}
/**
* The reference to a user
*/
@ManyToOne(() => User, user => (user as any).profileId, { onDelete: 'CASCADE' })
user: User;
/**
* Profile display name
*/
@Column()
name: string;
/**
* The profile picture
*/
@OneToOne(() => ProfileAsset, profileAsset => profileAsset.profile, {
onDelete: 'SET NULL',
nullable: true,
})
@JoinColumn()
featuredAsset: ProfileAsset;
/**
* Other assets
*/
@OneToMany(() => ProfileAsset, profileAsset => profileAsset.profile, {
onDelete: 'CASCADE',
})
assets: ProfileAsset[];
}
@Entity()
export class ProfileAsset extends VendureEntity {
constructor(input?: DeepPartial<ProfileAsset>) {
super(input);
}
@OneToOne(() => Asset, { eager: true, onDelete: 'CASCADE' })
@JoinColumn()
asset: Asset;
@ManyToOne(() => Profile, { onDelete: 'CASCADE' })
profile: Profile;
}
For indexing I want this data to be present:
ownerProfileName: {
graphQlType: 'String!',
valueFn: (product: Product) => (product.customFields as any).owner.customFields.profile.name,
},
Basically every time I try to eagerly load the User I get the error.
Hi @michaelbromley , try this plugin to reproduce the error when accessing an order detail page. I used PostgreSQL and received an error like this:
I still believe it may have something to do with same relations being eagerly loaded:
- Product -> User -> Profile -> Asset
- Order -> User -> Profile -> Asset
Here is the updated plugin code:
import { OnApplicationBootstrap } from '@nestjs/common';
import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
import {
Asset,
Channel,
CustomOrderFields,
CustomProductFields,
Order,
OrderService,
PluginCommonModule,
Product, RequestContext,
TransactionalConnection,
User,
VendurePlugin,
} from '@vendure/core';
import gql from 'graphql-tag';
import { ProfileAsset } from './profile-asset.entity';
import { Profile } from './profile.entity';
declare module '@vendure/core' {
interface CustomOrderFields {
productOwner: User;
}
interface CustomProductFields {
owner: User;
}
}
const schema = gql`
type Profile implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
name: String!
user: User!
}
`;
/**
* Test plugin for https://github.com/vendure-ecommerce/vendure/issues/1664
*
* Test query:
* ```graphql
* query {
* product(id: 1) {
* name
* customFields {
* owner {
* id
* identifier
* customFields {
* profile {
* id
* name
* }
* }
* }
* }
* }
* }
* ```
*/
@VendurePlugin({
imports: [PluginCommonModule],
entities: () => [Profile, ProfileAsset],
shopApiExtensions: { schema, resolvers: [] },
adminApiExtensions: { schema, resolvers: [] },
configuration: config => {
// Order
config.customFields.Order.push({
name: 'productOwner', // because orders are always c2c (and should be stored redundant in case product get's deleted)
nullable: true,
type: 'relation',
entity: User,
public: false,
eager: true,
readonly: true,
});
config.customFields.Product.push({
name: 'owner',
nullable: true,
type: 'relation',
entity: User,
public: false,
eager: true, // needs to be eager to enable indexing of user->profile attributes like name, etc.
readonly: true,
});
// User
config.customFields.User.push({
name: 'profile',
type: 'relation',
entity: Profile,
nullable: true,
public: false,
internal: false,
readonly: true,
eager: true, // needs to be eager to enable indexing of profile attributes like name, etc.
});
return config;
},
})
export class Test1664Plugin implements OnApplicationBootstrap {
constructor(private connection: TransactionalConnection, private orderService: OrderService) {}
async onApplicationBootstrap() {
await this.createDummyProfiles();
await this.createDummyOrder();
}
async createDummyProfiles() {
const profilesCount = await this.connection.rawConnection.getRepository(Profile).count();
if (0 < profilesCount) {
return;
}
// Create a Profile and assign it to all the products
const users = await this.connection.rawConnection.getRepository(User).find();
// tslint:disable-next-line:no-non-null-assertion
const user = users[1]!;
const profile = await this.connection.rawConnection.getRepository(Profile).save(
new Profile({
name: 'Test Profile',
user,
}),
);
(user.customFields as any).profile = profile;
await this.connection.rawConnection.getRepository(User).save(user);
const asset = await this.connection.rawConnection.getRepository(Asset).findOne(1);
if (asset) {
const profileAsset = this.connection.rawConnection.getRepository(ProfileAsset).save({
asset,
profile,
});
}
const products = await this.connection.rawConnection.getRepository(Product).find();
for (const product of products) {
(product.customFields as any).owner = user;
await this.connection.rawConnection.getRepository(Product).save(product);
}
}
async createDummyOrder() {
const orderCount = await this.connection.rawConnection.getRepository(Order).count();
if (0 < orderCount) {
return;
}
const defaultChannel = await this.connection.getRepository(Channel).findOne({
relations: ['defaultShippingZone', 'defaultTaxZone'],
where: {
code: DEFAULT_CHANNEL_CODE,
},
});
if (!defaultChannel) {
throw new Error(`Channel with code ${DEFAULT_CHANNEL_CODE} could not be found.`);
}
const ctx = new RequestContext({
apiType: 'shop',
authorizedAsOwnerOnly: false,
channel: defaultChannel,
isAuthorized: true,
languageCode: defaultChannel.defaultLanguageCode,
});
// Create order
const users = await this.connection.rawConnection.getRepository(User).find();
// tslint:disable-next-line:no-non-null-assertion
const customer = users[1]!;
const created = await this.orderService.create(ctx, customer.id);
// Add products
const products = await this.connection.rawConnection.getRepository(Product).find({relations: ['variants']});
const product = products[0];
await this.orderService.addItemToOrder(ctx, created.id, product.variants[0].id, 1);
// Add the product owner to order
const productOwner = product.customFields.owner;
await this.orderService.updateCustomFields(ctx, created.id, { productOwner })
}
}
With that last commit, it seems this can be closed. If anyone still encounters this after v1.8.5, please comment below.
In Vendure 1.9.1 the following plugin causes the same error when you try to open the product page in the Admin (GetProductWithVariants)
Using the @RelationId
decorator causes the error. If you remove that, it works fine.
import { Asset, DeepPartial, LanguageCode, PluginCommonModule, VendureEntity, VendurePlugin } from '@vendure/core';
import { gql } from 'apollo-server-core';
import { Entity, ManyToOne, RelationId } from 'typeorm';
@Entity('dropme_widget')
export class Widget extends VendureEntity {
constructor(input?: DeepPartial<Widget>) {
super(input);
}
@ManyToOne(() => Asset, { nullable: true })
asset: Asset | null;
@RelationId((widget: Widget) => widget.asset)
assetId: string;
}
export const schema = gql`
type Widget {
asset: Asset!
}
`;
declare module '@vendure/core/dist/entity/custom-entity-fields' {
interface CustomProductFields {
widget: Widget | null;
}
}
@VendurePlugin({
imports: [PluginCommonModule],
providers: [],
entities: [Widget],
controllers: [],
adminApiExtensions: { schema },
shopApiExtensions: { schema },
configuration: config => {
config.customFields.Product.push({
name: 'widget',
public: true,
type: 'relation',
entity: Widget,
nullable: true,
description: [{ languageCode: LanguageCode.en, value: 'Widget' }],
label: [{ languageCode: LanguageCode.en, value: 'Widget' }],
});
return config;
},
})
export class WidgetErrorPlugin {
constructor() {}
}
Possibly related:
- https://github.com/typeorm/typeorm/issues/9944