medusajs / medusa

Building blocks for digital commerce
https://medusajs.com
MIT License
24.67k stars 2.43k forks source link

Need a understanding around default entity API usage and its extendibility #4879

Closed bkvaiude closed 11 months ago

bkvaiude commented 1 year ago

Preliminary Checks

Issue Summary

Hello team

While exploring MedusaJS, I see that there are default entities are available for any eCommerce application

Like Products, Cart, Orders, Inventory, Payment etc.

But I'm not able to find fine way to extend Product Entity to apply filters (/store/products and /store/products/search)

I followed the documentation and looked into the code, I found that I can perform filtering on restricted fields which are maintained in FilterableProductProps type

medusajs/medusa/packages/medusa/src/types/product.ts

Possible solutions are

  1. extend the product entity and rewrite it to implement filter functionality
  2. Write a custom product endpoints and repository to build supporting functionality

Scenario

Building out a B2B website I have product with 15 options values that creates more than 100 variants combinations Also, each product have more than 50 properties or attributes Each attribute or option looks like Key:Value combination

Problem Statement 1

I want to provide filters on 15 options as well as 50+ product attributes I see store/products works with certain product properties which are maintained FilterableProductProps type Even, I discovered that I can not filter the products by multiple status like 'published' or 'draft'

search by options, for example: options.title[]=size&options.title[]=color&options.values[]=XL&&options.values[]=Blue

search by product attributes/properties, for example: color[]=Blue&size[]=XL

Problem Statement 2

I have created new entity to save the searches, I was expecting that CRUD options API will be available out of the box for newly created entity but I don't see such options

I can understand the part with logical implementation like Wishlist https://medusajs.com/blog/how-to-create-a-wishlist-with-medusa/#add-wishlist-service

But is there option available where CRUD operations like endpoints for entity?

How can this issue be resolved?

Possible solution for problem statement 1

In this context, I avoid reinventing the wheel by enhancing the current case filtering functionality. I genuinely appreciate the default entity implementation offered by MedusaJS. If I were to code the filtering feature from scratch, it might be somewhat excessive.

I haven't tried out but I see that extending the FilterableProductProps type will solve the problem

It will helpful if you provide your insights on the same with small example

End goal will be to implement following functionality, but now looking out simple approach to applying filters on multiple product props or options without reinventing the wheel

