├── src ├── index.ts ├── models │ └── custom.ts ├── api │ ├── middleware │ │ ├── index.ts │ │ └── await-middleware.ts │ ├── routes │ │ ├── custom-route-handle.ts │ │ └── index.ts │ ├── index.ts │ ├── admin │ │ └── custom-admin-handler.ts │ └── store │ │ └── custom-store-handler.ts ├── services │ ├── __tests__ │ │ └── testService.spec.ts │ └── myService.ts ├── loaders │ └── loader.ts ├── migrations │ └── 1617703530229-restock_notification.ts └── subscribers │ └── index.ts ├── .npmignore ├── tsconfig.json ├── tsconfig.spec.json ├── .gitignore ├── tsconfig.base.json ├── README.md └── package.json /src/index.ts: -------------------------------------------------------------------------------- 1 | // noop 2 | -------------------------------------------------------------------------------- /src/models/custom.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "typeorm"; 2 | 3 | @Entity() 4 | export class MyModel {} -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .DS_store 4 | .env* 5 | src 6 | coverage 7 | 8 | tsconfig.tsbuildinfo 9 | -------------------------------------------------------------------------------- /src/api/middleware/index.ts: -------------------------------------------------------------------------------- 1 | import { default as wrap } from "./await-middleware"; 2 | 3 | export default { wrap }; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["src"], 4 | "exclude": ["node_modules", "**/*.spec.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["src/**/*.spec.ts"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /src/services/__tests__/testService.spec.ts: -------------------------------------------------------------------------------- 1 | describe('MyService', () => { 2 | it('should do this', async () => { 3 | expect(true).toBe(true); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /src/loaders/loader.ts: -------------------------------------------------------------------------------- 1 | import { AwilixContainer } from 'awilix'; 2 | 3 | export default (container: AwilixContainer, config: Record): void | Promise => { 4 | /* Implement your own loader. */ 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /lib 3 | node_modules 4 | .DS_store 5 | **/.DS_Store 6 | .env* 7 | /*.js 8 | 9 | dist 10 | coverage 11 | 12 | api/ 13 | services/ 14 | models/ 15 | subscribers/ 16 | migrations/ 17 | loaders/ 18 | index.* 19 | 20 | !src/** 21 | 22 | ./tsconfig.tsbuildinfo 23 | ./package-lock.json 24 | ./yarn.json -------------------------------------------------------------------------------- /src/api/middleware/await-middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | 3 | export default ( 4 | fn: (req: Request, res: Response) => Promise 5 | ): (req: Request, res: Response, next: NextFunction) => Promise => { 6 | return (req: Request, res: Response, next: NextFunction) => fn(req, res).catch(next); 7 | } -------------------------------------------------------------------------------- /src/api/routes/custom-route-handle.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | export default async (req: Request, res: Response): Promise>> => { 4 | try { 5 | return res.sendStatus(200); 6 | } catch (err) { 7 | return res.status(400).json({ message: err.message }); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import routes from "./routes"; 3 | 4 | /* TODO second argument pluginConfig: Record part of PR https://github.com/medusajs/medusa/pull/959 not yet in master */ 5 | export default (rootDirectory: string): Router => { 6 | const app = Router(); 7 | 8 | routes(app, rootDirectory); 9 | 10 | return app; 11 | }; 12 | -------------------------------------------------------------------------------- /src/migrations/1617703530229-restock_notification.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm"; 2 | 3 | export class MyMigration1617703530229 implements MigrationInterface { 4 | name = "myMigration1617703530229"; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | } 8 | 9 | public async down(queryRunner: QueryRunner): Promise { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/api/admin/custom-admin-handler.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, Router } from "express"; 2 | import * as bodyParser from "body-parser"; 3 | 4 | export default (): Router => { 5 | const app = Router(); 6 | app.use(bodyParser.json()); 7 | 8 | app.get("/my-custom-admin-route", async (req: Request, res: Response): Promise> => { 9 | return res.status(200).json({}); 10 | }); 11 | 12 | return app; 13 | }; -------------------------------------------------------------------------------- /src/api/store/custom-store-handler.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, Router } from "express"; 2 | import * as bodyParser from "body-parser"; 3 | 4 | export default (): Router => { 5 | const app = Router(); 6 | app.use(bodyParser.json()); 7 | 8 | app.get("/my-custom-store-route", async (req: Request, res: Response): Promise> => { 9 | return res.status(200).json({}); 10 | }); 11 | 12 | return app; 13 | }; -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es5", "es6"], 4 | "target": "esnext", 5 | "esModuleInterop": false, 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "skipDefaultLibCheck": true, 12 | "declaration": false, 13 | "sourceMap": false, 14 | "outDir": ".", 15 | "rootDir": "src", 16 | "baseUrl": "src" 17 | }, 18 | "exclude": ["node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /src/subscribers/index.ts: -------------------------------------------------------------------------------- 1 | import MyService from "../services/myService"; 2 | import { EntityManager } from "typeorm"; 3 | import { EventBusService } from "@medusajs/medusa/dist/services"; 4 | 5 | class MySubscriber { 6 | #manager: EntityManager; 7 | #myService: MyService; 8 | 9 | constructor({ manager, eventBusService, myService }: { manager: EntityManager; eventBusService: EventBusService; myService: MyService }) { 10 | this.#manager = manager; 11 | this.#myService = myService; 12 | 13 | eventBusService.subscribe("order.placed", this.handleOrderPlaced); 14 | } 15 | 16 | public async handleOrderPlaced({ id }: { id: string }): Promise { 17 | return true; 18 | } 19 | } 20 | 21 | export default MySubscriber; 22 | -------------------------------------------------------------------------------- /src/api/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import * as bodyParser from "body-parser"; 3 | import cors from "cors"; 4 | import { getConfigFile } from "medusa-core-utils"; 5 | 6 | import middlewares from "../middleware"; 7 | 8 | const route = Router() 9 | 10 | export default (app: Router, rootDirectory: string): Router => { 11 | app.use("/my-custom-route", route); 12 | 13 | const { configModule } = getConfigFile(rootDirectory, "medusa-config") as Record; 14 | const { projectConfig } = configModule as { projectConfig: { store_cors: string } }; 15 | 16 | const corsOptions = { 17 | origin: projectConfig.store_cors.split(","), 18 | credentials: true, 19 | }; 20 | 21 | route.options("/my-custom-path", cors(corsOptions)); 22 | route.post( 23 | "/my-custom-path", 24 | cors(corsOptions), 25 | bodyParser.json(), 26 | middlewares.wrap(require("./custom-route-handle").default) 27 | ); 28 | return app; 29 | } 30 | -------------------------------------------------------------------------------- /src/services/myService.ts: -------------------------------------------------------------------------------- 1 | import { BaseService } from "medusa-interfaces"; 2 | import { EntityManager } from "typeorm"; 3 | import { EventBusService } from "@medusajs/medusa/dist/services"; 4 | 5 | class MyService extends BaseService { 6 | #manager: EntityManager; 7 | #eventBusService: EventBusService; 8 | #options: Record; 9 | 10 | public transactionManager: EntityManager; 11 | 12 | constructor( 13 | { manager, eventBusService }: { manager: EntityManager; eventBusService: EventBusService }, 14 | options: Record 15 | ) { 16 | super() 17 | 18 | this.#manager = manager; 19 | 20 | this.#options = options; 21 | this.#eventBusService = eventBusService 22 | } 23 | 24 | withTransaction(transactionManager: EntityManager): MyService { 25 | if (!transactionManager) { 26 | return this; 27 | } 28 | 29 | const cloned = new MyService( 30 | { 31 | manager: transactionManager, 32 | eventBusService: this.#eventBusService 33 | }, 34 | this.#options 35 | ); 36 | 37 | cloned.transactionManager = transactionManager; 38 | 39 | return cloned; 40 | } 41 | } 42 | 43 | export default MyService; 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | Medusa 5 |

