webface / medusa-extender

:syringe: Medusa on steroid. Badass modules, extends Typeorm entities and repositories, medusa core services and so on. Get the full power of modular architecture. Keep your domains clean. Build shareable modules :rocket:

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Medusa

Extend medusa to fit your needs

Did ever though about adding custom fields? Did you ever wonder how to add some custom features? Did you ever wanted to build something more than a single store? Well, this project has been made to help you reach you goal. It is now possible to customise Medusa in a way you will be able to enjoy all the awesome features that Medusa provides you but with the possibility to take your e-commerce project to the next level πŸš€


Awesome npm version activity issues download coverage licence twitter

Access the website Documentation

Table of contents

Getting started

Installation

npm i medusa-extender

Code base overview

Dependency graph

Features

  • πŸ§‘β€πŸ’» Decorators and full typing support

Makes DX easy with the usage of decorators for modular architecture and full typing support for a better DX

  • πŸ—οΈ Flexible architecture.

You can organize your code as modules and group your modules by domains.

  • πŸŽ‰ Create or extend entities

Some of the problems that developers encounter are that when you want to add custom fields to an entity, it is not that easy. You can't extend a typeorm entity and adding custom fields through configuration makes you lose the typings and the domains in which they exist. Here, you can now extend a typeorm entity just like any other object.

  • πŸŽ‰ Create or extend 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 extend repositories

When you extend an entity and you want to manipulate that entity in a service, you need to do it through a repository. In order for that repository to reflect your extended entities, while still getting 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 webhooks? It is easy to do it now.

  • πŸ’‘ Handle entity events from subscribers as easily 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.

Usage

Create your server

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();

Create your first module πŸš€

Entity

Let's 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;
}

Migration

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> {
    }
}

Repository

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(Order)
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[]>;
	}
}

Service

We now want 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, OnMedusaEntityEvent, MedusaEventHandlerParams, EntityEventType } 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: typeof ProductTypeRepository;
    productTagRepository: typeof ProductTagRepository;
    imageRepository: 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;
    }
    
    @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;
    }
    
    /**
    * This is an example. you must not necessarly keep that implementation.
    **/
    public prepareListQuery_(selector: Record<string, any>, config: FindConfig<Product>): object {
        selector['customField'] = 'custom_value';
        return super.prepareListQuery_(selector, config);
    }
}

Middleware

Let's 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();
        };
    }
}

Router

If you need to add custom routes to medusa here is a simple way to achieve this

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 {
}

Validator

If you add a custom field on an entity, there is a huge risk that you end up getting an error as soon as you it the end point with that new field. The medusa validators are not aware of your new field once the request arrive. In order to handle that you can extend the class validator in order to add your custom field constraint.

Click to see the example!
// modules/product/AdminPostProductsReq.validator.ts

import { Validator } from 'medusa-extender';
import { AdminPostProductsReq } from "@medusajs/medusa/dist";

@Validator({ override: AdminPostProductsReq })
class ExtendedClassValidator extends AdminPostProductsReq {
    @IsString()
    customField: string;
}

Module

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,
        ExtendedClassValidator
    ]
})
export class MyModule {}

That's it. You've completed your first module πŸš€

Decorators

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
@Validator(/*...*/) Decorate a validator { override: Type<TOverride>; };
@OnMedusaEntityEvent.\*.\*(/*...*/) Can be used to send the right event type or register handler to an event

Entity event handling

One of the feature out the box is the ability to emit (sync/async) events from your entity subscriber and to be able to handle those events easily.

To be able to achieve this, 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 are different ways to achieve this. We will see, as an example below, 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 {}

Contribute πŸ—³οΈ

Contributions are welcome! You can look at the contribution guidelines

About

:syringe: Medusa on steroid. Badass modules, extends Typeorm entities and repositories, medusa core services and so on. Get the full power of modular architecture. Keep your domains clean. Build shareable modules :rocket:

License:MIT License


Languages

Language:TypeScript 95.8%Language:JavaScript 2.5%Language:HTML 1.7%