Extending Entit" /> Extending Entit" /> Extending Entit"/>

Unable to update field in Medusa JS

458 Views Asked by At

I am working on Medusa JS and need to add a custom field in Core Model of "Cart" that means extending an Entity. So following guide -> Extending Entity I was able to do everything. On updating the value of newly created field of cart using Store API I am not getting any error that means validation is also working fine, but the problem is field is not getting updated in DB.

Step-2 (as per documentation): Extending the Cart Model

import { Column, Entity } from "typeorm";
import {
  // alias the core entity to not cause a naming conflict
  Cart as MedusaCart,
} from "@medusajs/medusa";

@Entity()
export class Cart extends MedusaCart {
  @Column()
  is_subscribed?: boolean;
}

Step-3: Creating Typescript Declaration File

export declare module "@medusajs/medusa/dist/models/order" {
    declare interface Order {
        is_subscribed: boolean;
    }
}

Extending Validation (Using Update Cart Validation)

import { registerOverriddenValidators } from "@medusajs/medusa";
import { StorePostCartsCartReq as MedusaStorePostCartsCartReq } from "@medusajs/medusa/dist/api/routes/store/carts/update-cart";
import { IsBoolean, IsOptional } from "class-validator";

class StorePostCartsCartReq extends MedusaStorePostCartsCartReq {
  @IsOptional()
  @IsBoolean()
  is_subscribed?: boolean;
}

registerOverriddenValidators(StorePostCartsCartReq);

I need to update the value of the custom field so that I can create a subscriber on the basis of this value. That I can only do when I am able to modify the value of this attribute using Update store API.

1

There are 1 best solutions below

0
user23665755 On

I believe you also have to also extend the repo and the services (I assume you ran a migration) that pertain to the cart. I solved this issue after a long time of messing around and looking into the node modules. Here is my solution that I posted in a thread on Github:

I have solved this issue after a long time of messing around with everything I could and looking into the node modules. I'll show you what my files look like. I wanted to extend the prices (MoneyAmount entity) array within the variants array which is nested in the products. So when I POST a new product, the new custom attributes should be present in the prices array in the return and updated in the db for in the MoneyAmount Entity.

Extension of MoneyAmount:

import { Column, Entity } from "typeorm"
import {
  // alias the core entity to not cause a naming conflict
  MoneyAmount as MedusaMoneyAmount,
} from "@medusajs/medusa"

enum PurchaseType {
    OneTime = "one_time",
    Recurring = "recurring"
  }

enum AggregrateUsageType{
LastDuringPeriod="last_during_period",
LastEver="last_ever",
Max="max",
Sum="sum"
}

enum IntervalType{
    Day = "day",
    Week = "week",
    Month = "month",
    Year = "year"
  }

enum UsageTypeType{
  Metered="metered",
  Licensed="licensed"
}

@Entity()
export class MoneyAmount extends MedusaMoneyAmount {
  @Column({
    type:"enum",
    enum:PurchaseType,
    default: PurchaseType.OneTime, // Set a default value if needed
    nullable:true
  })
  type: PurchaseType
 
  @Column({
    type: "jsonb",
    nullable: true,
  })
  recurring: {
    aggregate_usage: null | AggregrateUsageType;
    interval: IntervalType;
    interval_count: number;
    trial_period_days: null | number;
    usage_type: UsageTypeType;
  };
}

Migration:

import { MigrationInterface, QueryRunner,TableColumn } from "typeorm";

export class MoneyAmountExtension1709761619941 implements MigrationInterface {

    public async up(queryRunner: QueryRunner): Promise<void> {  await queryRunner.addColumn(
        "money_amount", 
        new TableColumn({
            name: "type",
            type: "enum",
            enum: ["one_time", "recurring"], 
            default: "'one_time'", 
            isNullable:true,
        })
    )

    await queryRunner.addColumn(
        "money_amount",
        new TableColumn({
            name: "recurring",
            type: "jsonb",
            isNullable: true,
        })
    )
    }

    public async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.dropColumn("money_amount", "type");
        await queryRunner.dropColumn("money_amount", "recurring");
    }

}

Loader file to allow return of custom attributes:

export default async function () {
    const imports = (await import(
      "@medusajs/medusa/dist/api/routes/store/products/index"
    )) as any
    imports.allowedStoreProductsFields = [
      ...imports.allowedStoreProductsFields,
      "variants__prices.type",
      "variants__prices.recurring"
    ]
    imports.defaultStoreProductsFields = [
      ...imports.defaultStoreProductsFields,
      "variants__prices.type",
      "variants__prices.recurring"
    ]
  }

