Medusa is an open source headless commerce platform that allows you to create your own store in a matter of minutes. Part of what makes Medusa a good choice for your ecommerce store is its extensibility. Now, it is also possible to create multi-vendor marketplaces using Medusa.
To make things easier for our open source community, Adrien de Peretti, one of our amazing contributors, created a Medusa module that allows you to extend anything and everything you want.
"I've been looking for an e-commerce solution that could provide me with some core features while being fully customisable... After some research, where I found that none of the present solutions could provide what I needed, I chose Medusa as it provided me with many of the needed features while being easy to extend. I ended up loving the community atmosphere, especially the proximity with the team, and have been helping those in the community looking for a similar fully-customisable solution by sharing a part of my private project. This is how the medusa-extender was born." — Adrien de Peretti
In this tutorial, you’ll learn how to install and set up the Medusa Extender module on your Medusa server. You’ll then learn how to use its customization abilities to create a marketplace in your store! The marketplace will have multiple stores or vendors, and each of these stores will be able to add its own products. This tutorial will be the first part of a series that will explore all aspects of creating a marketplace.
What is Medusa Extender
Medusa Extender is an NPM package that you can add to your Medusa store to extend or customize its functionalities. The scope of its customization entails Entities, Repositories, Services, and more.
The Medusa Extender has many use cases aside the marketplace functionality. It can be used in many other use cases, such as adding custom fields, listening to events to perform certain actions like sending emails, customizing Medusa’s validation of request parameters, and more.
What You’ll Be Creating
In this article and the following parts of this series, you’ll learn how to create a marketplace using Medusa and Medusa Extender. A marketplace is an online store that allows multiple vendors to add their products and sell them.
A marketplace has a lot of features, including managing a vendor's own orders and settings. This part of the tutorial will only showcase how to create stores for each user and attach the products they create to that store.
Code for This Tutorial
If you want to follow along you can find the code for this tutorial in this repository.
Alternatively, if you want to install the marketplace into your existing Medusa store, you can install the Medusa Marketplace plugin. This plugin is created with the code from this tutorial and will be updated with every new part of this series released.
Prerequisites
Before you follow along with this tutorial, make sure you have:
- A Medusa server instance was installed. You can follow along with our easy quickstart guide to learn how you can do that.
- PostgreSQL installed and your Medusa server connected to it.
- Redis installed and your Medusa server connected to it.
Building the Marketplace
Project Setup
In the directory that holds your Medusa server, start by installing Medusa Extender using NPM:
npm i medusa-extender
It’s recommended that you use TypeScript in your project to get the full benefits of Medusa-Extender. To do that, create the file tsconfig.json
in the root of the Medusa project with the following content:
{
"compilerOptions": {
"module": "CommonJS",
"declaration": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"target": "es2017",
"sourceMap": true,
"skipLibCheck": true,
"allowJs": true,
"outDir": "dist",
"rootDir": ".",
"esModuleInterop": true
},
"include": ["src", "medusa-config.js"],
"exclude": ["dist", "node_modules", "**/*.spec.ts"]
}
Next, update the scripts
key in package.json
with the following content:
"scripts": {
"seed": "medusa seed -f ./data/seed.json",
"build": "rm -rf dist && tsc",
"start": "npm run build && node dist/src/main.js",
},
These scripts will ensure that your TypeScript files will be transpiled before Medusa is run.
Then, create the file main.ts
in the directory src
with the following content:
import { Medusa } from 'medusa-extender';
import express = require('express');
async function bootstrap() {
const expressInstance = express();
await new Medusa(__dirname + '/../', expressInstance).load([]);
expressInstance.listen(9000, () => {
console.info('Server successfully started on port 9000');
});
}
bootstrap();
This file will make sure to load all the customizations you’ll add next when you run your Medusa server.
Now, Medusa Extender is fully integrated into your Medusa instance and you can start building the Marketplace.
Customize the Store Entity
You’ll start by customizing the Store entity. You’ll need to use it later on to add relations between the store entity and the users and products entities.
By convention, customizations using Medusa Extender are organized in a module-like structure. However, this is completely optional.
In the src
directory, create the directory modules
in which you’ll store all the customizations in.
Then, create the directory store
inside the modules
directory. The store
directory will hold all customizations related to the Store.
Create a Store Entity
Create the file src/modules/store/entities/store.entity.ts
with the following content:
import { Store as MedusaStore } from '@medusajs/medusa/dist';
import { Entity, JoinColumn, OneToMany } from 'typeorm';
import { Entity as MedusaEntity } from 'medusa-extender';
@MedusaEntity({ override: MedusaStore })
@Entity()
export class Store extends MedusaStore {
//TODO add relations
}
This uses the decorator @Entity
from medusa-extender
to customize Medusa’s Store
entity. You create a Store
class that extends Medusa’s Store entity (imported as MedusaStore
).
You’ll, later on, edit this entity to add the relations between the store and users and products.
Create a Store Repository
Next, you need to override Medusa’s StoreRepository
. This repository will return Medusa’s Store
entity. So, you need to override it to make sure it returns your Store
entity that you just created.
Create the file src/modules/store/repositories/store.repository.ts
with the following content:
import { EntityRepository } from 'typeorm';
import { StoreRepository as MedusaStoreRepository } from '@medusajs/medusa/dist/repositories/store';
import { Repository as MedusaRepository, Utils } from 'medusa-extender';
import { Store } from '../entities/store.entity';
@MedusaRepository({ override: MedusaStoreRepository })
@EntityRepository(Store)
export default class StoreRepository extends Utils.repositoryMixin<Store, MedusaStoreRepository>(MedusaStoreRepository) {
}
Create the Store Module
For now, these are the only files you’ll add for the store. You can create the Store module using these files.
Create the file src/modules/store/store.module.ts
with the following content:
import { Module } from 'medusa-extender';
import { Store } from './entities/store.entity';
import StoreRepository from './repositories/store.repository';
@Module({
imports: [Store, StoreRepository],
})
export class StoreModule {}
This uses the @Module
decorator from medusa-extender
and imports the 2 classes you created.
The last thing left is to import this module and use it with Medusa. In src/main.ts
import StoreModule
at the beginning of the file:
import { StoreModule } from './modules/store/store.module';
Then, add the StoreModule
in the array passed as a parameter to Medusa.load
:
await new Medusa(__dirname + '/../', expressInstance).load([
StoreModule
]);
This is all that you’ll do for now in the Store module. In the next sections, you’ll be adding more classes to it as necessary.
Customize the User Entity
In this section, you’ll customize the user entity mainly to link the user to a store.
Create the User Entity
Create the directory user
inside the modules
directory and create the file src/modules/user/entities/user.entity.ts
with the following content:
import { User as MedusaUser } from '@medusajs/medusa/dist';
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
import { Entity as MedusaEntity } from 'medusa-extender';
import { Store } from '../../store/entities/store.entity';
@MedusaEntity({ override: MedusaUser })
@Entity()
export class User extends MedusaUser {
@Index()
@Column({ nullable: false })
store_id: string;
@ManyToOne(() => Store, (store) => store.members)
@JoinColumn({ name: 'store_id' })
store: Store;
}
This class will add an additional column store_id
of type string and will add a relation to the Store
entity.
To add the new column to the user
table in the database, you need to create a Migration file. Create the file src/modules/user/user.migration.ts
with the following content:
import { Migration } from 'medusa-extender';
import { MigrationInterface, QueryRunner } from 'typeorm';
@Migration()
export default class addStoreIdToUser1644946220401 implements MigrationInterface {
name = 'addStoreIdToUser1644946220401';
public async up(queryRunner: QueryRunner): Promise<void> {
const query = `ALTER TABLE public."user" ADD COLUMN IF NOT EXISTS "store_id" text;`;
await queryRunner.query(query);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const query = `ALTER TABLE public."user" DROP COLUMN "store_id";`;
await queryRunner.query(query);
}
}
The migration is created using the @Migration
decorator from medusa-extender
. Notice that the migration name should end with a JavaScript timestamp based on typeorm
's conventions.
The up
method is run if the migration hasn’t been run before. It will add the column store_id
to the table user
if it doesn’t exist.
You’ll also need to add the relation between the Store and the User entities in src/modules/store/entities/store.entity.ts
. Replace the //TODO
with the following:
@OneToMany(() => User, (user) => user.store)
@JoinColumn({ name: 'id', referencedColumnName: 'store_id' })
members: User[];
Make sure to import the User
entity at the beginning of the file:
import { User } from '../../user/entities/user.entity';
Create the User Repository
Next, you need to override Medusa’s UserRepository
. Create the file src/modules/user/repositories/user.repository.ts
with the following content:
import { UserRepository as MedusaUserRepository } from "@medusajs/medusa/dist/repositories/user";
import { Repository as MedusaRepository, Utils } from "medusa-extender";
import { EntityRepository } from "typeorm";
import { User } from "../entities/user.entity";
@MedusaRepository({ override: MedusaUserRepository })
@EntityRepository(User)
export default class UserRepository extends Utils.repositoryMixin<User, MedusaUserRepository>(MedusaUserRepository) {
}
Create the User Service
Next, you need to override Medusa’s UserService
class. Create the file src/modules/user/services/user.service.ts
with the following content:
import { Service } from 'medusa-extender';
import { EntityManager } from 'typeorm';
import EventBusService from '@medusajs/medusa/dist/services/event-bus';
import { FindConfig } from '@medusajs/medusa/dist/types/common';
import { UserService as MedusaUserService } from '@medusajs/medusa/dist/services';
import { User } from '../entities/user.entity';
import UserRepository from '../repositories/user.repository';
import { MedusaError } from 'medusa-core-utils';
type ConstructorParams = {
manager: EntityManager;
userRepository: typeof UserRepository;
eventBusService: EventBusService;
};
@Service({ override: MedusaUserService })
export default class UserService extends MedusaUserService {
private readonly manager: EntityManager;
private readonly userRepository: typeof UserRepository;
private readonly eventBus: EventBusService;
constructor(private readonly container: ConstructorParams) {
super(container);
this.manager = container.manager;
this.userRepository = container.userRepository;
this.eventBus = container.eventBusService;
}
public async retrieve(userId: string, config?: FindConfig<User>): Promise<User> {
const userRepo = this.manager.getCustomRepository(this.userRepository);
const validatedId = this.validateId_(userId);
const query = this.buildQuery_({ id: validatedId }, config);
const user = await userRepo.findOne(query);
if (!user) {
throw new MedusaError(MedusaError.Types.NOT_FOUND, `User with id: ${userId} was not found`);
}
return user as User;
}
}
This uses the @Service
decorator from medusa-extender
to override Medusa’s UserService
. The class you create to override it will extend UserService
.
This new class overrides the retrieve
method to ensure that the user returned is the new User entity class you created earlier.
Create a User Middleware
The loggedInUser
is not available natively in Medusa. You’ll need to create a Middleware that, when a request is authenticated, registers the logged-in User within the scope.
Create the file src/modules/user/middlewares/loggedInUser.middleware.ts
with the following content:
import { MedusaAuthenticatedRequest, MedusaMiddleware, Middleware } from 'medusa-extender';
import { NextFunction, Response } from 'express';
import UserService from '../../user/services/user.service';
@Middleware({ requireAuth: true, routes: [{ method: "all", path: '*' }] })
export class LoggedInUserMiddleware implements MedusaMiddleware {
public async consume(req: MedusaAuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
if (req.user && req.user.userId) {
const userService = req.scope.resolve('userService') as UserService;
const loggedInUser = await userService.retrieve(req.user.userId, {
select: ['id', 'store_id'],
});
req.scope.register({
loggedInUser: {
resolve: () => loggedInUser,
},
});
}
next();
}
}
You can use the @Middleware
decorator from medusa-extender
to create a Middleware that runs on specific requests. This Middleware is run when the request is received from an authenticated user, and it runs for all paths (notice the use of path: '*'
) and for all types of requests (notice the use of method: "all"
).
Inside the middleware, you retrieve the current user ID from the request, then retrieve the user model and register it in the scope so that it can be accessed from services.
This approach is simplified for the purpose of this tutorial. However, it makes more sense to include this middleware in a separate
auth
module. Whether you include this middleware in theuser
module or theauth
middleware will not affect its functionality.
Create a Store Service to Handle User Insert Events
You need to ensure that when a user is created, a store is associated with it. You can do that by listening to the User-created event and creating a new store for that user. You’ll add this event handler in a StoreService
.
Create the file src/modules/store/services/store.service.ts
with the following content:
import { StoreService as MedusaStoreService } from '@medusajs/medusa/dist/services';
import { EntityManager } from 'typeorm';
import { CurrencyRepository } from '@medusajs/medusa/dist/repositories/currency';
import { Store } from '../entities/store.entity';
import { EntityEventType, Service, MedusaEventHandlerParams, OnMedusaEntityEvent } from 'medusa-extender';
import { User } from '../../user/entities/user.entity';
import EventBusService from '@medusajs/medusa/dist/services/event-bus';
import StoreRepository from '../repositories/store.repository';
interface ConstructorParams {
loggedInUser: User;
manager: EntityManager;
storeRepository: typeof StoreRepository;
currencyRepository: typeof CurrencyRepository;
eventBusService: EventBusService;
}
@Service({ override: MedusaStoreService, scope: 'SCOPED' })
export default class StoreService extends MedusaStoreService {
private readonly manager: EntityManager;
private readonly storeRepository: typeof StoreRepository;
constructor(private readonly container: ConstructorParams) {
super(container);
this.manager = container.manager;
this.storeRepository = container.storeRepository;
}
withTransaction(transactionManager: EntityManager): StoreService {
if (!transactionManager) {
return this;
}
const cloned = new StoreService({
...this.container,
manager: transactionManager,
});
cloned.transactionManager_ = transactionManager;
return cloned;
}
@OnMedusaEntityEvent.Before.Insert(User, { async: true })
public async createStoreForNewUser(
params: MedusaEventHandlerParams<User, 'Insert'>
): Promise<EntityEventType<User, 'Insert'>> {
const { event } = params;
const createdStore = await this.withTransaction(event.manager).createForUser(event.entity);
if (!!createdStore) {
event.entity.store_id = createdStore.id;
}
return event;
}
public async createForUser(user: User): Promise<Store | void> {
if (user.store_id) {
return;
}
const storeRepo = this.manager.getCustomRepository(this.storeRepository);
const store = storeRepo.create() as Store;
return storeRepo.save(store);
}
public async retrieve(relations: string[] = []) {
if (!this.container.loggedInUser) {
return super.retrieve(relations);
}
const storeRepo = this.manager.getCustomRepository(this.storeRepository);
const store = await storeRepo.findOne({
relations,
join: { alias: 'store', innerJoin: { members: 'store.members' } },
where: (qb) => {
qb.where('members.id = :memberId', { memberId: this.container.loggedInUser.id });
},
});
if (!store) {
throw new Error('Unable to find the user store');
}
return store;
}
}
@OnMedusaEntityEvent.Before.Insert
is used to add a listener to an insert event on an entity, which in this case is the User
entity. Inside the listener, you create the user using the createForUser
method. This method just uses the StoreRepository
to create a store.
You also add a helper event retrieve
to retrieve the store that belongs to the currently logged-in user.
Notice the use of scope: 'SCOPED'
in the @Service
decorator. This will allow you to access the logged in user you registered earlier in the scope.
You’ll need to import this new class into the StoreModule
. In src/modules/store/store.module.ts
add the following import at the beginning:
import StoreService from './services/store.service';
Then, add the StoreService
to the imports
array passed to @Module
:
imports: [Store, StoreRepository, StoreService],
Create a User Subscriber
For the event listener to work, you need to first emit this event in a subscriber. The event will be emitted before a User
is inserted. Create the file src/modules/user/subscribers/user.subscriber.ts
with the following content:
import { Connection, EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm';
import { eventEmitter, Utils as MedusaUtils, OnMedusaEntityEvent } from 'medusa-extender';
import { User } from '../entities/user.entity';
@EventSubscriber()
export default class UserSubscriber implements EntitySubscriberInterface<User> {
static attachTo(connection: Connection): void {
MedusaUtils.attachOrReplaceEntitySubscriber(connection, UserSubscriber);
}
public listenTo(): typeof User {
return User;
}
public async beforeInsert(event: InsertEvent<User>): Promise<void> {
return await eventEmitter.emitAsync(OnMedusaEntityEvent.Before.InsertEvent(User), {
event,
transactionalEntityManager: event.manager,
});
}
}
This will create a subscriber using the EventSubscriber
decorator from typeorm
. Then, before a user is inserted the OnMedusaEntityEvent.Before.InsertEvent
event from medusa-extender
is emitted, which will trigger creating the store.
To register the subscriber, you need to create a middleware that registers it. Create the file src/modules/user/middlewares/userSubscriber.middleware.ts
with the following content:
import {
MEDUSA_RESOLVER_KEYS,
MedusaAuthenticatedRequest,
MedusaMiddleware,
Utils as MedusaUtils,
Middleware
} from 'medusa-extender';
import { NextFunction, Response } from 'express';
import { Connection } from 'typeorm';
import UserSubscriber from '../subscribers/user.subscriber';
@Middleware({ requireAuth: false, routes: [{ method: "post", path: '/admin/users' }] })
export class AttachUserSubscriberMiddleware implements MedusaMiddleware {
public async consume(req: MedusaAuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
const { connection } = req.scope.resolve(MEDUSA_RESOLVER_KEYS.manager) as { connection: Connection };
MedusaUtils.attachOrReplaceEntitySubscriber(connection, UserSubscriber);
return next();
}
}
This will register the subscriber when a POST
request is sent to /admin/users
, which creates a new user.
Create a User Router
The last customization left is an optional one. By default, Medusa’s create user endpoint requires you to be authenticated as an admin. In a marketplace use case, you might want users to register on their own and create their own stores. If this is not the case for you, you can skip creating the following class.
Medusa Extender allows you to also override routes in Medusa. In this case, you’ll be adding the /admin/create-user
route to accept non-authenticated requests.
Create the file src/modules/user/routers/user.router.ts
and add the following content:
import { Router } from 'medusa-extender';
import createUserHandler from '@medusajs/medusa/dist/api/routes/admin/users/create-user';
import wrapHandler from '@medusajs/medusa/dist/api/middlewares/await-middleware';
@Router({
routes: [
{
requiredAuth: false,
path: '/admin/create-user',
method: 'post',
handlers: [wrapHandler(createUserHandler)],
},
],
})
export class UserRouter {
}
You use the @Router
decorator from medusa-extender
to create a router. This router will accept a routes
array which will either be added or override existing routes in your Medusa server. In this case, you override the /admin/create-user
route and set requiredAuth
to false.
To make sure that the AttachUserSubscriberMiddleware
also runs for this new route (so that the before insert user event handlers run for this new route), make sure to add a new entry to the routes
array:
@Middleware({ requireAuth: false, routes: [{ method: "post", path: '/admin/users' }, { method: "post", path: '/admin/create-user' }] })
Create a User Module
You’ve added all the customizations necessary to associate a user with their own store. Now, you can create the User module using these files.
Create the file src/modules/user/user.module.ts
with the following content:
import { AttachUserSubscriberMiddleware } from './middlewares/userSubscriber.middleware';
import { LoggedInUserMiddleware } from "./middlewares/loggedInUser.middleware";
import { Module } from 'medusa-extender';
import { User } from './entities/user.entity';
import UserRepository from './repositories/user.repository';
import { UserRouter } from "./routers/user.router";
import UserService from './services/user.service';
import addStoreIdToUser1644946220401 from './user.migration';
@Module({
imports: [
User,
UserService,
UserRepository,
addStoreIdToUser1644946220401,
UserRouter,
LoggedInUserMiddleware,
AttachUserSubscriberMiddleware
]
})
export class UserModule {}
If you didn’t create the
UserRouter
in the previous step then make sure to remove it from theimports
array.
The last thing left is to import this Module. In src/main.ts
import UserModule
at the beginning of the file:
import { UserModule } from './modules/user/user.module';
Then, add the UserModule
in the array passed as a parameter to Medusa.load
:
await new Medusa(__dirname + '/../', expressInstance).load([
UserModule,
StoreModule
]);
Test it Out
You are now ready to test out this customization! In your terminal, run your Medusa server:
npm start
Or using Medusa’s CLI:
medusa develop
After your run your server, you need to use a tool like Postman to easily send requests to your server.
If you didn’t add the UserRouter
, you first need to log in as an admin to be able to add users. You can do that by sending a POST
request to localhost:9000/admin/auth
. In the body, you should include the email and password. If you’re using a fresh Medusa install you can use the following credentials:
{
"email": "admin@medusa-test.com",
"password": "supersecret"
}
Following this request, you can send authenticated requests to the Admin.
Send a POST
request to [localhost:9000/admin/users](http://localhost:9000/admin/users)
to create a new user. In the body, you need to pass the email and password of the new user:
{
"email": "example@gmail.com",
"password": "supersecret"
}
The request will return a user object with the details of the new user:
Notice how there’s a store_id
field now. If you try to create a couple of users, you’ll see that the store_id
will be different each time.
Customize the Products Entity
Similar to how you just customized the User
entity, you need to customize the Product
entity to also hold the store_id
with the relationship as well. You’ll then customize the ProductService
as well as other classes to make sure that, when a product is created, the store ID of the user creating it is attached to it. You’ll also make sure that when the list of products is fetched, only the products that belong to the current user’s store are returned.
Create a Product Entity
Create the file src/modules/product/entities/product.entity.ts
with the following content:
import { Product as MedusaProduct } from '@medusajs/medusa/dist';
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
import { Entity as MedusaEntity } from 'medusa-extender';
import { Store } from '../../store/entities/store.entity';
@MedusaEntity({ override: MedusaProduct })
@Entity()
export class Product extends MedusaProduct {
@Index()
@Column({ nullable: false })
store_id: string;
@ManyToOne(() => Store, (store) => store.members)
@JoinColumn({ name: 'store_id', referencedColumnName: 'id' })
store: Store;
}
This will override Medusa’s Product
entity to add the store_id
field and relation to the Store
entity.
You need to also reflect this relation in the Store
entity, so, in src/modules/store/entities/store.entity.ts
add the following code below the relation with the User
entity you previously added:
@OneToMany(() => Product, (product) => product.store)
@JoinColumn({ name: 'id', referencedColumnName: 'store_id' })
products: Product[];
Make sure to import the Product
entity at the beginning of the file:
import { Product } from '../../product/entities/product.entity';
Create a Product Migration
Next, create the file src/modules/product/product.migration.ts
with the following content:
import { MigrationInterface, QueryRunner } from 'typeorm';
import { Migration } from 'medusa-extender';
@Migration()
export default class addStoreIdToProduct1645034402086 implements MigrationInterface {
name = 'addStoreIdToProduct1645034402086';
public async up(queryRunner: QueryRunner): Promise<void> {
const query = `ALTER TABLE public."product" ADD COLUMN IF NOT EXISTS "store_id" text;`;
await queryRunner.query(query);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const query = `ALTER TABLE public."product" DROP COLUMN "store_id";`;
await queryRunner.query(query);
}
}
This will add a migration that will add the store_id
column to the product
table.
Create a Product Repository
Next, create the file src/modules/repositories/product.repository.ts
with the following content:
import { Repository as MedusaRepository, Utils } from "medusa-extender";
import { EntityRepository } from "typeorm";
import { ProductRepository as MedusaProductRepository } from "@medusajs/medusa/dist/repositories/product";
import { Product } from '../entities/product.entity';
@MedusaRepository({ override: MedusaProductRepository })
@EntityRepository(Product)
export default class ProductRepository extends Utils.repositoryMixin<Product, MedusaProductRepository>(MedusaProductRepository) {
}
This will override Medusa’s ProductRepository
to return your new Product
entity.
Create a Product Service
Now, you’ll add the customization to ensure that only the products that belong to the currently logged-in user are returned when a request is sent.
Since you created the LoggedInUserMiddleware
earlier, you can have access to the logged-in user from any service through the container
object passed to the constructor of the service.
Create the file src/modules/product/services/product.service.ts
with the following content:
import { EntityEventType, MedusaEventHandlerParams, OnMedusaEntityEvent, Service } from 'medusa-extender';
import { EntityManager } from "typeorm";
import { ProductService as MedusaProductService } from '@medusajs/medusa/dist/services';
import { Product } from '../entities/product.entity';
import { User } from '../../user/entities/user.entity';
import UserService from '../../user/services/user.service';
type ConstructorParams = {
manager: any;
loggedInUser: User;
productRepository: any;
productVariantRepository: any;
productOptionRepository: any;
eventBusService: any;
productVariantService: any;
productCollectionService: any;
productTypeRepository: any;
productTagRepository: any;
imageRepository: any;
searchService: any;
userService: UserService;
}
@Service({ scope: 'SCOPED', override: MedusaProductService })
export class ProductService extends MedusaProductService {
readonly #manager: EntityManager;
constructor(private readonly container: ConstructorParams) {
super(container);
this.#manager = container.manager;
}
prepareListQuery_(selector: object, config: object): object {
const loggedInUser = this.container.loggedInUser
if (loggedInUser) {
selector['store_id'] = loggedInUser.store_id
}
return super.prepareListQuery_(selector, config);
}
}
This will override the prepareListQuery
method in Medusa’s ProductService
, which this new class extends, to get the logged-in user. Then, if the user is retrieved successfully the key store_id
is added to the selector
object to filter the products by the user’s store_id
.
Create a Product Module
That’s all the customization you’ll do for now. You just need to import all these files into a Product module.
Create src/modules/product/product.module.ts
with the following content:
import { Module } from 'medusa-extender';
import { Product } from './entities/product.entity';
import ProductRepository from './repositories/product.repository';
import { ProductService } from './services/product.service';
import addStoreIdToProduct1645034402086 from './product.migration';
@Module({
imports: [
Product,
ProductRepository,
ProductService,
addStoreIdToProduct1645034402086,
]
})
export class ProductModule {}
Finally, import the ProductModule
at the beginning of src/main.ts
:
import { ProductModule } from './modules/product/product.module';
And add the ProductModule
to the array passed to load
along with UserModule
:
await new Medusa(__dirname + '/../', expressInstance).load([
UserModule,
ProductModule,
StoreModule
]);
Test it Out
You can go ahead and test it out now. Run the server if it isn’t running already and log in with the user you created earlier by sending the credentials to localhost:9000/admin/auth
.
After that, send a GET
request to localhost:9000/admin/products
. You’ll receive an empty array of products as the current user does not have any products yet.
Create a Product Subscriber
You’ll now add the necessary customization to attach a store ID to a newly created product.
To listen to the product created event, create the file src/modules/product/subscribers/product.subscriber.ts
with the following content:
import { Connection, EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm';
import { OnMedusaEntityEvent, Utils, eventEmitter } from 'medusa-extender';
import { Product } from '../entities/product.entity';
@EventSubscriber()
export default class ProductSubscriber implements EntitySubscriberInterface<Product> {
static attachTo(connection: Connection): void {
Utils.attachOrReplaceEntitySubscriber(connection, ProductSubscriber);
}
public listenTo(): typeof Product {
return Product;
}
public async beforeInsert(event: InsertEvent<Product>): Promise<void> {
return await eventEmitter.emitAsync(OnMedusaEntityEvent.Before.InsertEvent(Product), {
event,
transactionalEntityManager: event.manager,
});
}
}
Then, you need to register this Subscriber using Middleware. Create the file src/modules/product/middlewares/product.middleware.ts
with the following content:
import {
MEDUSA_RESOLVER_KEYS,
MedusaAuthenticatedRequest,
MedusaMiddleware,
Utils as MedusaUtils,
Middleware
} from 'medusa-extender';
import { NextFunction, Request, Response } from 'express';
import { Connection } from 'typeorm';
import ProductSubscriber from '../subscribers/product.subscriber';
@Middleware({ requireAuth: true, routes: [{ method: 'post', path: '/admin/products' }] })
export default class AttachProductSubscribersMiddleware implements MedusaMiddleware {
public consume(req: MedusaAuthenticatedRequest | Request, res: Response, next: NextFunction): void | Promise<void> {
const { connection } = req.scope.resolve(MEDUSA_RESOLVER_KEYS.manager) as { connection: Connection };
MedusaUtils.attachOrReplaceEntitySubscriber(connection, ProductSubscriber);
return next();
}
}
This will register the subscriber when a POST
request is sent to /admin/products
, which creates a new product.
Add Event Listener in Product Service
Next, in src/modules/product/services/product.service.ts
add the following inside the class:
@OnMedusaEntityEvent.Before.Insert(Product, { async: true })
public async attachStoreToProduct(
params: MedusaEventHandlerParams<Product, 'Insert'>
): Promise<EntityEventType<Product, 'Insert'>> {
const { event } = params;
const loggedInUser = this.container.loggedInUser;
event.entity.store_id = loggedInUser.store_id;
return event;
}
This will listen to the Insert event using the @OnMedusaEntityEvent
decorator from medusa-extender
. It will then use the logged-in user and attach the user’s store_id
to the newly created product.
Add Middleware to Product Module
Finally, make sure to import the new middleware at the beginning of src/modules/product/product.module.ts
:
import AttachProductSubscribersMiddleware from './middlewares/product.middleware';
Then, add it in the imports
array passed to @Module
:
imports: [
Product,
ProductRepository,
ProductService,
addStoreIdToProduct1645034402086,
AttachProductSubscribersMiddleware
]
You’re ready to add products into a store now! Run the server if it’s not running and make sure you’re logged in with the user you created earlier. Then, send a POST
request to [localhost:9000/admin/products](http://localhost:9000/admin/products)
with the following body:
{
"title": "my product",
"options": []
}
This is the minimum structure of a product. You can rename the title to anything you want.
After you send the request, you should receive a Product object where you can see the store_id
is set to the same store_id
of the user you’re logged in with.
Now, try sending a GET
request to [localhost:9000/admin/products](http://localhost:9000/admin/products)
as you did earlier. Instead of an empty array, you’ll see the product you just added.
Testing it Out Using Medusa’s Admin
If you also have a Medusa Admin instance installed, you can also test this out. Log in with the user you created earlier and you’ll see that you can only see the product they added.
Conclusion
In this tutorial, you learned the first steps of creating a Marketplace using Medusa and Medusa Extender! In later points, you’ll learn about how you can add settings, manage orders, and more!
Be sure to support Medusa Extender and check the repository out for more details!
Should you have any issues or questions related to Medusa, then feel free to reach out to the Medusa team via Discord. You can also contact Adrien @adrien2p
for more details or help regarding Medusa Extender.