nestjsx / crud

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

Create relations between entities when POSTing #672

Open MarcMogdanz opened 3 years ago

MarcMogdanz commented 3 years ago

I don't really find anything in the issues/wiki regarding how to validate and create relations on POSTing a resource.

In my example I have an entity Product which has a One-To-Many relation to a Company (one company has multiple products). When I create a product (POST /products) I want to add a field like company or companyId to the body which contains the Id of the company that it should be linked to. My example below is my current implementation which is not type-safe but validates that the company exists and passes the company id down to TypeORM which then linkes them both together.

product.entity.ts:

@Entity()
export class Product extends BaseEntity {
  @PrimaryGeneratedColumn("uuid")
  id: string;

  @Column()
  @IsString()
  name: string;

  @IsNotEmpty()
  @IsUUID()
  @Type(t => Company)
  @ManyToOne(
    () => Company,
    company => company.products,
  )
  company: Company;
}

company.entity.ts:

@Entity()
export class Company extends BaseEntity {
  @PrimaryGeneratedColumn("uuid")
  id: string;

  @Column()
  name: string;

  @OneToMany(
    () => Product,
    product => product.company,
  )
  products: Product[];
}

product.controller.ts:

@Crud({
  model: {
    type: Product,
  },
  routes: {
    only: [
      "getOneBase",
      "replaceOneBase",
      "deleteOneBase",
      "getManyBase",
      "createOneBase",
    ],
  },
  params: {
    ...defaultParamsOptions,
  },
  dto: {
    create: Product,
  },
  query: {
    join: {
      // include company in response
      company: {
        eager: true,
        required: true,
      },
    },
  },
})
@ApiTags("Product")
@Controller("products")
export class ProductController implements CrudController<Product> {
  constructor(
    public service: ProductService,
    public companyService: CompanyService,
  ) {}

  get base(): CrudController<Product> {
    return this;
  }

  @Override()
  async createOne(
    @ParsedRequest() req: CrudRequest,
    @ParsedBody() dto: Product,
  ): Promise<Product> {
    // dto.company contains the company id supplied in the request
    // it does NOT contain the company object from the DB
    // so we check if it exists and if not throw an error
    // TypeORM will handle it correctly
    const company = await this.companyService.findOne(dto.company);

    if (!company) {
      throw new NotFoundException("Company not found");
    }

    return this.base.createOneBase(req, dto);
  }
}

Example request:

> POST /products HTTP/1.1

{
  "name": "test product",
  "company": "4b848311-c4c3-4fbb-ac27-9a190fa49141"
}

Response:

{
  "createdAt": "2021-02-13T14:31:24.437Z",
  "updatedAt": "2021-02-13T14:31:24.437Z",
  "id": "707921a9-dd4e-45d0-b05b-a67d52180f74",
  "name": "test product",
  "company": {
    "createdAt": "2021-02-13T00:11:04.746Z",
    "updatedAt": "2021-02-13T00:11:04.749Z",
    "id": "4b848311-c4c3-4fbb-ac27-9a190fa49141",
    "name": "test company"
  }
}

The problem is now that in ProductController.createOne() the second parameter is a DeepPartial<Product> and therefor the company property would be of type Company but in my example request in the body the company field would be the Id of the company. Is there any better way to create a relation and verify that the to-be-relation resource is existing?