JSPM

  • Created
  • Published
  • Downloads 60
  • Score
    100M100P100Q75297F
  • License MIT

:syringe: Medusa on steroid. 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:

Package Exports

  • medusa-extender
  • medusa-extender/dist/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (medusa-extender) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

Medusa

Extend medusa with badass features

Do you want to extend existing entities to add custom fields? Do you want to implement your own feature or extend existing one in a module way? 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 🚀



Access the website Documentation

Table of contents

Getting started

The usage of the extender does not break any features from the original medusa.

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 (Custom fields)

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 that are applied 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 handler to it.

Do you need to add new routes for new features? Do you want to receive webhooks? Create a new route, attach an handler and enjoy.

  • 🎉 Override existing validators.

Really useful when your adding custom fields.

  • 💡 Handle entity events from subscribers as smoothly as possible.

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

For the purpose of the examples that will follow in the next sections, I will organise my files in the following manner (You can organise it as you want, there is no restrictions to your architecture).

Scenario 1 module architecture

Extending an existing feature

Let's create a scenario.

As a user, I want to add a new field to the existing product entity to manage some custom data. For that I will need:

  • To extend an entity (for the example we will use the product);
  • To extend the custom repository in order to reflect the extended entity through the repository;
  • To extend the service, in order to take that new field in count;
  • To create a validator that will extend and existing one to add the custom field.
  • To create a migration that will add the field in the database.

For the purpose of the example, I will want to be able to register an handler on an entity event that I will implement in the extended service. That subscriber will be request scoped, which means a middleware will attach the subscriber to the connection for each request (This is only for the purpose of showing some features).

Step 1: Extend the product entity

The idea here, is that we will import the medusa product entity that we will extend in order to add our new field. Of course, you can do everything typeorm provides (if you need to add a custom relationships, then follow the typeorm doc.).

Step 1 Extend the product entity
Click to see the raw example!
// src/modules/product/product.entity.ts

import { Column, Entity } from "typeorm"; 
import { Product as MedusaProduct } from '@medusa/medusa/dist';
import { Entity as MedusaEntity } from "medusa-extender";

@MedusaEntity({ override: MedusaProduct })
@Entity()
export class Product extends MedusaProduct {
    @Column()
    customField: string;
}

Step 2: Extend the product repository

The idea here, is that we will import the medusa product repository that we will extend in order to reflect our custom entity.

Step 2: Extend the product repository
Click to see the raw example!
// src/modules/product/product.repository.ts

import { ProductRepository as MedusaProductRepository } from '@medusa/medusa/dist/repositories/order'; 
import { EntityRepository } from "typeorm"; 
import { Repository as MedusaRepository, Utils } from "medusa-extender"; 
import { Product } from "./product.entity";

@MedusaRepository({ override: MedusaProductRepository })
@EntityRepository(Product)
export class ProductRepository extends Utils.repositoryMixin<Product, MedusaProductRepository>(MedusaProductRepository) {
    /* You can implement custom repository methods here. */
}

Step 3: Extend the product service to manage our custom entity field

The idea here, is that we will import the medusa product service that we will extend in order to override the product creation method of the base class in order to take in count the new field of our extended product entity.

Step 3: Extend the product service
Click to see the raw example!
// src/modules/product/product.service.ts

import { Service, OnMedusaEntityEvent, MedusaEventHandlerParams, EntityEventType } from 'medusa-extender';
import { ProductService as MedusaProductService } from '@medusa/medusa/dist/services';
import { EntityManager } from "typeorm";

type ConstructorParams = /* ... */

@Service({ scope: 'SCOPED', override: MedusaProductService })
export class ProductService extends MedusaProductService {
    readonly #manager: EntityManager;
    
    constructor(private readonly container: ConstructorParams) {
        super(container);
        this.#manager = container.manager;
    }
    
    /**
    * In that example, the customField could represent a static value
    * such as a store_id which depends on the loggedInUser store_id.
    **/
    @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.
    * Here, we are overriding the existing method to add a custom constraint.
    * For example, if you add a store_id on a product, that value
    * will probably depends on the loggedInUser store_id which is a static
    * value.
    **/
    public prepareListQuery_(selector: Record<string, any>, config: FindConfig<Product>): object {
        selector['customField'] = 'custom_value';
        return super.prepareListQuery_(selector, config);
    }
}

Step 4: Extend the product validator class to reflect the new field

When adding a new field, the class validator of the end point handler is not aware about it. In order to handle that, it is possible to extend the validator to add the constraint on the new custom field.

Step 4: Extend the product validator class to reflect the new field
Click to see the raw example!
// src/modules/product/adminPostProductsReq.validator.ts

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

Step 5: Create the migration

To persist your custom field, you need to add it to the corresponding table. As normal, write a new migration, except this time, you decorate it with the @Migration() decorator.

