nestjsx / crud

NestJs CRUD for RESTful APIs
https://github.com/nestjsx/crud/wiki
MIT License
4.09k stars 541 forks source link

Cannot join embedded relations - TypeOrmCrudService #533

Open paulKabira opened 4 years ago

paulKabira commented 4 years ago

First of all, I have been using this library for many projects and it reduces the development time drastically. I really appreciate the hard work you have put on this.

I have encountered an error when I try to join a relation inside embedded entity. I cannot seem to do it.


// UserDto
export class UserDto extends withId() {
 @Column()
 name: string;
 @Column(() => Family)
 family: Family;
 @OneToOne(() => UserDto)
 @JoinColumn()
 referrer: UserDto;
}

export class Family {
  @OneToOne(() => UserDto)
  @JoinColumn()
  mother: UserDto;
}

// Controller
@Crud({
  model: { type: UserDto },
  routes: {
    exclude: ['createManyBase', 'replaceOneBase'],
    ...spreadCrudPermissionInterceptors(UserDto),
  },
  query: {
    alwaysPaginate: true,
    join: {
      referrer: { eager: true, allow: ['id'] },
      'family.mother': { eager: true, allow: ['id'] }, // Embedded
    },
  }
})
@Controller(api`/user/${p => p.type}`)
export class UserController implements CrudController<UserDto> {
  constructor(public service: UserDto) {}
}

Thanks for your help.

hakimio commented 4 years ago

Duplicate of the following:

@zMotivat0r might be a good idea to add some breaking changes note in the change log?

michaelyali commented 4 years ago

@paulKabira have you tried using aliases for relations? It should do the thing

paulKabira commented 4 years ago

Yes. I have tried using alias for the relations, they are not found, this is only the case for relation inside the embedded entities.

I checked the code and found that when I join a nested embedded entity, The getRelationMetadata does not get the nested metadata if the join is in an embedded embedded relation. Since I was in a hurry, I wrote a quick and dirty fix, maybe this can help better understand the problem better. Most of the code is copied from the parent method.. Only thing is alias is always required.

export class TypeormCrudDelete<T extends { updatedBy: UserDto }> extends TypeOrmCrudService<T> {
  /**
   * Support for soft delete with auth persist until
   * the library releases one.
   * TODO: Update after release
   */
  async deleteOne(req: CrudRequest): Promise<void | T> {
    const { returnDeleted } = req.options.routes.deleteOneBase;
    const entity = await super.getOneOrFail(req);
    const deleted = await this.repo.manager.transaction(async manager => {
      const toDelete = await manager.save(Object.assign(entity, req.parsed.authPersist));
      return await manager.softRemove(toDelete);
    });
    return returnDeleted ? plainToClass(this.entityType, { ...deleted }) : undefined;
  }
  /**
   * Check if the property path is inside a embedded entity.
   * TODO: Add better check detection/Remove after issue resolved
   * @param propertyPath The enitity property path
   */
  isEmbedded(propertyPath: string) {
    const [prefix] = propertyPath.split('.');
    return this.repo.metadata.embeddeds.some(e => e.propertyName === prefix);
  }
  /**
   * Dirty fix for embedded relations.
   * NOTE: Check parent for the logic copypasta.
   * TODO: Update after issue is resolved.
   */
  getRelationMetadata(field: string, options: JoinOption): IAllowedRelation {
    const relation = super.getRelationMetadata(field, options);
    const fields = field.split('.');
    if (this.isEmbedded(field) && !relation && fields.length > 1) {
      let relationMetadata: EntityMetadata;
      let name: string;
      let path: string;
      let allowedRelation: IAllowedRelation;
      const reduced = fields.reduce(
        (res, propertyName) => {
          const found = res.relations.length
            ? res.relations.find(one => one.propertyName === propertyName)
            : null;
          const relationMetadata = found ? found.inverseEntityMetadata : null;
          const relations = relationMetadata ? relationMetadata.relations : res.relations;
          name = propertyName;
          path = found ? found.propertyPath : '';
          return {
            relations,
            relationMetadata,
          };
        },
        {
          relations: this.repo.metadata.relations,
          relationMetadata: null,
        },
      );
      relationMetadata = reduced.relationMetadata;
      if (relationMetadata) {
        const { columns, primaryColumns } = this.getEntityColumns(relationMetadata);
        allowedRelation = {
          alias: options.alias,
          name,
          path: `${this.alias}.${path}`,
          columns,
          nested: false,
          primaryColumns,
        } as IAllowedRelation;
      }
      if (allowedRelation) {
        const allowedColumns = this.getAllowedColumns(allowedRelation.columns, options);
        const toSave = { ...allowedRelation, allowedColumns };
        this.entityRelationsHash.set(field, toSave);
        if (options.alias) {
          this.entityRelationsHash.set(options.alias, toSave);
        }
        return toSave;
      }
    }
    return relation;
  }
}