6 |

Plugin starter (Typescript)

7 |

Start to write your own plugin as quick as possible

8 | 9 | 10 | Awesome 11 | 12 |
13 | 14 | # Getting started 15 | 16 | Installation 17 | 18 | ```bash 19 | git clone git@github.com:adrien2p/medusa-plugin-starter-ts.git 20 | ``` 21 | 22 | # Usage 23 | 24 | ## Api 25 | 26 | ### Admin routes 27 | 28 | Those routes will automatically be attached by medusa to the `admin` path. 29 | 30 | ### Store routes 31 | 32 | Those routes will automatically be attached by medusa to the `store` path. 33 | 34 | ### Custom routes 35 | 36 | All those routes are added in the main router and you have to manage them. 37 | 38 | ## Models/Migrations 39 | 40 | Those models will be attach to the manager and included into the medusa container. 41 | The migrations will be applied automatically. 42 | 43 | ## Subscribers 44 | 45 | It acts like a service but its main purpose is to extends core flow depending on the 46 | events you want to listen to. 47 | 48 | ## Services 49 | 50 | Those services will be automatically added to the medusa container and will be available 51 | in any other service through the constructor injection. 52 | 53 | ## Loaders 54 | 55 | Those will be applied during the loading and allow you to register custom components 56 | to the container to be accessible later on. 57 | 58 | # Deployment 59 | 60 | Once your plugin is done. 61 | 62 | ```bash 63 | npm run build && npm version && npm publish 64 | ``` 65 | 66 | You can now install it into your project file `medusa-config`. 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "medusa-plugin-starter-ts", 3 | "version": "1.0.0", 4 | "description": "A plugin starter for medusa using typescript and pre-configured", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/adrien2p/medusa-plugin-starter-ts" 10 | }, 11 | "keywords": [ 12 | "medusa", 13 | "medusajs", 14 | "starter", 15 | "typescript", 16 | "plugin", 17 | "ecommerce", 18 | "e-commerce" 19 | ], 20 | "author": "Adrien de Peretti ", 21 | "license": "MIT", 22 | "scripts": { 23 | "clean": "./node_modules/.bin/rimraf services/ models/ migrations/ api/ subscribers/ index.js index.map.js", 24 | "build": "npm run clean && tsc -p tsconfig.json", 25 | "watch": "tsc --watch", 26 | "test": "jest" 27 | }, 28 | "devDependencies": { 29 | "@medusajs/medusa": "^1.x", 30 | "@types/express": "^4.17.13", 31 | "@types/jest": "^27.4.0", 32 | "@types/node": "^17.0.15", 33 | "cross-env": "^7.0.3", 34 | "eslint": "^8.8.0", 35 | "jest": "^27.5.0", 36 | "medusa-interfaces": "1.x", 37 | "ts-jest": "^27.1.3", 38 | "ts-loader": "^9.2.6", 39 | "typescript": "^4.5.5", 40 | "mongoose": "^6.2.0", 41 | "rimraf": "^3.0.2" 42 | }, 43 | "peerDependencies": { 44 | "medusa-interfaces": "latest", 45 | "@medusajs/medusa": "latest" 46 | }, 47 | "dependencies": { 48 | "body-parser": "^1.19.1", 49 | "cors": "^2.8.5", 50 | "express": "^4.17.2", 51 | "medusa-core-utils": "^1.1.31", 52 | "medusa-test-utils": "^1.1.37", 53 | "typeorm": "^0.2.41" 54 | }, 55 | "jest": { 56 | "globals": { 57 | "ts-jest": { 58 | "tsconfig": "tsconfig.spec.json" 59 | } 60 | }, 61 | "moduleFileExtensions": [ 62 | "js", 63 | "json", 64 | "ts" 65 | ], 66 | "testPathIgnorePatterns": [ 67 | "/node_modules/", 68 | "/node_modules/" 69 | ], 70 | "rootDir": "src", 71 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|js)$", 72 | "transform": { 73 | ".ts": "ts-jest" 74 | }, 75 | "collectCoverageFrom": [ 76 | "**/*.(t|j)s" 77 | ], 78 | "coverageDirectory": "./coverage", 79 | "testEnvironment": "node" 80 | } 81 | } 82 | --------------------------------------------------------------------------------