# Medusa Marketplace #2.2 |  Extending ProductService

**Hello everyone!**

Now that our vendors have access to their store, I can tell they are getting bored. Indeed, without any things to sell, our back office is useless, therefore we're spending this time expanding the product concept and adding a decent bit of customization.

## What is the goal here ?

The benefit of this section of the tutorial is that it allows you to apply everything you've learned in prior sections.

First, we will:

* **Create a new file in the**`models`**folder, that will extend the core entity with new properties**
    
* **Create a new migration using the TypeORM CLI**
    
* **Create a new file in the**`services`**folder that will initially only include our basic changes (**`LIFE_TIME`**,**`this.loggedInUser_`**...)**
    

Once these steps are completed, we'll get down to the essential of adding these features :

* **Create a product related to a vendor**
    
* **List the products of a vendor**
    

## Extend the Product entity

Here is where we define the new schema for the product table. In fact, we want to associate a product with a store, therefore we use the same reasoning as the User table we extended earlier :

```typescript
// src/models/product.ts
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm'

import { Product as MedusaProduct } from '@medusajs/medusa'

import { Store } from './store'

@Entity()
export class Product extends MedusaProduct {
    @Index('ProductStoreId')
    @Column({ nullable: true })
    store_id?: string

    @ManyToOne(() => Store, (store) => store.products)
    @JoinColumn({ name: 'store_id', referencedColumnName: 'id' })
    store?: Store
}
```

A relationship with the `Store` implies an update of the previously extended `Store` model:

```typescript
// src/models/store.ts
import { Entity, OneToMany } from 'typeorm'

import { Store as MedusaStore } from '@medusajs/medusa'

import { Product } from './product'
import { User } from './user'

@Entity()
export class Store extends MedusaStore {
    @OneToMany(() => User, (user) => user.store)
    members?: User[]

    @OneToMany(() => Product, (product) => product.store)
    products?: Product[]
}
```

### Create a Migration

Once the preceding parts are completed, we may create a migration to notify the database of certain changes; the command is as follows :

```apache
npx typeorm migration:create src/migrations/add-product-store-id
```

Once the migration file has been created, we can update the `up` and `down` functions like this :

```typescript
// src/migrations/...-add-product-store-id.ts

// ...
public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`ALTER TABLE "product" ADD "store_id" character varying`)
    await queryRunner.query(`CREATE INDEX "ProductStoreId" ON "product" ("store_id")`)
}

public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`DROP INDEX "public"."ProductStoreId"`)
    await queryRunner.query(`ALTER TABLE "product" DROP COLUMN "store_id"`)
}
// ...
```