Step 5: Create the migration
Click to see the raw example!
// src/modules/product/customField.migration.ts

import { Migration } from 'medusa-extender';
import { MigrationInterface, QueryRunner } from 'typeorm';

@Migration()
export default class addCustomFieldToProduct1611063162649 implements MigrationInterface {
    name = 'addCustomFieldToProduct1611063162649';
    
    public async up(queryRunner: QueryRunner): Promise<void> {
        /* Write your query there. */
    }
    
    public async down(queryRunner: QueryRunner): Promise<void> {
        /* Write your query there. */
    }
}

Step 6: Wrapping everything in a module

Now that we have done the job, we will import the entity, repository and service into a module that will be loaded by Medusa.

Step 4: Create the product module
Click to see the raw example!
// src/modules/product/product.module.ts

import { Module } from 'medusa-extender';
import { Product } from './product.entity';
import { ProductRepository } from './product.repository';
import { ProductService } from './product.service';
import { ExtendedClassValidator } from './adminPostProductsReq.validator';
import { addCustomFieldToProduct1611063162649 } from './customField.migration';

@Module({
    imports: [
        Product,
        ProductRepository,
        ProductService,
        ExtendedClassValidator,
        addCustomFieldToProduct1611063162649
    ]
})
export class ProductModule {}

Handling entity subscribers

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!
// src/modules/product/product.subscriber.ts

import { Connection, EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm';
import { eventEmitter, Utils, OnMedusaEntityEvent } from 'medusa-extender';
import { Product } from './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!
// src/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 a request scoped subscribers.

Click to see the example!
// src/modules/product/attachSubscriber.middleware.ts

import { NextFunction, Request, Response } from 'express';
import {
    Middleware,
    MedusaAuthenticatedRequest,
    Utils as MedusaUtils,
    MedusaMiddleware
} from 'medusa-extender';
import UserSubscriber from './product.subscriber';

@Middleware({ requireAuth: true, routerOptions: [{ method: 'post', path: '/admin/products/' }] })
export default class AttachProductSubscribersMiddleware implements MedusaMiddleware {
    public consume(err: unknonw, req: MedusaAuthenticatedRequest | Request, res: Response, next: NextFunction): void | Promise<void> {
        MedusaUtils.attachOrReplaceEntitySubscriber(connection, UserSubscriber);
        return next();
    }
}

Now, you only need to add that middleware to the previous module we've created.

Click to see the example!
// src/modules/products/product.module.ts

import { Module } from 'medusa-extender';
import { AttachProductSubscribersMiddleware } from './attachSubscriber.middleware'

@Module({
    imports: [
        /* ... */
        AttachProductSubscribersMiddleware
    ]
})
export class ProductModule {}

Create a custom feature module

This is the same principle as overriding an existing feature. Instead of giving an override options to the decorators, you'll have to use the resolutionKey in order to register them into the container using that key. You'll be then able to retrieve them using the custom resolutionKey to resolve through the container.

Build a shareable module

Building a shareable module is nothing more that the previous section. to achieve that you can start using the plugin-module starter.

Use custom configuration inside service

Each service is resolve by the container. One of the object that the container holds is, the configModule. Which means that in any service, you are able to retrieve everything that is in your medusa-config file. In other word, all the config you need to access in a service, can be added to your medusa-config file.

Integration in an existing medusa project

To benefit from all the features that the extender offers you, the usage of typescript is recommended. If you have already an existing project scaffold with the command medusa new ... here is how are the following steps to integrate the extender in your project.

follow the next steps yo be ready to launch 🚀

npm i -D typescript
echo '{
  "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"]
}' > tsconfig.json

update the scripts in your package.json

{
  "scripts": {
    "build": "rm -rf dist && tsc",
    "start": "npm run build && node dist/src/main.js"
  } 
}

add a main file in the src directory

// src/main.ts

import express = require('express');
import { Medusa } from 'medusa-extender';
import { resolve } from 'path';

async function bootstrap() {
    const expressInstance = express();
    
    const rootDir = resolve(__dirname) + '/../';
    await new Medusa(rootDir, expressInstance).load([]);
    
    expressInstance.listen(9000, () => {
        console.info('Server successfully started on port 9000');
    });
}

bootstrap();

And finally update the develop.sh script with the following

# develop.sh

#!/bin/bash

#Run migrations to ensure the database is updated
medusa migrations run

#Start development environment
npm run start

Decorators API

Here is the list of the provided decorators.

Decorator Description Option
@Entity(/*...*/) Decorate an entity { 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[]; };
@Validator(/*...*/) Decorate a validator { override: Type<TOverride>; };
@Migration(/*...*/) Decorate a migration
@OnMedusaEntityEvent.\*.\*(/*...*/) Can be used to send the right event type or register the handler to an event `(entity: TEntity, { async? boolean; metatype?: Type })

Contribute 🗳️

Contributions are welcome! You can look at the contribution guidelines