Installation
npm i medusa-extender
This packages exports the necessary objects to customize medusajs and fit your needs.
Here is the architecture of this package and how modules are related to each other. It will help you navigate into the code base.
- π§βπ» Decorators and full typings
Made DX easy with the usage of decorators for modular architecture and full typings support for a better DX
- ποΈ Flexible architecture.
No need anymore to put your services in the services directory, your entities in the models directory and so on. You put your files where you want. That way you can organize your code as modules for example and group your modules by domains.
- π Create or extends entities
Some of the problem that developers encounter, is that when you want to add custom fields to an entity, it is not that easy. You can't extends a typeorm entity and adding custom fields through configuration make lost of the typings and the domains in which they exists. Here, you can now extend a typeorm entity just like any other object.
- π Create or extends services
If you need to extend a service to manage your new fields or update the business logic according to your new needs, you only need to extend the original service from medusa and that's it.
- π Create or extends repositories
When you extend an entity and you want to manipulate that entity in a service, you need to do that through a repository. In order for that repository to reflect your extended entities, but still get access to the base repository methods, you are provided with the right tools to do so.
- π Create custom middlewares to apply before/after authentication
Do you want to apply custom middlewares to load data on the requests or add some custom checks or any other situations? Then what are you waiting for?
- π Create custom route and attach custom service to handle it.
Do you need to add new routes for new features? Do you want to receive webhook? It is easy to do it now.
- π‘ Handle entity events from subscriber as easy as possible through the provided decorators.
Emit an event (async/sync) from your subscriber and then register a new handler in any of your files. Just use the
OnMedusaEntityEvent
decorator.
- π¦ Build sharable modules
Build a module, export it and share it with the community.
Click to see the example!
// index.ts
import { MyModule } from './modules/myModule/myModule.module';
async function bootstrap() {
const expressInstance = express();
const rootDir = resolve(__dirname);
await new Medusa(rootDir, expressInstance).load(MyModule);
expressInstance.listen(config.serverConfig.port, () => {
logger.info('Server successfully started on port ' + config.serverConfig.port);
});
}
bootstrap();
Let say that you want to add a new field on the Product
entity.
Click to see the example!
// modules/product/product.entity.ts
import { Product as MedusaProduct } from '@medusa/medusa/dist';
import { Column, Entity } from "typeorm";
import { Entity as MedusaEntity } from "medusa-extender";
//...
@MedusaEntity({ override: MedusaProduct })
@Entity()
class Product extends MedusaProduct {
@Column()
customField: string;
}
After have updated your entity, you will have to migrate the database in order to reflect the new fields.
Click to see the example!
// modules/product/20211126000001-add-field-to-product
import { MigrationInterface, QueryRunner } from 'typeorm';
import { Migration } from 'medusa-extender';
@Migration()
export default class AddFieldToProduct1611063162649 implements MigrationInterface {
name = 'addFieldToProduct1611063162649';
public async up(queryRunner: QueryRunner): Promise<void> {
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}
We will then create a new repository to reflect our custom entity.
Click to see the example!
// modules/product/product.repository.ts
import { OrderRepository as MedusaOrderRepository } from '@medusa/medusa/dist/repositories/order';
import { EntityRepository } from "typeorm";
import { Repository as MedusaRepository, Utils } from "medusa-extender";
import { Order } from "./order.entity";
//...
@MedusaRepository({ override: MedusaOrderRepository })
@EntityRepository()
export class OrderRepository extends Utils.repositoryMixin<Order, MedusaOrderRepository>(MedusaOrderRepository) {
testProperty = 'I am the property from OrderRepository that extend MedusaOrderRepository';
test(): Promise<Order[]> {
return this.findWithRelations() as Promise<Order[]>;
}
}
We want now to add a custom service to implement our custom logic for our new field.
Click to see the example!
// modules/product/product.service.ts
import { Service } from 'medusa-extender';
//...
interface ConstructorParams<TSearchService extends DefaultSearchService = DefaultSearchService> {
manager: EntityManager;
productRepository: typeof ProductRepository;
productVariantRepository: typeof ProductVariantRepository;
productOptionRepository: typeof ProductOptionRepository;
eventBusService: EventBusService;
productVariantService: ProductVariantService;
productCollectionService: ProductCollectionService;
productTypeRepository: ObjectType<typeof ProductTypeRepository>;
productTagRepository: ObjectType<typeof ProductTagRepository>;
imageRepository: ObjectType<typeof ImageRepository>;
searchService: TSearchService;
}
@Service({ scope: 'SCOPED', override: MedusaProductService })
export default class ProductService extends MedusaProductService {
readonly #manager: EntityManager;
constructor(private readonly container: ConstructorParams) {
super(container);
this.#manager = container.manager;
}
@OnMedusaEvent.Before.Insert(Product, { async: true })
public async attachStoreToProduct(
params: MedusaEventHandlerParams<Product, 'Insert'>
): Promise<EntityEventType<Product, 'Insert'>> {
const { event } = params;
event.entity.customField = 'custom_value';
return event;
}
public prepareListQuery_(selector: Record<string, any>, config: FindConfig<Product>): any {
selector['customField'] = 'custom_value';
return super.prepareListQuery_(selector, config) as any;
}
}
Let say that you want to attach a custom middleware to certain routes
Click to see the example!
// modules/product/custom.middleware.ts
import { Express, NextFunction, Response } from 'express';
import {
Middleware,
MedusaAuthenticatedRequest,
MedusaMiddleware,
} from 'medusa-extender';
const routerOption = { method: 'post', path: '/admin/products/' };
@Middleware({ requireAuth: true, routerOptions: [routerOption] })
export class CustomMiddleware implements MedusaMiddleware {
public consume(
options: { app: Express }
): (req: MedusaAuthenticatedRequest, res: Response, next: NextFunction) => void | Promise<void> {
return (req: MedusaAuthenticatedRequest, res: Response, next: NextFunction): void => {
return next();
};
}
}
If you need to add custom routes to medusa here is a simple way to achieve that
Click to see the example!
// modules/product/product.router.ts
import { Router } from 'medusa-extender';
import yourController from './yourController.contaoller';
@Router({
router: [{
requiredAuth: true,
path: '/admin/dashboard',
method: 'get',
handler: yourController.getStats
}]
})
export class ProductRouter {
}
the last step is to import everything in our module π¦
Click to see the example!
// modules/products/myModule.module.ts
import { Module } from 'medusa-extender';
import { Product } from './product.entity';
import { ProductRouter } from './product.router';
import { CustomMiddleware } from './custom.middleware';
import ProductRepository from './product.repository';
import ProductService from './product.service';
import AddFieldToProduct1611063162649 from './product.20211126000001-add-field-to-product';
@Module({
imports: [
Product,
ProductRepository,
ProductService,
ProductRouter,
CustomMiddleware,
AddFieldToProduct1611063162649
]
})
export class MyModule {}
That's it you've completed your first module π
Here is the list of the provided decorators.
Decorator | Description | Option |
---|---|---|
@Entity(/*...*/) |
Decorate an entity | { scope?: LifetimeType; resolutionKey?: string; override?: Type<TOverride>; }; |
@Repository(/*...*/) |
Decorate a repository | { resolutionKey?: string; override?: Type<TOverride>; }; |
@Service(/*...*/) |
Decorate a service | { scope?: LifetimeType; resolutionKey?: string; override?: Type<TOverride>; }; |
@Middleware(/*...*/) |
Decorate a middleware | { requireAuth: boolean; string; routerOptions: MedusaRouteOptions[]; }; |
@Router(/*...*/) |
Decorate a router | { router: RoutesInjectionRouterConfiguration[]; }; |
@Migration(/*...*/) |
Decorate a migration | |
@OnMedusaEntityEvent.\*.\*(/*...*/) |
Can be used to send the right event type or register handler to an event |
One of the feature out the box is the ability to emit (sync/async) event from your entity subscriber and to be able to handle those event easily.
To be able to achieve that, here is an example.
Click to see the example!
// modules/products/product.subscriber.ts
import { Connection, EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm';
import { eventEmitter, Utils, OnMedusaEntityEvent } 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;
}
/**
* Relay the event to the handlers.
* @param event Event to pass to the event handler
*/
public async beforeInsert(event: InsertEvent<Product>): Promise<void> {
return await eventEmitter.emitAsync(OnMedusaEntityEvent.Before.InsertEvent(Product), {
event,
transactionalEntityManager: event.manager,
});
}
}
And then create a new handler.
Click to see the example!
// modules/product/product.service.ts
import { Service, OnMedusaEntityEvent } from 'medusa-extender';
//...
interface ConstructorParams {
// ...
}
@Service({ scope: 'SCOPED', override: MedusaProductService })
export default class ProductService extends MedusaProductService {
readonly #manager: EntityManager;
constructor(private readonly container: ConstructorParams) {
super(container);
this.#manager = container.manager;
}
@OnMedusaEntityEvent.Before.Insert(Product, { async: true })
public async attachStoreToProduct(
params: MedusaEventHandlerParams<Product, 'Insert'>
): Promise<EntityEventType<Product, 'Insert'>> {
const { event } = params;
event.entity.customField = 'custom_value';
return event;
}
}
And finally, we need to add the subscriber to the connection. There is different way to achieve it. Will see as an example a way to attach request scoped subscribers.
Click to see the example!
// modules/product/attachSubscriber.middleware.ts
import { Express, NextFunction, Response } from 'express';
import {
Middleware,
MEDUSA_RESOLVER_KEYS,
MedusaAuthenticatedRequest,
MedusaMiddleware,
MedusaRouteOptions,
Utils as MedusaUtils,
} from 'medusa-extender';
import { Connection } from 'typeorm';
import Utils from '@core/utils';
import ProductSubscriber from '@modules/product/subscribers/product.subscriber'; import { Middleware } from "./components.decorator";
@Middleware({ requireAuth: true, routerOptions: [{ method: 'post', path: '/admin/products/' }] })
export class AttachProductSubscribersMiddleware implements MedusaMiddleware {
private app: Express;
private hasBeenAttached = false;
public static get routesOptions(): MedusaRouteOptions {
return {
path: '/admin/products/',
method: 'post',
};
}
public consume(
options: { app: Express }
): (req: MedusaAuthenticatedRequest, res: Response, next: NextFunction) => void | Promise<void> {
this.app = options.app;
const attachIfNeeded = (routeOptions: MedusaRouteOptions): void => {
if (!this.hasBeenAttached) {
this.app.use((req: MedusaAuthenticatedRequest, res: Response, next: NextFunction): void => {
if (Utils.isExpectedRoute([routeOptions], req)) {
const { connection } = req.scope.resolve(MEDUSA_RESOLVER_KEYS.manager) as { connection: Connection };
MedusaUtils.attachOrReplaceEntitySubscriber(connection, ProductSubscriber);
}
return next();
});
this.hasBeenAttached = true;
}
}
return (req: MedusaAuthenticatedRequest, res: Response, next: NextFunction): void => {
const routeOptions = AttachProductSubscribersMiddleware.routesOptions;
attachIfNeeded(routeOptions);
return next();
};
}
}
Now, you only need to add that middleware to the previous module we've created.
Click to see the example!
// modules/products/myModule.module.ts
import { Module } from 'medusa-extender';
import { Product } from './product.entity';
import { ProductRouter } from './product.router';
import { CustomMiddleware } from './custom.middleware';
import ProductRepository from './product.repository';
import ProductService from './product.service';
import AddFieldToProduct1611063162649 from './product.20211126000001-add-field-to-product';
import { AttachProductSubscribersMiddleware } from './attachSubscriber.middleware'
@Module({
imports: [
Product,
ProductRepository,
ProductService,
ProductRouter,
CustomMiddleware,
AttachProductSubscribersMiddleware,
AddFieldToProduct1611063162649
]
})
export class MyModule {}
Contributions welcome! You can look at the contribution guidelines