Extending Repository of MoneyAmount (Notice that it actually is different form the DOCS as following the way the docs showed resulted in an error. Refer to this thread which I followed https://github.com/medusajs/medusa/issues/6139):

import { MoneyAmount } from "@medusajs/medusa";
import { dataSource } from "@medusajs/medusa/dist/loaders/database";
import { MoneyAmountRepository as MedusaMoneyAmountRepository } from "@medusajs/medusa/dist/repositories/money-amount";

export const MoneyAmountRepository:any = dataSource
  .getRepository(MoneyAmount)
  // .extend({
  //  ...Object.assign(MedusaMoneyAmountRepository, {
  //     target: MoneyAmount
  //   }),
  // });
// this causes "moneyAmountRepo.deleteVariantPricesNotIn is not a function\nCannot read properties of undefined (reading 'length')" but is in DOCS

  .extend(
    Object.assign(MedusaMoneyAmountRepository, {
       target: MoneyAmount
     }),
   );

export default MoneyAmountRepository;

Overriding validators:

import { registerOverriddenValidators } from "@medusajs/medusa"
import {AdminPostProductsReq as MedusaAdminPostProductsReq} from "@medusajs/medusa/dist/api/routes/admin/products/create-product"
import{AdminPostProductsProductVariantsReq as MedusaAdminPostProductsProductVariantsReq} from "@medusajs/medusa/dist/api/routes/admin/products/create-variant"
import{ ProductVariantPricesCreateReq as MedusaProductVariantPricesCreateReq }from "@medusajs/medusa/dist/types/product-variant"
import { IsString,ValidateNested,IsArray,IsEnum,IsObject,IsOptional } from "class-validator"
import {Type} from "class-transformer"

class AdminPostProductsReq extends MedusaAdminPostProductsReq {
@Type(() => AdminPostProductsProductVariantsReq)
@ValidateNested({ each: true })
@IsArray()
variants: AdminPostProductsProductVariantsReq[];
}

class AdminPostProductsProductVariantsReq extends MedusaAdminPostProductsProductVariantsReq {
 @Type(() => ProductVariantPricesCreateReq)
 @ValidateNested({ each: true })
 @IsArray()
 prices: ProductVariantPricesCreateReq[];
}

enum PurchaseType {
 OneTime = "one_time",
 Recurring = "recurring"
}

enum AggregrateUsageType{
 LastDuringPeriod="last_during_period",
 LastEver="last_ever",
 Max="max",
 Sum="sum"
 }
 
 enum IntervalType{
     Day = "day",
     Week = "week",
     Month = "month",
     Year = "year"
   }
 
 enum UsageTypeType{
   Metered="metered",
   Licensed="licensed"
 }

class ProductVariantPricesCreateReq extends MedusaProductVariantPricesCreateReq{
@IsOptional()
 @IsEnum(PurchaseType)
 type:PurchaseType

 @IsOptional()
 @IsObject()
 recurring:{
  aggregate_usage: null | AggregrateUsageType;
  interval: IntervalType;
  interval_count: number;
  trial_period_days: null | number;
  usage_type: UsageTypeType;
 }
}

registerOverriddenValidators(AdminPostProductsReq)
// registerOverriddenValidators(AdminPostProductsProductVariantsReq)
// post request goes through without overriding AdminPostProductsProductVariantsReq

// registerOverriddenValidators(ProductVariantPricesCreateReq) 
// post request goes through without overriding ProductVariantPricesCreateReq)

Extending Product Variants Services (Creation of a product calls on product variants services):


import { ProductVariantService as MedusaProductVariantService } from "@medusajs/medusa";
import { Lifetime } from "awilix";
import MoneyAmountRepository from "src/repositories/money-amount";
import { CreateProductVariantInput as MedusaCreateProductVariantInput } from "@medusajs/medusa/dist/types/product-variant";
import { ProductVariantPrice as MedusaProductVariantPrice } from "@medusajs/medusa/dist/types/product-variant";
import ProductVariantRepository from "@medusajs/medusa/dist/repositories/product-variant";

type ProductVariantPrice={
  type?:string,
  recurring?:object
} & MedusaProductVariantPrice

type CreateProductVariantInput={
  prices:ProductVariantPrice[]
} & MedusaCreateProductVariantInput[]

class ProductVariantService extends MedusaProductVariantService {
  static LIFE_TIME = Lifetime.SCOPED;
  protected readonly moneyAmountRepository_: typeof MoneyAmountRepository;
  protected readonly productVariantRepository_: typeof ProductVariantRepository;

  constructor(container, options) {
    // @ts-expect-error prefer-rest-params
    super(...arguments);
    this.moneyAmountRepository_ = container.moneyAmountRepository;
    this.productVariantRepository_ =container.productVariantRepository
  }

  async create(productOrProductId, variants: CreateProductVariantInput): Promise<any> {

    const variantsWithExtras = variants.map(variant => {

      const pricesWithExtras = variant.prices.map((price:ProductVariantPrice) => ({
        ...price,
        type: price.type || null,
        recurring: price.recurring || null
      }));
      // console.log(pricesWithExtras)

      return {
        ...variant,
        prices: pricesWithExtras
      };
    });

    // variantsWithExtras.forEach(variant => console.log(variant));

    const createdVariants:any = await super.create(productOrProductId, variantsWithExtras);

    this.updateVariantPrices(variantsWithExtras.map(function (v:any) { 
      console.log(`this is v:`,v)
      console.log(`this is createdvariantsid`,createdVariants[0].id)
      console.log(`this is v.prices:`,v.prices)
      return ({
      variantId: createdVariants[0].id,
      prices: v.prices,
  });
}))

    return createdVariants;
  }
}

export default ProductVariantService;

I want to also include that I was trying to get this to work while working on Github code spaces and my files looked exactly like the ones I have shown you for a long time and it would not input the custom attribute values to the db I have supplied in the POST body, rather it would just leave it as the default values. After I used up all the hours they allowed me I had to export the changes I made in the code space to a new branch and clone the GitHub repo to VS code and work on there. I ran npm i twice because the first failed for some reason, and then instead of using region_id in my post request body I switched it to currency-code (for some reason the region_id in the POST body caused an error when sending the request). Then it worked to my amazement.

FIGURED OUT CUASE OF ISSUE HERE: Turns out you do not need to extend the repo or the service to have it to work. I figured out what was causing the issue. Replacing region_id to currency_code was the fix. In the docs it states for a POST request when crating a product, the prices array needs to have EITHER region_id or currency_code (having both results in a XOR error). I'm not really sure why that is the fix as the Money Amount (prices) table has columns for both currency_code and region_id. My guess is that when you create a product on the admin dashboard with both variants and prices, the data inserted in the Money Amount table has a currency_code value but the region_id value is empty. So by only having currency_code and not region_id in your request body, you follow how the admin dashboards creates data for the Money Amount table. Again this is an assumption for why region_id creates the issue and currency_code does not.