Perfect, [now we can run build our server and run the migrations](https://blog.perseides.org/marketplace-lets-follow-the-recipe?t=1715156441170#heading-how-to-apply-migrations), after that, the database we should see the new column in the `product` table :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1715069489835/1f99b342-22ce-4810-96b5-6752dfc3d737.jpeg align="center")

## Adding new features

### Create a Product for a Store

To create a Product tied to a Store, we'll start by extending the `ProductService` and overriding the `create` function to force the logged-in user's `store_id` into the product before inserting it. However, you will see type problems with the input type expected on the method, therefore we will need to expand the type to add our new `store_id` property :

```typescript
// src/services/product.ts

import type { CreateProductInput as MedusaCreateProductInput } from '@medusajs/medusa/dist/types/product'
import { ProductService as MedusaProductService } from '@medusajs/medusa'
import { Lifetime } from 'awilix'

import type { User } from '../models/user'
import type { Product } from '../models/product'

// We override the type definition so it will not throw TS errors in the `create` method
type CreateProductInput = {
    store_id?: string
} & MedusaCreateProductInput

class ProductService extends MedusaProductService {
    static LIFE_TIME = Lifetime.TRANSIENT
    protected readonly loggedInUser_: User | null

    constructor(container) {
        // @ts-ignore
        super(...arguments)

        try {
            this.loggedInUser_ = container.loggedInUser
        } catch (e) {
            // avoid errors when backend first runs
        }
    }

    async create(productObject: CreateProductInput): Promise<Product> {
        if (!productObject.store_id && this.loggedInUser_?.store_id) {
            productObject.store_id = this.loggedInUser_.store_id
        }

        return await super.create(productObject)
    }
}

export default ProductService
```

Okay, at our level here, we can be sure that a User who is logged in AND has a `µ store_id` must be able to create a product for its store.

On the other hand, when a User wants to retrieve products, there are currently no constraints preventing him from seeing ONLY its store's products, let's implement this feature.

### List Products for a Store

[`ProductService.listAndCount`](https://github.com/medusajs/medusa/blob/v1.20.4/packages/medusa/src/services/product.ts#L156) is the function that retrieves products used in the back-office, therefore we'll alter it to include the logged-in user's `store_id` to retrieve only his products. However, the selector of this function does not know the `store_id` property at all, so we will expand the selector to make it aware of that new property :

```typescript
// ... rest
import { ProductSelector as MedusaProductSelector } from '@medusajs/medusa/dist/types/product'

type ProductSelector = {
    store_id?: string
} & MedusaProductSelector

class ProductService extends MedusaProductService {
    // ... rest

    async listAndCount(selector: ProductSelector, config?: FindProductConfig): Promise<[Product[], number]> {
        if (!selector.store_id && this.loggedInUser_?.store_id) {
            selector.store_id = this.loggedInUser_.store_id
        }
    
        config.select?.push('store_id')
    
        config.relations?.push('store')
    
        const products = await super.listAndCount(selector, config)
        return products
    }
}
```

You can now try it on the Admin UI or directly with the API, and you should get the behavior expected.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>You can also override others method like </strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/medusajs/medusa/blob/v1.20.4/packages/medusa/src/services/product.ts#L242" style="pointer-events: none"><code>ProductService.retrieve</code></a><strong>, </strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/medusajs/medusa/blob/v1.20.4/packages/medusa/src/services/product.ts#L620" style="pointer-events: none"><code>ProductService.update</code></a><strong> and more, depending on your needs</strong></div>
</div>

## What's Next ?

We now have the foundation; vendors can access the admin UI and create/list products associated with their store; in the next phase, we will extend entities to **allow each store to create its own shipping options.**

## Common Issues

**A product handle must be unique**

The `product.handle` might create an issue in the long term when creating a product for a store, **as it's supposed to be unique**. You can for example either add a prefix or suffix containing the `store_id` to avoid any problems :

```typescript
// src/services/product.ts
class ProductService extends MedusaProductService {
    // ... rest
    async create(productObject: CreateProductInput): Promise<Product> {
        if (!productObject.store_id && this.loggedInUser_?.store_id) {
            productObject.store_id = this.loggedInUser_.store_id
    
            // This will generate a handle for the product based on the title and store_id
            // e.g. "sunglasses-01HXVYMJF9DW..."
            const title = productObject.title.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/\s+/g, '-').toLowerCase()
            const store_id = this.loggedInUser_.store_id.replace("store_", "")
    
            productObject.handle = `${title}-${store_id}`
        }
    
        return await super.create(productObject)
    }
}
```

**I want to retrieve products from a specific store only from my storefront**

For that you'll need to create a [loader](https://docs.medusajs.com/development/loaders/overview), that will extend the default `/store/products` relations and allowed fields.

First you'll need to create a new loader in the `/src/loaders/` directory :

```typescript
// src/loaders/extend-store-products.ts
export default async function (): Promise<void> {
    const module = await import('@medusajs/medusa/dist/api/routes/store/products/index')

    Object.assign(module, {
        ...module,
        defaultStoreProductsRelations: [...module.defaultStoreProductsRelations, 'store'],
        defaultStoreProductsFields: [...module.defaultStoreProductsFields, 'store_id'],
        allowedStoreProductsRelations: [...module.allowedStoreProductsRelations, 'store'],
        allowedStoreProductsFields: [...module.allowedStoreProductsFields, 'store_id'],
    })
}
```

Then we'll extend the validators of the `/store/products` API route, the goal here is to be able to add a query parameter `/store/products?store_id=<STORE_ID>` , and the default Medusa validator for that API is not aware of the new property we wants to add.

Next step, is to create a file in the `/src/api/` folder, named specifically `index.ts` .

```typescript
// src/api/index.ts
import { registerOverriddenValidators } from "@medusajs/medusa"

// Here we are importing the original Medusa validator as an alias :
import {
   StoreGetProductsParams as MedusaStoreGetProductsParams,
} from "@medusajs/medusa/dist/api/routes/store/products/list-products"
import { IsString, IsOptional } from "class-validator"

// Here we add the new allowed property `store_id` :
class StoreGetProductsParams extends MedusaStoreGetProductsParams {
   @IsString()
   @IsOptional() // Optional of course
   store_id?: string
}

// The following function will replace the original validator by the new one.
registerOverriddenValidators(StoreGetProductsParams)
```

Now you can successfully retrieve all products from a specific store like this.

```typescript
curl http://localhost:9000/store/products?store_id=<YOUR_STORE_ID>
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The URL is just an example, replace by your own backend URL.</div>
</div>

**I have TypeScript errors !**

In the same way that we have notified the database of new changes thanks to [migrations](https://docs.medusajs.com/development/entities/migrations/create), we will have to do the same with our packages, which are not aware of new properties.

Remember to declare your types according to your needs. In our case, we only need the following types :

```typescript
// src/index.d.ts
import type { Product } from './models/product'
import type { Store } from './models/store'

declare module '@medusajs/medusa/dist/models/product' {
    interface Product {
        store_id?: string
        store?: Store
    }
}
```

## GitHub Branch

You can access the complete part's code [**here**](https://git.new/perseides-part-2-2)**.**

## Contact

You can contact me on Discord and X with the same username : [@adevinwild](https://twitter.com/adevinwild)