Discord Source: Show Products Documentation (https://docs.medusajs.com/modules/products/storefront/show-products#filtering-retrieved-products)

For a more advanced filtering experience, you can implement next-level filters like the ones used by Blake. Some practices they apply in their product filters are:

Possible solution for problem statement 2

Is there any possibility where we can enable/disable the basic CRUD operation API endpoints?

Are you interested in working on this issue?

shahednasser commented 1 year ago

Thank you for submitting this issue! Have you tried checking this documentation on how to extend an entity? And if so, where did you find it fell short exactly?

Regarding CRUD operations, unfortunately, that's not currently available automatically out of the box. You have to create endpoints for each of these operations. Check out this documentation on how to create an endpoint.

bkvaiude commented 1 year ago

@shahednasser I failed to do following things after extend entity documentations

  1. Failed to apply the filter on the custom field or newly added attribute, not sure how to do it
  2. Also, not able to rewrite the API response by adding a new (computed fields) or removing some existing response fields

Expectations: I need to achieve all these with without writing a new endpoint, as I want to use /store/products endpoint only I have explained the same in detail on issue description

I would need your help to find out possible correct way of implementation of same if my approach is not correct or no implementable

shahednasser commented 1 year ago
  1. Regarding the filter, if you mean that you want to pass the field as a filter to an endpoint, you extend the validator that specifies which fields are allowed. Check this documentation on how to do it.
  2. As for ensuring the field is returned in the response, you'll need to also extend the service to use your extended entity and specify your field within the returned fields. So, in this case, you would extend the ProductService and maybe extend the listAndCount method to pass to the parent your field as one of the selectors. You can learn more about extending a service here. You can also learn about the available properties and methods in the ProductService in this reference if you don't want to go through the core code.
shahednasser commented 11 months ago

Closing this issue for lack of response.

BorisKamp commented 7 months ago

Closing this issue for lack of response.

@shahednasser I would like to follow up an this excellent post by the OP.

Im in the process of adding a field invoiced_at to the order model, I did so successfully and now I want to add a filter for it so I can use it like so: {{medusa-host}}/admin/orders?invoiced_at=2024-01-01.

I see there's AdminGetOrdersParams in /node_modules/@medusajs/medusa/dist/api/routes/admin/orders/list-orders.js as well that we might need to use.

I did the following and can send the query param now, however, it is not working as results are always empty:

import { AdminGetOrdersParams as MedusaAdminGetOrdersParams } from "@medusajs/medusa/dist/api/routes/admin/orders/list-orders"
import { IsDateString, IsOptional } from "class-validator"

export class AdminGetOrdersParams extends MedusaAdminGetOrdersParams {
  @IsDateString()
  @IsOptional()
  invoiced_at: Date
}

And I registered the validator in my index file: registerOverriddenValidators(AdminGetOrdersParams)

How do I add invoiced_at as a filter with the same capabilities as the other date filters on List orders: https://docs.medusajs.com/api/admin#orders_getorders (created_at for example)

shahednasser commented 7 months ago

Hi @BorisKamp

I think your issue is different. For dates, such as created_at, the expected filter is an object (see here an example of the object's shape and here on how dates can be passed in the query)

So maybe try {{medusa-host}}/admin/orders?invoiced_at[lte]=2024-01-01

BorisKamp commented 7 months ago

Thank you for your comment @shahednasser, I agree I need to pass the param as an object like you said, but for that to be accepted, I first need to write the right validator for that.

And that is where I'm stuck, how do I write that validator so that it accepts ?invoiced_at[lte]=2024-01-01 as now it return error: invoiced_at must be a valid ISO 8601 date string.

And accepting is part one, part two is that it's actually applied to the query....

I know how to do it on a new custom endpoint, but I want it to work for the existing endpoints.

shahednasser commented 7 months ago

Actually to use the object the type of the invoiced_at filter should be DateComparisonOperator like this:

import { DateComparisonOperator } from "@medusajs/medusa"
import { AdminGetOrdersParams as MedusaAdminGetOrdersParams } from "@medusajs/medusa/dist/api/routes/admin/orders/list-orders"
import { IsDateString, IsOptional } from "class-validator"

export class AdminGetOrdersParams extends MedusaAdminGetOrdersParams {
  @IsOptional()
  invoiced_at: DateComparisonOperator
}

But that's optional, so I don't think the issue is here.

Maybe @adrien2p would know the issue here? There are certain things that I'm not very familiar with

BorisKamp commented 7 months ago

Using DateComparisonOperator as type works @shahednasser , thanks! This actually makes it work at all, the whole filter works now!

I just found out this bug https://github.com/medusajs/medusa/issues/6359 which I bump into here again, My date in the table is 2023-08-18 16:28:04.669+02, when I use ?invoiced_at[lte]=2023-08-19 it returns the order and when I use ?invoiced_at[lte]=2023-08-18 it does not, meaning lte is not lower than or equal, but lower than....

I now have one more issue left to tackle, which is how to add the invoiced_at as a default in the reponses. I have the following loader file src/loaders/extend-order-fields.ts:

export default async function () {
  const imports = (await import(
    "@medusajs/medusa/dist/api/routes/admin/orders/index"
  )) as any
  imports.allowedAdminOrdersFields = [
    ...imports.allowedAdminOrdersFields,
    "invoiced_at",
  ]
  imports.defaultAdminOrdersFields = [
    ...imports.defaultAdminOrdersFields,
    "invoiced_at",
  ]
}

defaultAdminOrdersFields seems different then the defaultStoreProductsFields from the example/docs, and my invoiced_at is not loaded by default. When I look at the /node_modules/@medusajs/medusa/dist/api/routes/admin/orders/index.js file, I do not see the default fields specified. How is this done? Would be nice if documented.

adrien2p commented 7 months ago

@BorisKamp , it is normal that ?invoiced_at[lte]=2023-08-18 does not return your data since by default if you do not specify any hours then I think it will take the first second of the day which is before 2023-08-18 16:28:04.669+02. You can run a SQL query and the result will be the same.

About the default fields, they are used in the order routes, except that you didn't look where they are imported from :) dist/types/orders.ts, should be good after that.

cc @shahednasser

BorisKamp commented 7 months ago

Thank you so much @adrien2p makes sense regarding the date filter, I learned from that.

Ah now I see (this is all quite new for me, learning a lot) that defaultAdminOrdersFields is defined in /dist/types/order.js indeed. What I did now is the following in my loader file:

export default async function () {
  const imports = (await import(
    "@medusajs/medusa/dist/api/routes/admin/orders/index"
  )) as any

  const importTypes = (await import(
    "@medusajs/medusa/dist/types/orders"
  )) as any

  imports.allowedAdminOrdersFields = [
    ...imports.allowedAdminOrdersFields,
    "invoiced_at",
    "bb_company_slug"
  ]

  importTypes.defaultAdminOrdersFields = [
    ...importTypes.defaultAdminOrdersFields,
    "invoiced_at",
    "bb_company_slug"
  ]
}

However, the fields are still not returned as default...

adrien2p commented 7 months ago

@BorisKamp no worries, the reason I think is because you are importing the route index first, which itself import the types already. I would suggest you to first import the types, modify them then import the route and modify what you need there

BorisKamp commented 7 months ago

@BorisKamp no worries, the reason I think is because you are importing the route index first, which itself import the types already. I would suggest you to first import the types, modify them then import the route and modify what you need there

Genius! Thank you so much, so happy now!

BorisKamp commented 7 months ago

@adrien2p one more related question, can we extend the core orders list route with filters that apply on relations such as where cart.customfield === 'value' or do we need a dedicated new endpoint for that?

ridham-improwised commented 3 months ago

@shahednasser @adrien2p @BorisKamp I wanted to filter the products based on options like size or color and size and color would have values like grey, XL respectively. @shahednasser as you said earlier I read the docs and i got a way to add custom field to the existing API as below

import { StoreGetProductsParams as MedusaStoreGetProductsParams } from '@medusajs/medusa/dist/api/routes/store/products/list-products';

import { IsOptional } from 'class-validator';

class StoreGetProductsParams extends MedusaStoreGetProductsParams {
  @IsOptional()
  subtitle: string;

  @IsOptional()
  options: Object;
}
registerOverriddenValidators(StoreGetProductsParams);

But im unable to find a way to pass options which has values like Size and Color further having values like grey, Xl etc. Can you guys suggest me a way to do so ?