Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): self-referencing relations Not unique table/alias #2740

Merged
merged 4 commits into from
Mar 18, 2024

Conversation

monrostar
Copy link
Contributor

@monrostar monrostar commented Mar 15, 2024

Description

This code bypasses the typeorm bug related to an error in duplicate aliases when linking to a parent table
Related issue: #2738

Breaking changes

Does this PR include any breaking changes we should be aware of?

  • no

Checklist

📌 Always:

  • I have set a clear title
  • My PR is small and contains a single feature
  • I have checked my own PR

👍 Most of the time:

  • I have added or updated test cases
  • I have updated the README if needed

Comment on lines 699 to 711
if (!currentParentIsTreeType && !currentMetadataIsTreeType) {
let currentMetadataHasOneOrMoreSelfReferencingRelations = false;
// Check if the current entity has one or more self-referencing relations
// to determine if it is a tree type or has tree relations.
for (const relation of currentMetadata.relations) {
if (relation.inverseEntityMetadata === currentMetadata) {
currentMetadataHasOneOrMoreSelfReferencingRelations = true;
break;
}
}

if (!currentParentIsTreeType && !currentMetadataIsTreeType && !currentMetadataHasOneOrMoreSelfReferencingRelations) {
return;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the most important code. It is enough to determine whether it is necessary to do join manually or leave this job to typeorm

Comment on lines +147 to +152

@ManyToOne(() => TestEntity, (type) => type.parent)
parent: TestEntity | null;

@Column('int', { nullable: true })
parentId: ID | null;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here I had to update the model a bit to repeat the error and take it into account in the tests

Copy link
Contributor

Choose a reason for hiding this comment

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

above fix i think need consider more behavior
if we have translations like

  @OneToMany(
    () => LensProcessOptionTranslation,
    (translation) => translation.base,
    { eager: true }
  )
  translations: Array<Translation<LensProcessOption>>;

the parent should be auto load translations otherwise it will result translateEntity failed

function translateLeaf(
    object: { [key: string]: any } | undefined,
    property: string,
    languageCode: LanguageCode | [LanguageCode, ...LanguageCode[]],
): any {
    if (object && object[property]) {
        if (Array.isArray(object[property])) {
            return object[property].map((nested2: any) => translateEntity(nested2, languageCode));
        } else if (object[property]) {
            return translateEntity(object[property], languageCode);
        }
    }
}

if parent existed but it have auto loaded translations it will broken here

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

InternalServerError [GraphQLError]: error.entity-has-no-translation-in-language
    at new I18nError (/Users/tianyingchun/Documents/coding/kzfoo/vendure-issue/node_modules/@vendure/core/dist/i18n/i18n-error.js:21:9)
    at new InternalServerError (/Users/tianyingchun/Documents/coding/kzfoo/vendure-issue/node_modules/@vendure/core/dist/common/error/errors.js:15:9)
    at translateEntity (/Users/tianyingchun/Documents/coding/kzfoo/vendure-issue/node_modules/@vendure/core/dist/service/helpers/utils/translate-entity.js:33:15)
    at translateLeaf (/Users/tianyingchun/Documents/coding/kzfoo/vendure-issue/node_modules/@vendure/core/dist/service/helpers/utils/translate-entity.js:104:20)
    at translateDeep (/Users/tianyingchun/Documents/coding/kzfoo/vendure-issue/node_modules/@vendure/core/dist/service/helpers/utils/translate-entity.js:89:21)
    at TranslatorService.translate (/Users/tianyingchun/Documents/coding/kzfoo/vendure-issue/node_modules/@vendure/core/dist/service/helpers/translator/translator.service.js:54:53)
    at file:///Users/tianyingchun/Documents/coding/kzfoo/vendure-issue/packages/plugin-issue/src/services/menu.service.ts:82:76
    at Array.map (<anonymous>)
    at file:///Users/tianyingchun/Documents/coding/kzfoo/vendure-issue/packages/plugin-issue/src/services/menu.service.ts:82:37
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  path: undefined,
  locations: undefined,
  extensions: { code: 'INTERNAL_SERVER_ERROR' },
  variables: { entityName: 'Menu', languageCode: ',,en' },
  code: 'INTERNAL_SERVER_ERROR',
  logLevel: 0
}

Copy link
Contributor Author

@monrostar monrostar Mar 16, 2024

Choose a reason for hiding this comment

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

Interesting. I thought I had accounted for translations for all self-related relations. I will look into this and add join for eager relations

above fix i think need consider more behavior if we have translations like

  @OneToMany(
    () => LensProcessOptionTranslation,
    (translation) => translation.base,
    { eager: true }
  )
  translations: Array<Translation<LensProcessOption>>;

the parent should be auto load translations otherwise it will result translateEntity failed

function translateLeaf(
    object: { [key: string]: any } | undefined,
    property: string,
    languageCode: LanguageCode | [LanguageCode, ...LanguageCode[]],
): any {
    if (object && object[property]) {
        if (Array.isArray(object[property])) {
            return object[property].map((nested2: any) => translateEntity(nested2, languageCode));
        } else if (object[property]) {
            return translateEntity(object[property], languageCode);
        }
    }
}

if parent existed but it have auto loaded translations it will broken here

@monrostar
Copy link
Contributor Author

monrostar commented Mar 16, 2024

@michaelbromley
10c0798
Added a fix. And refactored the code a bit.

I'm not sure if we should look for translation in nested objects automatically because that would lead to uncontrolled execution of this method.

translateDeep works fine.

@tianyingchun
Copy link
Contributor

tianyingchun commented Mar 17, 2024

oh it seems that this change also have some issue here if we have relation comes from inherits entity consider hehavior as bellow:

// module.entity.ts
@Entity('module')
@TableInheritance({ column: { type: 'varchar', name: 'type' } })
export class Module extends VendureEntity implements ModuleChannelAware {
  constructor(input?: DeepPartial<Module>) {
    super(input);
  }

  @Column()
  type: string;


  name: LocaleString;

  @OneToMany(() => ModuleAcl, (moduleAcl) => moduleAcl.module)
  moduleAcls: ModuleAcl[];


  @ManyToMany(() => ModuleChannel)
  @JoinTable()
  channels: ModuleChannel[];

  @OneToMany(() => ModuleTranslation, (translation) => translation.base, {
    eager: true,
  })
  translations: Array<Translation<Module>>;
}
// module-translation.entity.ts


@Entity('module_translation')
export class ModuleTranslation
  extends VendureEntity
  implements Translation<Module>
{
  constructor(input?: DeepPartial<Translation<Module>>) {
    super(input);
  }

  @Column('varchar')
  languageCode: LanguageCode;

  @Column()
  name: string;

  @ManyToOne(() => Module, (base) => base.translations, {
    onDelete: 'CASCADE',
  })
  base: Relation<Module>;
}
// module-menu.entity.ts

@ChildEntity('module_menu')
export class ModuleMenu extends Module {
  constructor(input: DeepPartial<ModuleMenu>) {
    super(input);
  }


  @Column({ default: false })
  invisible: boolean;

  @Column({ type: 'varchar', nullable: true })
  iconImgActive: string | null;

  @Column({ default: '_self' })
  target: string;

  @TreeParent()
  parent: ModuleMenu | null;

  @Column('int', { nullable: true })
  parentId: ID | null;

  @TreeChildren()
  children: ModuleMenu[];
}
// module-acl.entity.ts
@Entity('module_acl')
export class ModuleAcl extends VendureEntity {
  constructor(input?: DeepPartial<ModuleAcl>) {
    super(input);
  }

  aclTagName: LocaleString;


  @ManyToOne(() => Module)
  module: Relation<Module>;

  @Column('int')
  moduleId: ID;


  @OneToMany(() => ModuleAclTranslation, (translation) => translation.base, {
    eager: true,
  })
  translations: Array<Translation<ModuleAcl>>;
}
// module-acl-translation.entity.ts

@Entity('module_acl_translation')
export class ModuleAclTranslation
  extends VendureEntity
  implements Translation<ModuleAcl>
{
  constructor(input?: DeepPartial<Translation<ModuleAcl>>) {
    super(input);
  }

  @Column('varchar')
  languageCode: LanguageCode;

  @Column({ unique: true })
  aclTagName: string;

  @ManyToOne(() => ModuleAcl, (base) => base.translations, {
    onDelete: 'CASCADE',
  })
  base: Relation<ModuleAcl>;
}
// module-channel.entity.ts
@Entity('module_channel')
export class ModuleChannel extends VendureEntity {
  constructor(input?: DeepPartial<ModuleChannel>) {
    super(input);
  }

  @Column({ unique: true })
  code: string;

  @Column({ unique: true })
  token: string;

  description: LocaleString;


  @OneToMany(
    () => ModuleChannelTranslation,
    (translation) => translation.base,
    {
      eager: true,
    }
  )
  translations: Array<Translation<ModuleChannel>>;
}
// module-channel-translation.entity.ts

@Entity('module_channel_translation')
export class ModuleChannelTranslation
  extends VendureEntity
  implements Translation<ModuleChannel>
{
  constructor(input?: DeepPartial<Translation<ModuleChannel>>) {
    super(input);
  }

  @Column('varchar')
  languageCode: LanguageCode;


  @Column({ default: '', nullable: true })
  description: string;

  @ManyToOne(() => ModuleChannel, (base) => base.translations, {
    onDelete: 'CASCADE',
  })
  base: Relation<ModuleChannel>;
}
  public async findAll(
    ctx: RequestContext,
    options?: ListQueryOptions<ModuleMenu>,
  ) {
    return this.listQueryBuilder
      .build(ModuleMenu, options, {
        ctx,
        relations: ['moduleAcls', 'channels']
      })
      .getManyAndCount()
      .then(([modules, totalItems]) => {
        const items = modules.map((moduleItem) =>
          this.translator.translate(moduleItem, ctx, ['moduleAcls', 'channels'])
        );

        return {
          items,
          totalItems,
        };
      });
  }

it will lose translations property on channels or moduleAcls if they have records it will throw

InternalServerError [GraphQLError]: error.entity-has-no-translation-in-language
...
  path: undefined,
  locations: undefined,
  extensions: { code: 'INTERNAL_SERVER_ERROR' },
  variables: { entityName: 'ModuleChannel', languageCode: ',,en' },
  code: 'INTERNAL_SERVER_ERROR',
  logLevel: 0
image

@monrostar
Copy link
Contributor Author

Can you also provide the missing code from ModuleChannelAware?
At the moment I can see that there might be an error due to missing ReverseSide for relations, but I guess that can be bypassed too.

export class Module extends VendureEntity implements ModuleChannelAware {

@tianyingchun
Copy link
Contributor

tianyingchun commented Mar 17, 2024

/**
 * @description
 * Entities which can be assigned to Module Channels should implement this interface.
 */
export interface ModuleChannelAware {
  channels: ModuleChannel[];
}

@monrostar
Copy link
Contributor Author

@michaelbromley
I think the attempt to fix the next bug should be done in a separate PR because there are too many changes here already

In this PR, the first bug has been fixed. I'll open a new PR from #2744

@michaelbromley michaelbromley merged commit 357ba49 into vendure-ecommerce:minor Mar 18, 2024
9 of 12 checks passed
@michaelbromley
Copy link
Member

@monrostar yep, makes sense to split the other changes.

@tianyingchun thank you very much for the ongoing feedback!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants