From e733d6b1ee4452c11bba308f1eb251ef14e0c3ef Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Thu, 16 May 2024 12:13:25 +0200 Subject: [PATCH 1/4] fix(modules-sdk): Resolve discovery location properly (#7345) --- .../src/loaders/utils/load-internal.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/core/modules-sdk/src/loaders/utils/load-internal.ts b/packages/core/modules-sdk/src/loaders/utils/load-internal.ts index 04b620b1d1..ce94162315 100644 --- a/packages/core/modules-sdk/src/loaders/utils/load-internal.ts +++ b/packages/core/modules-sdk/src/loaders/utils/load-internal.ts @@ -187,7 +187,7 @@ export async function loadModuleMigrations( const migrationScriptOptions = { moduleName: resolution.definition.key, models: moduleResources.models, - pathToMigrations: moduleResources.normalizedPath + "/dist/migrations", + pathToMigrations: moduleResources.normalizedPath + "/migrations", } runMigrations ??= ModulesSdkUtils.buildMigrationScript( @@ -238,7 +238,7 @@ async function loadResources( logger: Logger ): Promise { const modulePath = moduleResolution.resolutionPath as string - let normalizedPath = modulePath.replace("dist/", "").replace("index.js", "") + let normalizedPath = modulePath.replace("index.js", "") normalizedPath = resolve(normalizedPath) try { @@ -248,13 +248,11 @@ async function loadResources( const [moduleService, services, models, repositories] = await Promise.all([ import(modulePath).then((moduleExports) => moduleExports.default.service), - importAllFromDir(resolve(normalizedPath, "dist", "services")).catch( + importAllFromDir(resolve(normalizedPath, "services")).catch( defaultOnFail ), - importAllFromDir(resolve(normalizedPath, "dist", "models")).catch( - defaultOnFail - ), - importAllFromDir(resolve(normalizedPath, "dist", "repositories")).catch( + importAllFromDir(resolve(normalizedPath, "models")).catch(defaultOnFail), + importAllFromDir(resolve(normalizedPath, "repositories")).catch( defaultOnFail ), ]) @@ -277,7 +275,7 @@ async function loadResources( repositories: potentialRepositories, services: potentialServices, moduleResolution, - migrationPath: normalizedPath + "/dist/migrations", + migrationPath: normalizedPath + "/migrations", }) return { @@ -286,7 +284,7 @@ async function loadResources( repositories: potentialRepositories, loaders: finalLoaders, moduleService, - normalizedPath + normalizedPath, } } catch (e) { logger.warn( @@ -363,7 +361,7 @@ function prepareLoaders({ const connectionLoader = ModulesSdkUtils.mikroOrmConnectionLoaderFactory({ moduleName: moduleResolution.definition.key, moduleModels: models, - migrationsPath: migrationPath, //normalizedPath + "/dist/migrations", + migrationsPath: migrationPath, }) finalLoaders.push(connectionLoader) } From 845eda46438248b925015fe5c07b77eba0a0bc85 Mon Sep 17 00:00:00 2001 From: Stevche Radevski Date: Thu, 16 May 2024 12:30:32 +0200 Subject: [PATCH 2/4] feat: Create the Medusa API SDK as js-sdk package (#7276) --- .changeset/weak-cherries-lie.md | 5 + .eslintrc.js | 1 + packages/core/js-sdk/CHANGELOG.md | 0 packages/core/js-sdk/jest.config.js | 7 + packages/core/js-sdk/package.json | 40 +++ .../core/js-sdk/src/__tests__/client.spec.ts | 145 ++++++++++ packages/core/js-sdk/src/admin/index.ts | 8 + packages/core/js-sdk/src/client.ts | 183 +++++++++++++ packages/core/js-sdk/src/index.ts | 18 ++ packages/core/js-sdk/src/store/index.ts | 250 ++++++++++++++++++ packages/core/js-sdk/src/types.ts | 37 +++ packages/core/js-sdk/tsconfig.json | 28 ++ yarn.lock | 246 ++++++++++++++++- 13 files changed, 963 insertions(+), 5 deletions(-) create mode 100644 .changeset/weak-cherries-lie.md create mode 100644 packages/core/js-sdk/CHANGELOG.md create mode 100644 packages/core/js-sdk/jest.config.js create mode 100644 packages/core/js-sdk/package.json create mode 100644 packages/core/js-sdk/src/__tests__/client.spec.ts create mode 100644 packages/core/js-sdk/src/admin/index.ts create mode 100644 packages/core/js-sdk/src/client.ts create mode 100644 packages/core/js-sdk/src/index.ts create mode 100644 packages/core/js-sdk/src/store/index.ts create mode 100644 packages/core/js-sdk/src/types.ts create mode 100644 packages/core/js-sdk/tsconfig.json diff --git a/.changeset/weak-cherries-lie.md b/.changeset/weak-cherries-lie.md new file mode 100644 index 0000000000..48c9a97852 --- /dev/null +++ b/.changeset/weak-cherries-lie.md @@ -0,0 +1,5 @@ +--- +"@medusajs/js-sdk": patch +--- + +Introduce a js-sdk package for the Medusa API diff --git a/.eslintrc.js b/.eslintrc.js index e1bccc2377..5bbd25a6f0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -100,6 +100,7 @@ module.exports = { "./packages/core/orchestration/tsconfig.json", "./packages/core/workflows-sdk/tsconfig.spec.json", "./packages/core/modules-sdk/tsconfig.spec.json", + "./packages/core/js-sdk/tsconfig.spec.json", "./packages/core/types/tsconfig.spec.json", "./packages/core/utils/tsconfig.spec.json", "./packages/core/medusa-test-utils/tsconfig.spec.json", diff --git a/packages/core/js-sdk/CHANGELOG.md b/packages/core/js-sdk/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/core/js-sdk/jest.config.js b/packages/core/js-sdk/jest.config.js new file mode 100644 index 0000000000..c22bbc6965 --- /dev/null +++ b/packages/core/js-sdk/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + transform: { + "^.+\\.[jt]s?$": "@swc/jest", + }, + testEnvironment: `node`, + moduleFileExtensions: [`js`, `ts`, `json`], +} diff --git a/packages/core/js-sdk/package.json b/packages/core/js-sdk/package.json new file mode 100644 index 0000000000..5e7a0469e9 --- /dev/null +++ b/packages/core/js-sdk/package.json @@ -0,0 +1,40 @@ +{ + "name": "@medusajs/js-sdk", + "version": "0.0.1", + "description": "SDK for the Medusa API", + "main": "dist/index.js", + "repository": { + "type": "git", + "url": "https://github.com/medusajs/medusa", + "directory": "packages/core/js-sdk" + }, + "engines": { + "node": ">=18" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "author": "Medusa", + "license": "MIT", + "devDependencies": { + "cross-env": "^5.2.1", + "jest": "^29.6.3", + "msw": "^2.3.0", + "rimraf": "^5.0.1", + "ts-jest": "^29.1.1", + "typescript": "^5.1.6" + }, + "dependencies": { + "@medusajs/types": "^1.11.16", + "qs": "^6.12.1" + }, + "scripts": { + "prepublishOnly": "cross-env NODE_ENV=production tsc --build", + "build": "rimraf dist && tsc --build", + "test": "jest --passWithNoTests --runInBand --bail --forceExit", + "watch": "tsc --build --watch" + } +} diff --git a/packages/core/js-sdk/src/__tests__/client.spec.ts b/packages/core/js-sdk/src/__tests__/client.spec.ts new file mode 100644 index 0000000000..6714c548c5 --- /dev/null +++ b/packages/core/js-sdk/src/__tests__/client.spec.ts @@ -0,0 +1,145 @@ +import { http, HttpResponse } from "msw" +import { setupServer } from "msw/node" + +import { Client, FetchError } from "../client" + +const baseUrl = "https://someurl.com" + +// This is just a network-layer mocking, it doesn't start an actual server +const server = setupServer( + http.get(`${baseUrl}/test`, ({ request, params, cookies }) => { + return HttpResponse.json({ + test: "test", + }) + }), + http.get(`${baseUrl}/throw`, ({ request, params, cookies }) => { + return new HttpResponse(null, { + status: 500, + statusText: "Internal Server Error", + }) + }), + http.get(`${baseUrl}/header`, ({ request, params, cookies }) => { + if ( + request.headers.get("X-custom-header") === "test" && + request.headers.get("Content-Type") === "application/json" + ) { + return HttpResponse.json({ + test: "test", + }) + } + }), + http.get(`${baseUrl}/apikey`, ({ request, params, cookies }) => { + console.log(request.headers.get("authorization")) + if (request.headers.get("authorization")?.startsWith("Basic")) { + return HttpResponse.json({ + test: "test", + }) + } + }), + http.get(`${baseUrl}/pubkey`, ({ request, params, cookies }) => { + if (request.headers.get("x-medusa-pub-key") === "test-pub-key") { + return HttpResponse.json({ + test: "test", + }) + } + }), + http.post(`${baseUrl}/create`, async ({ request, params, cookies }) => { + return HttpResponse.json(await request.json()) + }), + http.delete(`${baseUrl}/delete/123`, async ({ request, params, cookies }) => { + return HttpResponse.json({ test: "test" }) + }), + http.all("*", ({ request, params, cookies }) => { + return new HttpResponse(null, { + status: 404, + statusText: "Not Found", + }) + }) +) + +describe("Client", () => { + let client: Client + beforeAll(() => { + client = new Client({ + baseUrl, + }) + + server.listen() + }) + afterEach(() => server.resetHandlers()) + afterAll(() => server.close()) + + describe("header configuration", () => { + it("should allow passing custom request headers while the defaults are preserved", async () => { + const resp = await client.fetch("header", { + headers: { "X-custom-header": "test" }, + }) + + expect(resp).toEqual({ test: "test" }) + }) + + it("should allow passing global headers", async () => { + const headClient = new Client({ + baseUrl, + globalHeaders: { + "X-custom-header": "test", + }, + }) + + const resp = await headClient.fetch("header") + expect(resp).toEqual({ test: "test" }) + }) + + it("should allow setting an API key", async () => { + const authClient = new Client({ + baseUrl, + apiKey: "test-api-key", + }) + + const resp = await authClient.fetch("apikey") + expect(resp).toEqual({ test: "test" }) + }) + + it("should allow setting a publishable key", async () => { + const pubClient = new Client({ + baseUrl, + publishableKey: "test-pub-key", + }) + + const resp = await pubClient.fetch("pubkey") + expect(resp).toEqual({ test: "test" }) + }) + }) + + describe("GET requests", () => { + it("should fire a simple GET request and get back a JSON response by default", async () => { + const resp = await client.fetch<{ test: string }>("test") + expect(resp).toEqual({ test: "test" }) + }) + + it("should throw an exception if a non-2xx status is received", async () => { + const err: FetchError = await client.fetch("throw").catch((e) => e) + expect(err.status).toEqual(500) + expect(err.message).toEqual("Internal Server Error") + }) + }) + + describe("POST requests", () => { + it("should fire a simple POST request and get back a JSON response", async () => { + const resp = await client.fetch("create", { + body: { test: "test" }, + method: "POST", + }) + expect(resp).toEqual({ test: "test" }) + }) + }) + + describe("DELETE requests", () => { + it("should fire a simple DELETE request and get back a JSON response", async () => { + const resp = await client.fetch("delete/123", { + method: "DELETE", + }) + expect(resp).toEqual({ test: "test" }) + }) + }) +}) diff --git a/packages/core/js-sdk/src/admin/index.ts b/packages/core/js-sdk/src/admin/index.ts new file mode 100644 index 0000000000..657e3d789a --- /dev/null +++ b/packages/core/js-sdk/src/admin/index.ts @@ -0,0 +1,8 @@ +import { Client } from "../client" + +export class Admin { + private client: Client + constructor(client: Client) { + this.client = client + } +} diff --git a/packages/core/js-sdk/src/client.ts b/packages/core/js-sdk/src/client.ts new file mode 100644 index 0000000000..51e70290b1 --- /dev/null +++ b/packages/core/js-sdk/src/client.ts @@ -0,0 +1,183 @@ +import qs from "qs" +import { ClientFetch, Config, FetchArgs, FetchInput, Logger } from "./types" + +const isBrowser = () => typeof window !== "undefined" + +const toBase64 = (str: string) => { + if (typeof window !== "undefined") { + return window.btoa(str) + } + + return Buffer.from(str).toString("base64") +} + +const sanitizeHeaders = (headers: Headers) => { + return { + ...Object.fromEntries(headers.entries()), + Authorization: "", + } +} + +const normalizeRequest = ( + init: FetchArgs | undefined, + headers: Headers +): RequestInit | undefined => { + let body = init?.body + if (body && headers.get("content-type")?.includes("application/json")) { + body = JSON.stringify(body) + } + + return { + ...init, + headers, + ...(body ? { body: body as RequestInit["body"] } : {}), + } as RequestInit +} + +const normalizeResponse = async (resp: Response, reqHeaders: Headers) => { + if (resp.status >= 300) { + const error = new FetchError(resp.statusText, resp.status) + throw error + } + + // If we both requested JSON, we try to parse. Otherwise, we return the raw response. + const isJsonRequest = reqHeaders.get("accept")?.includes("application/json") + return isJsonRequest ? await resp.json() : resp +} + +export class FetchError extends Error { + status: number | undefined + + constructor(message: string, status?: number) { + super(message) + this.status = status + } +} + +export class Client { + public fetch_: ClientFetch + private logger: Logger + + private DEFAULT_JWT_STORAGE_KEY = "medusa_auth_token" + private token = "" + + constructor(config: Config) { + const logger = config.logger || { + error: console.error, + warn: console.warn, + info: console.info, + debug: console.debug, + } + + this.logger = { + ...logger, + debug: config.debug ? logger.debug : () => {}, + } + + this.fetch_ = this.initClient(config) + } + + // Since the response is dynamically determined, we cannot know if it is JSON or not. Therefore, it is important to pass `Response` as the return type + fetch(input: FetchInput, init?: FetchArgs): Promise { + return this.fetch_(input, init) as unknown as Promise + } + + protected initClient(config: Config): ClientFetch { + const defaultHeaders = new Headers({ + "content-type": "application/json", + accept: "application/json", + ...this.getApiKeyHeader(config), + ...this.getPublishableKeyHeader(config), + }) + + this.logger.debug( + "Initiating Medusa client with default headers:\n", + `${JSON.stringify(sanitizeHeaders(defaultHeaders), null, 2)}\n` + ) + + return (input: FetchInput, init?: FetchArgs) => { + // We always want to fetch the up-to-date JWT token before firing off a request. + const headers = new Headers(defaultHeaders) + const customHeaders = { + ...config.globalHeaders, + ...this.getJwtTokenHeader(config), + ...init?.headers, + } + // We use `headers.set` in order to ensure headers are overwritten in a case-insensitive manner. + Object.entries(customHeaders).forEach(([key, value]) => { + headers.set(key, value) + }) + + let normalizedInput: RequestInfo | URL = input + if (input instanceof URL || typeof input === "string") { + normalizedInput = new URL(input, config.baseUrl) + if (init?.query) { + const existing = qs.parse(normalizedInput.search) + const stringifiedQuery = qs.stringify({ existing, ...init.query }) + normalizedInput.search = stringifiedQuery + } + } + + this.logger.debug( + "Performing request to:\n", + `URL: ${normalizedInput.toString()}\n`, + `Headers: ${JSON.stringify(sanitizeHeaders(headers), null, 2)}\n` + ) + + // Any non-request errors (eg. invalid JSON in the response) will be thrown as-is. + return fetch(normalizedInput, normalizeRequest(init, headers)).then( + (resp) => { + this.logger.debug(`Received response with status ${resp.status}\n`) + return normalizeResponse(resp, headers) + } + ) + } + } + + protected getApiKeyHeader = ( + config: Config + ): { Authorization: string } | {} => { + return config.apiKey + ? { Authorization: "Basic " + toBase64(config.apiKey + ":") } + : {} + } + + protected getPublishableKeyHeader = ( + config: Config + ): { "x-medusa-pub-key": string } | {} => { + return config.publishableKey + ? { "x-medusa-pub-key": config.publishableKey } + : {} + } + + protected getJwtTokenHeader = ( + config: Config + ): { Authorization: string } | {} => { + const storageMethod = + config.jwtToken?.storageMethod || (isBrowser() ? "local" : "memory") + const storageKey = + config.jwtToken?.storageKey || this.DEFAULT_JWT_STORAGE_KEY + + switch (storageMethod) { + case "local": { + if (!isBrowser()) { + throw new Error("Local JWT storage is only available in the browser") + } + const token = window.localStorage.getItem(storageKey) + return token ? { Authorization: `Bearer ${token}` } : {} + } + case "session": { + if (!isBrowser()) { + throw new Error( + "Session JWT storage is only available in the browser" + ) + } + const token = window.sessionStorage.getItem(storageKey) + return token ? { Authorization: `Bearer ${token}` } : {} + } + case "memory": { + return this.token ? { Authorization: `Bearer ${this.token}` } : {} + } + } + } +} diff --git a/packages/core/js-sdk/src/index.ts b/packages/core/js-sdk/src/index.ts new file mode 100644 index 0000000000..6d2fdeb99f --- /dev/null +++ b/packages/core/js-sdk/src/index.ts @@ -0,0 +1,18 @@ +import { Admin } from "./admin" +import { Client } from "./client" +import { Store } from "./store" +import { Config } from "./types" + +class Medusa { + public client: Client + public admin: Admin + public store: Store + + constructor(config: Config) { + this.client = new Client(config) + this.admin = new Admin(this.client) + this.store = new Store(this.client) + } +} + +export default Medusa diff --git a/packages/core/js-sdk/src/store/index.ts b/packages/core/js-sdk/src/store/index.ts new file mode 100644 index 0000000000..a8403e17fd --- /dev/null +++ b/packages/core/js-sdk/src/store/index.ts @@ -0,0 +1,250 @@ +import { Client } from "../client" +import { ClientHeaders } from "../types" + +export class Store { + private client: Client + + constructor(client: Client) { + this.client = client + } + + public region = { + list: async ( + queryParams?: Record, + headers?: ClientHeaders + ) => { + return this.client.fetch(`/store/regions`, { + query: queryParams, + headers, + }) + }, + retrieve: async ( + id: string, + queryParams?: Record, + headers?: ClientHeaders + ) => { + return this.client.fetch(`/store/regions/${id}`, { + query: queryParams, + headers, + }) + }, + } + + public collection = { + list: async ( + queryParams?: Record, + headers?: ClientHeaders + ) => { + return this.client.fetch(`/store/collections`, { + query: queryParams, + headers, + }) + }, + retrieve: async ( + id: string, + queryParams?: Record, + headers?: ClientHeaders + ) => { + return this.client.fetch(`/store/collections/${id}`, { + query: queryParams, + headers, + }) + }, + } + + public category = { + list: async ( + queryParams?: Record, + headers?: ClientHeaders + ) => { + return this.client.fetch(`/store/product-categories`, { + query: queryParams, + headers, + }) + }, + retrieve: async ( + id: string, + queryParams?: Record, + headers?: ClientHeaders + ) => { + return this.client.fetch(`/store/product-categories/${id}`, { + query: queryParams, + headers, + }) + }, + } + + public product = { + list: async ( + queryParams?: Record, + headers?: ClientHeaders + ) => { + return this.client.fetch(`/store/products`, { + query: queryParams, + headers, + }) + }, + retrieve: async ( + id: string, + queryParams?: Record, + headers?: ClientHeaders + ) => { + return this.client.fetch(`/store/products/${id}`, { + query: queryParams, + headers, + }) + }, + } + + public order = { + retrieve: async ( + id: string, + queryParams?: Record, + headers?: ClientHeaders + ) => { + return this.client.fetch(`/store/orders/${id}`, { + query: queryParams, + headers, + }) + }, + } + + public cart = { + create: async (body: any, headers?: ClientHeaders) => { + return this.client.fetch(`/store/carts`, { + headers, + method: "POST", + body, + }) + }, + update: async (id: string, body: any, headers?: ClientHeaders) => { + return this.client.fetch(`/store/carts/${id}`, { + headers, + method: "POST", + body, + }) + }, + retrieve: async ( + id: string, + queryParams?: Record, + headers?: ClientHeaders + ) => { + return this.client.fetch(`/store/carts/${id}`, { + query: queryParams, + headers, + }) + }, + createLineItem: async ( + cartId: string, + body: any, + headers?: ClientHeaders + ) => { + return this.client.fetch(`/store/carts/${cartId}/line-items`, { + headers, + method: "POST", + body, + }) + }, + updateLineItem: async ( + cartId: string, + lineItemId: string, + body: any, + headers?: ClientHeaders + ) => { + return this.client.fetch( + `/store/carts/${cartId}/line-items/${lineItemId}`, + { + headers, + method: "POST", + body, + } + ) + }, + deleteLineItem: async ( + cartId: string, + lineItemId: string, + headers?: ClientHeaders + ) => { + return this.client.fetch( + `/store/carts/${cartId}/line-items/${lineItemId}`, + { + headers, + method: "DELETE", + } + ) + }, + addShippingMethod: async ( + cartId: string, + body: any, + headers?: ClientHeaders + ) => { + return this.client.fetch(`/store/carts/${cartId}/shipping-methods`, { + headers, + method: "POST", + body, + }) + }, + complete: async (cartId: string, headers?: ClientHeaders) => { + return this.client.fetch(`/store/carts/${cartId}/complete`, { + headers, + method: "POST", + }) + }, + } + + public fulfillment = { + listCartOptions: async ( + queryParams?: Record, + headers?: ClientHeaders + ) => { + return this.client.fetch(`/store/shipping-options`, { + query: queryParams, + headers, + }) + }, + } + + public payment = { + listPaymentProviders: async ( + queryParams?: Record, + headers?: ClientHeaders + ) => { + return this.client.fetch(`/store/payment-providers`, { + query: queryParams, + headers, + }) + }, + + initiatePaymentSession: async ( + cart: any, + body: Record, + headers?: ClientHeaders + ) => { + let paymentCollectionId = (cart as any).payment_collection?.id + if (!paymentCollectionId) { + const collectionBody = { + cart_id: cart.id, + region_id: cart.region_id, + currency_code: cart.currency_code, + amount: cart.total, + } + paymentCollectionId = ( + await this.client.fetch(`/store/payment-collections`, { + headers, + method: "POST", + body: collectionBody, + }) + ).payment_collection.id + } + + return this.client.fetch( + `/store/payment-collections/${paymentCollectionId}/payment-sessions`, + { + headers, + method: "POST", + body, + } + ) + }, + } +} diff --git a/packages/core/js-sdk/src/types.ts b/packages/core/js-sdk/src/types.ts new file mode 100644 index 0000000000..c3fa4b1097 --- /dev/null +++ b/packages/core/js-sdk/src/types.ts @@ -0,0 +1,37 @@ +export type Logger = { + error: (...messages: string[]) => void + warn: (...messages: string[]) => void + info: (...messages: string[]) => void + debug: (...messages: string[]) => void +} + +export type Config = { + baseUrl: string + globalHeaders?: ClientHeaders + publishableKey?: string + apiKey?: string + jwtToken?: { + storageKey?: string + // TODO: Add support for cookie storage + storageMethod?: "local" | "session" | "memory" + } + logger?: Logger + debug?: boolean +} + +export type FetchParams = Parameters + +export type ClientHeaders = Record + +export type FetchInput = FetchParams[0] + +export type FetchArgs = Omit & { + query?: Record + headers?: ClientHeaders + body?: RequestInit["body"] | Record +} + +export type ClientFetch = ( + input: FetchInput, + init?: FetchArgs +) => Promise diff --git a/packages/core/js-sdk/tsconfig.json b/packages/core/js-sdk/tsconfig.json new file mode 100644 index 0000000000..d41dc00d5c --- /dev/null +++ b/packages/core/js-sdk/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "lib": ["es2021"], + "target": "es2021", + "outDir": "./dist", + "esModuleInterop": true, + "declaration": true, + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true, + "noImplicitReturns": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "allowJs": true, + "skipLibCheck": true, + "downlevelIteration": true // to use ES5 specific tooling + }, + "include": ["./src/**/*"], + "exclude": [ + "./dist/**/*", + "./src/**/__tests__", + "./src/**/__mocks__", + "node_modules" + ] +} diff --git a/yarn.lock b/yarn.lock index 26c2bee234..7cc06b870d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2861,6 +2861,24 @@ __metadata: languageName: node linkType: hard +"@bundled-es-modules/cookie@npm:^2.0.0": + version: 2.0.0 + resolution: "@bundled-es-modules/cookie@npm:2.0.0" + dependencies: + cookie: ^0.5.0 + checksum: 0655dd331b35d7b5b6dd2301c3bcfb7233018c0e3235a40ced1d53f00463ab92dc01f0091f153812867bc0ef0f8e0a157a30acb16e8d7ef149702bf8db9fe7a6 + languageName: node + linkType: hard + +"@bundled-es-modules/statuses@npm:^1.0.1": + version: 1.0.1 + resolution: "@bundled-es-modules/statuses@npm:1.0.1" + dependencies: + statuses: ^2.0.1 + checksum: c1a8ede3efa8da61ccda4b98e773582a9733edfbeeee569d4630785f8e018766202edb190a754a3ec7a7f6bd738e857829affc2fdb676b6dab4db1bb44e62785 + languageName: node + linkType: hard + "@changesets/apply-release-plan@npm:^7.0.0": version: 7.0.0 resolution: "@changesets/apply-release-plan@npm:7.0.0" @@ -4059,6 +4077,37 @@ __metadata: languageName: node linkType: hard +"@inquirer/confirm@npm:^3.0.0": + version: 3.1.7 + resolution: "@inquirer/confirm@npm:3.1.7" + dependencies: + "@inquirer/core": ^8.2.0 + "@inquirer/type": ^1.3.1 + checksum: e500fb3b39564a738b4403eb611621bab68a6dcf546d0c936f28e9c28e19cfb140eaa7b66f693ab2dff553257dbd7d8b1e1f6761674e078d7880d738f78f931f + languageName: node + linkType: hard + +"@inquirer/core@npm:^8.2.0": + version: 8.2.0 + resolution: "@inquirer/core@npm:8.2.0" + dependencies: + "@inquirer/figures": ^1.0.1 + "@inquirer/type": ^1.3.1 + "@types/mute-stream": ^0.0.4 + "@types/node": ^20.12.11 + "@types/wrap-ansi": ^3.0.0 + ansi-escapes: ^4.3.2 + chalk: ^4.1.2 + cli-spinners: ^2.9.2 + cli-width: ^4.1.0 + mute-stream: ^1.0.0 + signal-exit: ^4.1.0 + strip-ansi: ^6.0.1 + wrap-ansi: ^6.2.0 + checksum: 76db6c437789481147fd2c40f8fb63892b963c86de8e3e9837a443ff40737d528480add1ecf791e375e968efd037d59621c88e9957d60ba8de866822c2fc8b4d + languageName: node + linkType: hard + "@inquirer/figures@npm:^1.0.1": version: 1.0.1 resolution: "@inquirer/figures@npm:1.0.1" @@ -4066,6 +4115,13 @@ __metadata: languageName: node linkType: hard +"@inquirer/type@npm:^1.3.1": + version: 1.3.1 + resolution: "@inquirer/type@npm:1.3.1" + checksum: 7dbf7ca10f758f2b6dbc7b7302ce01e79596747692468805c340afa0bf608adecbe33cd3c3b2b806bb3987cadf233b52ead7652b479a052455bc06855849f97f + languageName: node + linkType: hard + "@internationalized/date@npm:^3.5.3": version: 3.5.3 resolution: "@internationalized/date@npm:3.5.3" @@ -5451,6 +5507,21 @@ __metadata: languageName: node linkType: hard +"@medusajs/js-sdk@workspace:packages/core/js-sdk": + version: 0.0.0-use.local + resolution: "@medusajs/js-sdk@workspace:packages/core/js-sdk" + dependencies: + "@medusajs/types": ^1.11.16 + cross-env: ^5.2.1 + jest: ^29.6.3 + msw: ^2.3.0 + qs: ^6.12.1 + rimraf: ^5.0.1 + ts-jest: ^29.1.1 + typescript: ^5.1.6 + languageName: unknown + linkType: soft + "@medusajs/link-modules@^0.2.11, @medusajs/link-modules@workspace:packages/modules/link-modules": version: 0.0.0-use.local resolution: "@medusajs/link-modules@workspace:packages/modules/link-modules" @@ -6521,6 +6592,27 @@ __metadata: languageName: node linkType: hard +"@mswjs/cookies@npm:^1.1.0": + version: 1.1.0 + resolution: "@mswjs/cookies@npm:1.1.0" + checksum: c8442b77f4d4f72c63a29049bbd33e7f9d85517471c09e1a1a71f424e5261feee5311b096d42d4447a51f199017b2227feb2b5dd77da83b733917560ace58940 + languageName: node + linkType: hard + +"@mswjs/interceptors@npm:^0.29.0": + version: 0.29.1 + resolution: "@mswjs/interceptors@npm:0.29.1" + dependencies: + "@open-draft/deferred-promise": ^2.2.0 + "@open-draft/logger": ^0.3.0 + "@open-draft/until": ^2.0.0 + is-node-process: ^1.2.0 + outvariant: ^1.2.1 + strict-event-emitter: ^0.5.1 + checksum: 816660a17b0e89e6e6955072b96882b5807c8c9faa316eab27104e8ba80e8e7d78b1862af42e1044156a5ae3ae2071289dc9211ecdc8fd5f7078d8c8a8a7caa3 + languageName: node + linkType: hard + "@ndelangen/get-tarball@npm:^3.0.7": version: 3.0.9 resolution: "@ndelangen/get-tarball@npm:3.0.9" @@ -6762,6 +6854,30 @@ __metadata: languageName: node linkType: hard +"@open-draft/deferred-promise@npm:^2.2.0": + version: 2.2.0 + resolution: "@open-draft/deferred-promise@npm:2.2.0" + checksum: eafc1b1d0fc8edb5e1c753c5e0f3293410b40dde2f92688211a54806d4136887051f39b98c1950370be258483deac9dfd17cf8b96557553765198ef2547e4549 + languageName: node + linkType: hard + +"@open-draft/logger@npm:^0.3.0": + version: 0.3.0 + resolution: "@open-draft/logger@npm:0.3.0" + dependencies: + is-node-process: ^1.2.0 + outvariant: ^1.4.0 + checksum: 90010647b22e9693c16258f4f9adb034824d1771d3baa313057b9a37797f571181005bc50415a934eaf7c891d90ff71dcd7a9d5048b0b6bb438f31bef2c7c5c1 + languageName: node + linkType: hard + +"@open-draft/until@npm:^2.0.0, @open-draft/until@npm:^2.1.0": + version: 2.1.0 + resolution: "@open-draft/until@npm:2.1.0" + checksum: 61d3f99718dd86bb393fee2d7a785f961dcaf12f2055f0c693b27f4d0cd5f7a03d498a6d9289773b117590d794a43cd129366fd8e99222e4832f67b1653d54cf + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -11080,6 +11196,13 @@ __metadata: languageName: node linkType: hard +"@types/cookie@npm:^0.6.0": + version: 0.6.0 + resolution: "@types/cookie@npm:0.6.0" + checksum: 5b326bd0188120fb32c0be086b141b1481fec9941b76ad537f9110e10d61ee2636beac145463319c71e4be67a17e85b81ca9e13ceb6e3bb63b93d16824d6c149 + languageName: node + linkType: hard + "@types/cross-spawn@npm:^6.0.2": version: 6.0.6 resolution: "@types/cross-spawn@npm:6.0.6" @@ -11423,6 +11546,15 @@ __metadata: languageName: node linkType: hard +"@types/mute-stream@npm:^0.0.4": + version: 0.0.4 + resolution: "@types/mute-stream@npm:0.0.4" + dependencies: + "@types/node": "*" + checksum: 944730fd7b398c5078de3c3d4d0afeec8584283bc694da1803fdfca14149ea385e18b1b774326f1601baf53898ce6d121a952c51eb62d188ef6fcc41f725c0dc + languageName: node + linkType: hard + "@types/node-emoji@npm:^1.8.2": version: 1.8.2 resolution: "@types/node-emoji@npm:1.8.2" @@ -11489,6 +11621,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.12.11": + version: 20.12.12 + resolution: "@types/node@npm:20.12.12" + dependencies: + undici-types: ~5.26.4 + checksum: f374b763c744e8f16e4f38cf6e2c0eef31781ec9228c9e43a6f267880fea420fab0a238b59f10a7cb3444e49547c5e3785787e371fc242307310995b21988812 + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.0": version: 2.4.4 resolution: "@types/normalize-package-data@npm:2.4.4" @@ -11677,6 +11818,13 @@ __metadata: languageName: node linkType: hard +"@types/statuses@npm:^2.0.4": + version: 2.0.5 + resolution: "@types/statuses@npm:2.0.5" + checksum: 4dacec0b29483a44be902a022a11a22b339de7a6e7b2059daa4f7add10cb6dbcc28d02d2a416fe9687e48d335906bf983065391836d4e7c847e55ddef4de8fad + languageName: node + linkType: hard + "@types/stripe@npm:^8.0.417": version: 8.0.417 resolution: "@types/stripe@npm:8.0.417" @@ -11762,6 +11910,13 @@ __metadata: languageName: node linkType: hard +"@types/wrap-ansi@npm:^3.0.0": + version: 3.0.0 + resolution: "@types/wrap-ansi@npm:3.0.0" + checksum: 8d8f53363f360f38135301a06b596c295433ad01debd082078c33c6ed98b05a5c8fe8853a88265432126096084f4a135ec1564e3daad631b83296905509f90b3 + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 21.0.3 resolution: "@types/yargs-parser@npm:21.0.3" @@ -14520,7 +14675,7 @@ __metadata: languageName: node linkType: hard -"cli-spinners@npm:^2.5.0, cli-spinners@npm:^2.6.1": +"cli-spinners@npm:^2.5.0, cli-spinners@npm:^2.6.1, cli-spinners@npm:^2.9.2": version: 2.9.2 resolution: "cli-spinners@npm:2.9.2" checksum: 907a1c227ddf0d7a101e7ab8b300affc742ead4b4ebe920a5bf1bc6d45dce2958fcd195eb28fa25275062fe6fa9b109b93b63bc8033396ed3bcb50297008b3a3 @@ -15151,6 +15306,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:^0.5.0": + version: 0.5.0 + resolution: "cookie@npm:0.5.0" + checksum: c01ca3ef8d7b8187bae434434582288681273b5a9ed27521d4d7f9f7928fe0c920df0decd9f9d3bbd2d14ac432b8c8cf42b98b3bdd5bfe0e6edddeebebe8b61d + languageName: node + linkType: hard + "cookiejar@npm:^2.1.0": version: 2.1.4 resolution: "cookiejar@npm:2.1.4" @@ -19243,7 +19405,7 @@ __metadata: languageName: node linkType: hard -"graphql@npm:^16.6.0": +"graphql@npm:^16.6.0, graphql@npm:^16.8.1": version: 16.8.1 resolution: "graphql@npm:16.8.1" checksum: 129c318156b466f440914de80dbf7bc67d17f776f2a088a40cb0da611d19a97c224b1c6d2b13cbcbc6e5776e45ed7468b8432f9c3536724e079b44f1a3d57a8a @@ -19502,6 +19664,13 @@ __metadata: languageName: node linkType: hard +"headers-polyfill@npm:^4.0.2": + version: 4.0.3 + resolution: "headers-polyfill@npm:4.0.3" + checksum: 53e85b2c6385f8d411945fb890c5369f1469ce8aa32a6e8d28196df38568148de640c81cf88cbc7c67767103dd9acba48f4f891982da63178fc6e34560022afe + languageName: node + linkType: hard + "helpertypes@npm:^0.0.19": version: 0.0.19 resolution: "helpertypes@npm:0.0.19" @@ -20627,6 +20796,13 @@ __metadata: languageName: node linkType: hard +"is-node-process@npm:^1.2.0": + version: 1.2.0 + resolution: "is-node-process@npm:1.2.0" + checksum: 5b24fda6776d00e42431d7bcd86bce81cb0b6cabeb944142fe7b077a54ada2e155066ad06dbe790abdb397884bdc3151e04a9707b8cd185099efbc79780573ed + languageName: node + linkType: hard + "is-number-object@npm:^1.0.4": version: 1.0.7 resolution: "is-number-object@npm:1.0.7" @@ -24804,6 +24980,38 @@ __metadata: languageName: node linkType: hard +"msw@npm:^2.3.0": + version: 2.3.0 + resolution: "msw@npm:2.3.0" + dependencies: + "@bundled-es-modules/cookie": ^2.0.0 + "@bundled-es-modules/statuses": ^1.0.1 + "@inquirer/confirm": ^3.0.0 + "@mswjs/cookies": ^1.1.0 + "@mswjs/interceptors": ^0.29.0 + "@open-draft/until": ^2.1.0 + "@types/cookie": ^0.6.0 + "@types/statuses": ^2.0.4 + chalk: ^4.1.2 + graphql: ^16.8.1 + headers-polyfill: ^4.0.2 + is-node-process: ^1.2.0 + outvariant: ^1.4.2 + path-to-regexp: ^6.2.0 + strict-event-emitter: ^0.5.1 + type-fest: ^4.9.0 + yargs: ^17.7.2 + peerDependencies: + typescript: ">= 4.7.x" + peerDependenciesMeta: + typescript: + optional: true + bin: + msw: cli/index.js + checksum: 704d808741c7a7abc8757406816fd8fffa5450c1cdf8669355e7d01748c372818c61b4bf6fab3ffce5c3ad32e25302737da664e079973a18becb10396989f933 + languageName: node + linkType: hard + "multer@npm:^1.4.5-lts.1": version: 1.4.5-lts.1 resolution: "multer@npm:1.4.5-lts.1" @@ -24838,7 +25046,7 @@ __metadata: languageName: node linkType: hard -"mute-stream@npm:1.0.0": +"mute-stream@npm:1.0.0, mute-stream@npm:^1.0.0": version: 1.0.0 resolution: "mute-stream@npm:1.0.0" checksum: dce2a9ccda171ec979a3b4f869a102b1343dee35e920146776780de182f16eae459644d187e38d59a3d37adf85685e1c17c38cf7bfda7e39a9880f7a1d10a74c @@ -25805,6 +26013,13 @@ __metadata: languageName: node linkType: hard +"outvariant@npm:^1.2.1, outvariant@npm:^1.4.0, outvariant@npm:^1.4.2": + version: 1.4.2 + resolution: "outvariant@npm:1.4.2" + checksum: 48041425a4cb725ff8871b7d9889bfc2eaded867b9b35b6c2450a36fb3632543173098654990caa6c9e9f67d902b2a01f4402c301835e9ecaf4b4695d3161853 + languageName: node + linkType: hard + "p-cancelable@npm:^2.0.0": version: 2.1.1 resolution: "p-cancelable@npm:2.1.1" @@ -26223,6 +26438,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:^6.2.0": + version: 6.2.2 + resolution: "path-to-regexp@npm:6.2.2" + checksum: 4b60852d3501fd05ca9dd08c70033d73844e5eca14e41f499f069afa8364f780f15c5098002f93bd42af8b3514de62ac6e82a53b5662de881d2b08c9ef21ea6b + languageName: node + linkType: hard + "path-type@npm:^4.0.0": version: 4.0.0 resolution: "path-type@npm:4.0.0" @@ -27562,7 +27784,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.10.0, qs@npm:^6.10.3, qs@npm:^6.11.0, qs@npm:^6.11.2, qs@npm:^6.12.0, qs@npm:^6.5.1": +"qs@npm:^6.10.0, qs@npm:^6.10.3, qs@npm:^6.11.0, qs@npm:^6.11.2, qs@npm:^6.12.0, qs@npm:^6.12.1, qs@npm:^6.5.1": version: 6.12.1 resolution: "qs@npm:6.12.1" dependencies: @@ -30273,7 +30495,7 @@ __metadata: languageName: node linkType: hard -"statuses@npm:2.0.1": +"statuses@npm:2.0.1, statuses@npm:^2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" checksum: 34378b207a1620a24804ce8b5d230fea0c279f00b18a7209646d5d47e419d1cc23e7cbf33a25a1e51ac38973dc2ac2e1e9c647a8e481ef365f77668d72becfd0 @@ -30390,6 +30612,13 @@ __metadata: languageName: node linkType: hard +"strict-event-emitter@npm:^0.5.1": + version: 0.5.1 + resolution: "strict-event-emitter@npm:0.5.1" + checksum: f5228a6e6b6393c57f52f62e673cfe3be3294b35d6f7842fc24b172ae0a6e6c209fa83241d0e433fc267c503bc2f4ffdbe41a9990ff8ffd5ac425ec0489417f7 + languageName: node + linkType: hard + "string-argv@npm:0.3.1": version: 0.3.1 resolution: "string-argv@npm:0.3.1" @@ -32008,6 +32237,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^4.9.0": + version: 4.18.2 + resolution: "type-fest@npm:4.18.2" + checksum: 5e669128bf7cbc9f9cea4e4862c974517a1d9f77652589c2ac0908a8be5d852d4e52593ed14f4d8a44a604fb5e8a8ec1b658e461acd8bb7592f5e5265a04cbab + languageName: node + linkType: hard + "type-is@npm:^1.6.4, type-is@npm:~1.6.17, type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" From 2e42e053d4c9d5445d227bdc268c66713aad8e2e Mon Sep 17 00:00:00 2001 From: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Thu, 16 May 2024 12:36:30 +0200 Subject: [PATCH 3/4] chore: Publish preview release + deploy staging every 3 hours (#7341) * wip * Wrap up * Update .github/workflows/trigger-staging-deployment.yml Co-authored-by: Sebastian Rindom * enable schedule --------- Co-authored-by: Sebastian Rindom --- .changeset/little-books-reply.md | 59 +++++++++++++++++++ .github/workflows/trigger-release.yml | 15 ++--- .../workflows/trigger-staging-deployment.yml | 18 ++++++ 3 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 .changeset/little-books-reply.md create mode 100644 .github/workflows/trigger-staging-deployment.yml diff --git a/.changeset/little-books-reply.md b/.changeset/little-books-reply.md new file mode 100644 index 0000000000..9a63169911 --- /dev/null +++ b/.changeset/little-books-reply.md @@ -0,0 +1,59 @@ +--- +"babel-preset-medusa-package": patch +"create-medusa-app": patch +"@medusajs/medusa-cli": patch +"medusa-dev-cli": patch +"@medusajs/medusa-oas-cli": patch +"@medusajs/oas-github-ci": patch +"@medusajs/openapi-typescript-codegen": patch +"@medusajs/core-flows": patch +"medusa-test-utils": patch +"@medusajs/modules-sdk": patch +"@medusajs/orchestration": patch +"@medusajs/types": patch +"@medusajs/utils": patch +"@medusajs/workflows-sdk": patch +"@medusajs/icons": patch +"@medusajs/toolbox": patch +"@medusajs/ui": patch +"@medusajs/ui-preset": patch +"@medusajs/medusa": patch +"medusa-core-utils": patch +"medusa-interfaces": patch +"medusa-telemetry": patch +"@medusajs/api-key": patch +"@medusajs/auth": patch +"@medusajs/cache-inmemory": patch +"@medusajs/cache-redis": patch +"@medusajs/cart": patch +"@medusajs/currency": patch +"@medusajs/customer": patch +"@medusajs/event-bus-local": patch +"@medusajs/event-bus-redis": patch +"@medusajs/file": patch +"@medusajs/fulfillment": patch +"@medusajs/inventory-next": patch +"@medusajs/link-modules": patch +"@medusajs/notification": patch +"@medusajs/order": patch +"@medusajs/payment": patch +"@medusajs/pricing": patch +"@medusajs/product": patch +"@medusajs/promotion": patch +"@medusajs/file-local-next": patch +"@medusajs/file-s3": patch +"@medusajs/fulfillment-manual": patch +"@medusajs/notification-local": patch +"@medusajs/notification-sendgrid": patch +"@medusajs/payment-stripe": patch +"@medusajs/region": patch +"@medusajs/sales-channel": patch +"@medusajs/stock-location-next": patch +"@medusajs/store": patch +"@medusajs/tax": patch +"@medusajs/user": patch +"@medusajs/workflow-engine-inmemory": patch +"@medusajs/workflow-engine-redis": patch +--- + +chore: Preview release changeset diff --git a/.github/workflows/trigger-release.yml b/.github/workflows/trigger-release.yml index e7c67b5674..cfd6cc1af7 100644 --- a/.github/workflows/trigger-release.yml +++ b/.github/workflows/trigger-release.yml @@ -19,13 +19,14 @@ on: - "docs/**" - "www/**" - ".github/**" - + schedule: + - cron: "0 */3 * * *" concurrency: ${{ github.workflow }}-${{ github.ref }} jobs: release: - name: Trigger Release + name: Trigger Preview Release runs-on: ubuntu-latest steps: - name: Checkout Repo @@ -50,16 +51,16 @@ jobs: - name: Install Dependencies run: yarn - - name: Build all packages - run: yarn build - - name: Version packages - run: yarn changeset version --snapshot ${{ github.event.inputs.version }} + run: yarn changeset version --snapshot ${{ github.event.inputs.version || 'preview' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Install Dependencies run: yarn install --no-immutable + + - name: Build all packages + run: yarn build - name: Publish packages under next tag - run: yarn changeset publish --no-git-tags --snapshot --tag ${{ github.event.inputs.version }} + run: yarn changeset publish --no-git-tags --snapshot --tag ${{ github.event.inputs.version || 'preview' }} diff --git a/.github/workflows/trigger-staging-deployment.yml b/.github/workflows/trigger-staging-deployment.yml new file mode 100644 index 0000000000..b08caa5881 --- /dev/null +++ b/.github/workflows/trigger-staging-deployment.yml @@ -0,0 +1,18 @@ +name: Trigger Staging Deployment + +on: + workflow_run: + workflows: [Trigger Release and Publish] + types: + - completed + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - run: | + curl -X POST \ + -H "Authorization: Bearer ${{ secrets.STAGING_DEPLOY_ACCESS_TOKEN }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/medusajs/staging/actions/workflows/deploy.yml/dispatches \ + -d '{"ref":"main"}' \ No newline at end of file From ee924b1b28bc5759347901c393ce18d8004d5b86 Mon Sep 17 00:00:00 2001 From: Stevche Radevski Date: Thu, 16 May 2024 13:36:09 +0200 Subject: [PATCH 4/4] feat: Add a simple configurable notifications subscriber (#7331) * feat: Add a simple configurable notifications subscriber that is configurable * Proposal on awaiting all subscribers to run * fix: Clean up wait subscribers util and notifications test --------- Co-authored-by: adrien2p --- .../notification/admin/notification.spec.ts | 255 +++++++++++------- packages/core/medusa-test-utils/src/events.ts | 28 ++ packages/core/medusa-test-utils/src/index.ts | 3 +- .../src/module-test-runner.ts | 2 +- .../src/__mocks__/stock-location-module.ts | 2 +- packages/core/modules-sdk/src/definitions.ts | 10 +- .../abstract-module-service-factory.ts | 7 +- .../api-v2/hooks/payment/[provider]/route.ts | 2 +- .../subscribers/order-notifier.ts | 17 -- .../subscribers/product-updater.ts | 20 -- .../subscribers/variant-created.ts | 17 -- .../helpers/subscribers/__mocks__/index.ts | 16 -- .../subscribers/__tests__/index.spec.ts | 115 -------- .../src/loaders/helpers/subscribers/index.ts | 248 ----------------- packages/medusa/src/loaders/index.ts | 13 +- .../src/loaders/load-medusa-project-apis.ts | 164 ----------- packages/medusa/src/loaders/subscribers.ts | 13 +- .../subscribers/configurable-notifications.ts | 64 +++++ .../medusa/src/subscribers/payment-webhook.ts | 13 +- packages/modules/api-key/src/types/index.ts | 2 +- packages/modules/currency/src/types/index.ts | 2 +- .../modules/fulfillment/src/types/index.ts | 2 +- packages/modules/order/src/types/index.ts | 2 +- .../__tests__/services.spec.ts | 2 +- .../notification-local/src/services/local.ts | 6 +- .../src/services/stock-location-module.ts | 8 +- packages/modules/store/src/types/index.ts | 2 +- 27 files changed, 295 insertions(+), 740 deletions(-) create mode 100644 packages/core/medusa-test-utils/src/events.ts delete mode 100644 packages/medusa/src/loaders/helpers/subscribers/__fixtures__/subscribers/order-notifier.ts delete mode 100644 packages/medusa/src/loaders/helpers/subscribers/__fixtures__/subscribers/product-updater.ts delete mode 100644 packages/medusa/src/loaders/helpers/subscribers/__fixtures__/subscribers/variant-created.ts delete mode 100644 packages/medusa/src/loaders/helpers/subscribers/__mocks__/index.ts delete mode 100644 packages/medusa/src/loaders/helpers/subscribers/__tests__/index.spec.ts delete mode 100644 packages/medusa/src/loaders/helpers/subscribers/index.ts delete mode 100644 packages/medusa/src/loaders/load-medusa-project-apis.ts create mode 100644 packages/medusa/src/subscribers/configurable-notifications.ts diff --git a/integration-tests/modules/__tests__/notification/admin/notification.spec.ts b/integration-tests/modules/__tests__/notification/admin/notification.spec.ts index 1d264e0cce..75940d269e 100644 --- a/integration-tests/modules/__tests__/notification/admin/notification.spec.ts +++ b/integration-tests/modules/__tests__/notification/admin/notification.spec.ts @@ -1,11 +1,12 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { CreateNotificationDTO, + IEventBusModuleService, INotificationModuleService, Logger, } from "@medusajs/types" import { ContainerRegistrationKeys } from "@medusajs/utils" -import { medusaIntegrationTestRunner } from "medusa-test-utils" +import { medusaIntegrationTestRunner, TestEventUtils } from "medusa-test-utils" jest.setTimeout(50000) @@ -13,7 +14,7 @@ const env = { MEDUSA_FF_MEDUSA_V2: true } medusaIntegrationTestRunner({ env, testSuite: ({ getContainer }) => { - describe("Notification module", () => { + describe("Notifications", () => { let service: INotificationModuleService let logger: Logger @@ -26,102 +27,168 @@ medusaIntegrationTestRunner({ jest.restoreAllMocks() }) - it("should successfully send a notification for an available channel", async () => { - const logSpy = jest.spyOn(logger, "info") - const notification = { - to: "test@medusajs.com", - channel: "email", - template: "order-created", - data: { username: "john-doe" }, - trigger_type: "order-created", - resource_id: "order-id", - resource_type: "order", - } as CreateNotificationDTO - - const result = await service.create(notification) - const fromDB = await service.retrieve(result.id) - - expect(result).toEqual( - expect.objectContaining({ - id: expect.any(String), - to: "test@medusajs.com", - provider_id: "local-notification-provider", - }) - ) - - delete fromDB.original_notification_id - delete fromDB.external_id - delete fromDB.receiver_id - delete (fromDB as any).idempotency_key - delete (fromDB as any).provider - - expect(result).toEqual(fromDB) - expect(logSpy).toHaveBeenCalledWith( - 'Attempting to send a notification to: test@medusajs.com on the channel: email with template: order-created and data: {"username":"john-doe"}' - ) - }) - - it("should throw an exception if there is no provider for the channel", async () => { - const notification = { - to: "test@medusajs.com", - channel: "sms", - } as CreateNotificationDTO - - const error = await service.create(notification).catch((e) => e) - expect(error.message).toEqual( - "Could not find a notification provider for channel: sms" - ) - }) - - it("should allow listing all notifications with filters", async () => { - const notification1 = { - to: "test@medusajs.com", - channel: "email", - template: "order-created", - } as CreateNotificationDTO - - const notification2 = { - to: "test@medusajs.com", - channel: "log", - template: "product-created", - } as CreateNotificationDTO - - await service.create([notification1, notification2]) - - const notifications = await service.list({ channel: "log" }) - expect(notifications).toHaveLength(1) - expect(notifications[0]).toEqual( - expect.objectContaining({ - to: "test@medusajs.com", - channel: "log", - template: "product-created", - }) - ) - }) - - it("should allow retrieving a notification", async () => { - const notification1 = { - to: "test@medusajs.com", - channel: "email", - template: "order-created", - } as CreateNotificationDTO - - const notification2 = { - to: "test@medusajs.com", - channel: "log", - template: "product-created", - } as CreateNotificationDTO - - const [first] = await service.create([notification1, notification2]) - - const notification = await service.retrieve(first.id) - expect(notification).toEqual( - expect.objectContaining({ + describe("Notifications module", () => { + it("should successfully send a notification for an available channel", async () => { + const logSpy = jest.spyOn(logger, "info") + const notification = { to: "test@medusajs.com", channel: "email", template: "order-created", + data: { username: "john-doe" }, + trigger_type: "order-created", + resource_id: "order-id", + resource_type: "order", + } as CreateNotificationDTO + + const result = await service.create(notification) + const fromDB = await service.retrieve(result.id) + + expect(result).toEqual( + expect.objectContaining({ + id: expect.any(String), + to: "test@medusajs.com", + provider_id: "local-notification-provider", + }) + ) + + expect(result).toEqual( + expect.objectContaining({ + to: "test@medusajs.com", + channel: "email", + data: { + username: "john-doe", + }, + id: expect.any(String), + provider_id: "local-notification-provider", + resource_id: "order-id", + resource_type: "order", + template: "order-created", + trigger_type: "order-created", + }) + ) + + expect(fromDB).toEqual( + expect.objectContaining({ + to: "test@medusajs.com", + channel: "email", + data: { + username: "john-doe", + }, + id: expect.any(String), + provider_id: "local-notification-provider", + resource_id: "order-id", + resource_type: "order", + template: "order-created", + trigger_type: "order-created", + }) + ) + + expect(logSpy).toHaveBeenCalledWith( + `Attempting to send a notification to: 'test@medusajs.com' on the channel: 'email' with template: 'order-created' and data: '{\"username\":\"john-doe\"}'` + ) + }) + + it("should throw an exception if there is no provider for the channel", async () => { + const notification = { + to: "test@medusajs.com", + channel: "sms", + } as CreateNotificationDTO + + const error = await service.create(notification).catch((e) => e) + expect(error.message).toEqual( + "Could not find a notification provider for channel: sms" + ) + }) + + it("should allow listing all notifications with filters", async () => { + const notification1 = { + to: "test@medusajs.com", + channel: "email", + template: "order-created", + } as CreateNotificationDTO + + const notification2 = { + to: "test@medusajs.com", + channel: "log", + template: "product-created", + } as CreateNotificationDTO + + await service.create([notification1, notification2]) + + const notifications = await service.list({ channel: "log" }) + expect(notifications).toHaveLength(1) + expect(notifications[0]).toEqual( + expect.objectContaining({ + to: "test@medusajs.com", + channel: "log", + template: "product-created", + }) + ) + }) + + it("should allow retrieving a notification", async () => { + const notification1 = { + to: "test@medusajs.com", + channel: "email", + template: "order-created", + } as CreateNotificationDTO + + const notification2 = { + to: "test@medusajs.com", + channel: "log", + template: "product-created", + } as CreateNotificationDTO + + const [first] = await service.create([notification1, notification2]) + + const notification = await service.retrieve(first.id) + expect(notification).toEqual( + expect.objectContaining({ + to: "test@medusajs.com", + channel: "email", + template: "order-created", + }) + ) + }) + }) + + describe("Configurable notification subscriber", () => { + let eventBus: IEventBusModuleService + beforeAll(async () => { + eventBus = getContainer().resolve(ModuleRegistrationName.EVENT_BUS) + }) + + it("should successfully sent a notification when an order is created (based on configuration)", async () => { + const subscriberExecution = TestEventUtils.waitSubscribersExecution( + "order.created", + eventBus + ) + const logSpy = jest.spyOn(logger, "info") + + await eventBus.emit("order.created", { + data: { + order: { + id: "1234", + email: "test@medusajs.com", + }, + }, }) - ) + await subscriberExecution + + const notifications = await service.list() + + expect(logSpy).toHaveBeenLastCalledWith( + `Attempting to send a notification to: 'test@medusajs.com' on the channel: 'email' with template: 'order-created-template' and data: '{\"order_id\":\"1234\"}'` + ) + expect(notifications).toHaveLength(1) + expect(notifications[0]).toEqual( + expect.objectContaining({ + to: "test@medusajs.com", + channel: "email", + template: "order-created-template", + }) + ) + }) }) }) }, diff --git a/packages/core/medusa-test-utils/src/events.ts b/packages/core/medusa-test-utils/src/events.ts new file mode 100644 index 0000000000..ab1f6d1abd --- /dev/null +++ b/packages/core/medusa-test-utils/src/events.ts @@ -0,0 +1,28 @@ +import { IEventBusModuleService } from "@medusajs/types" + +// Allows you to wait for all subscribers to execute for a given event. Only works with the local event bus. +export const waitSubscribersExecution = ( + eventName: string, + eventBus: IEventBusModuleService +) => { + const subscriberPromises: Promise[] = [] + + ;(eventBus as any).eventEmitter_.listeners(eventName).forEach((listener) => { + ;(eventBus as any).eventEmitter_.removeListener("order.created", listener) + + let ok, nok + const promise = new Promise((resolve, reject) => { + ok = resolve + nok = reject + }) + subscriberPromises.push(promise) + + const newListener = async (...args2) => { + return await listener.apply(eventBus, args2).then(ok).catch(nok) + } + + ;(eventBus as any).eventEmitter_.on("order.created", newListener) + }) + + return Promise.all(subscriberPromises) +} diff --git a/packages/core/medusa-test-utils/src/index.ts b/packages/core/medusa-test-utils/src/index.ts index 46cd657210..5d965d06d2 100644 --- a/packages/core/medusa-test-utils/src/index.ts +++ b/packages/core/medusa-test-utils/src/index.ts @@ -1,6 +1,7 @@ export * as TestDatabaseUtils from "./database" -export { default as IdMap } from "./id-map" +export * as TestEventUtils from "./events" export * as JestUtils from "./jest" +export { default as IdMap } from "./id-map" export { default as MockManager } from "./mock-manager" export { default as MockRepository } from "./mock-repository" export * from "./init-modules" diff --git a/packages/core/medusa-test-utils/src/module-test-runner.ts b/packages/core/medusa-test-utils/src/module-test-runner.ts index 27acd6273f..2d5313e334 100644 --- a/packages/core/medusa-test-utils/src/module-test-runner.ts +++ b/packages/core/medusa-test-utils/src/module-test-runner.ts @@ -78,7 +78,7 @@ export function moduleIntegrationTestRunner({ const moduleOptions_: InitModulesOptions = { injectedDependencies: { [ContainerRegistrationKeys.PG_CONNECTION]: connection, - eventBusService: new MockEventBusService(), + ["eventBusModuleService"]: new MockEventBusService(), [ContainerRegistrationKeys.LOGGER]: console, ...injectedDependencies, }, diff --git a/packages/core/modules-sdk/src/__mocks__/stock-location-module.ts b/packages/core/modules-sdk/src/__mocks__/stock-location-module.ts index 97137c4e73..6b2d6c3995 100644 --- a/packages/core/modules-sdk/src/__mocks__/stock-location-module.ts +++ b/packages/core/modules-sdk/src/__mocks__/stock-location-module.ts @@ -6,7 +6,7 @@ export const StockLocationModule = { label: "StockLocationService", isRequired: false, isQueryable: true, - dependencies: ["eventBusService"], + dependencies: ["eventBusModuleService"], defaultModuleDeclaration: { scope: "internal", resources: "shared", diff --git a/packages/core/modules-sdk/src/definitions.ts b/packages/core/modules-sdk/src/definitions.ts index 687d4d58a4..294f574a03 100644 --- a/packages/core/modules-sdk/src/definitions.ts +++ b/packages/core/modules-sdk/src/definitions.ts @@ -113,7 +113,7 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = label: upperCaseFirst(ModuleRegistrationName.STOCK_LOCATION), isRequired: false, isQueryable: true, - dependencies: ["eventBusService"], + dependencies: [ModuleRegistrationName.EVENT_BUS], defaultModuleDeclaration: { scope: MODULE_SCOPE.INTERNAL, resources: MODULE_RESOURCE_TYPE.SHARED, @@ -126,7 +126,7 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = label: upperCaseFirst(ModuleRegistrationName.INVENTORY), isRequired: false, isQueryable: true, - dependencies: ["eventBusService"], + dependencies: [ModuleRegistrationName.EVENT_BUS], defaultModuleDeclaration: { scope: MODULE_SCOPE.INTERNAL, resources: MODULE_RESOURCE_TYPE.SHARED, @@ -228,7 +228,7 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = label: upperCaseFirst(ModuleRegistrationName.FULFILLMENT), isRequired: false, isQueryable: true, - dependencies: ["logger", "eventBusService"], + dependencies: ["logger", ModuleRegistrationName.EVENT_BUS], defaultModuleDeclaration: { scope: MODULE_SCOPE.INTERNAL, resources: MODULE_RESOURCE_TYPE.SHARED, @@ -306,7 +306,7 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = label: upperCaseFirst(ModuleRegistrationName.ORDER), isRequired: false, isQueryable: true, - dependencies: ["logger", "eventBusService"], + dependencies: ["logger", ModuleRegistrationName.EVENT_BUS], defaultModuleDeclaration: { scope: MODULE_SCOPE.INTERNAL, resources: MODULE_RESOURCE_TYPE.SHARED, @@ -319,7 +319,7 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = label: upperCaseFirst(ModuleRegistrationName.TAX), isRequired: false, isQueryable: true, - dependencies: ["logger", "eventBusService"], + dependencies: ["logger", ModuleRegistrationName.EVENT_BUS], defaultModuleDeclaration: { scope: MODULE_SCOPE.INTERNAL, resources: MODULE_RESOURCE_TYPE.SHARED, diff --git a/packages/core/utils/src/modules-sdk/abstract-module-service-factory.ts b/packages/core/utils/src/modules-sdk/abstract-module-service-factory.ts index c300ad53c7..3273dbd085 100644 --- a/packages/core/utils/src/modules-sdk/abstract-module-service-factory.ts +++ b/packages/core/utils/src/modules-sdk/abstract-module-service-factory.ts @@ -571,13 +571,8 @@ export function abstractModuleServiceFactory< // TODO: Should use ModuleRegistrationName.EVENT_BUS but it would require to move it to the utils package to prevent circular dependencies (key) => key === "eventBusModuleService" ) - const hasEventBusService = Object.keys(this.__container__).find( - (key) => key === "eventBusService" - ) - this.eventBusModuleService_ = hasEventBusService - ? this.__container__.eventBusService - : hasEventBusModuleService + this.eventBusModuleService_ = hasEventBusModuleService ? this.__container__.eventBusModuleService : undefined } diff --git a/packages/medusa/src/api-v2/hooks/payment/[provider]/route.ts b/packages/medusa/src/api-v2/hooks/payment/[provider]/route.ts index fb3a592200..a02d3f010e 100644 --- a/packages/medusa/src/api-v2/hooks/payment/[provider]/route.ts +++ b/packages/medusa/src/api-v2/hooks/payment/[provider]/route.ts @@ -16,7 +16,7 @@ export const POST = async (req: MedusaRequest, res: MedusaResponse) => { payload: { data: req.body, rawData: req.rawBody, headers: req.headers }, } - const eventBus = req.scope.resolve("eventBusService") + const eventBus = req.scope.resolve(ModuleRegistrationName.EVENT_BUS) // we delay the processing of the event to avoid a conflict caused by a race condition await eventBus.emit(PaymentWebhookEvents.WebhookReceived, event, { diff --git a/packages/medusa/src/loaders/helpers/subscribers/__fixtures__/subscribers/order-notifier.ts b/packages/medusa/src/loaders/helpers/subscribers/__fixtures__/subscribers/order-notifier.ts deleted file mode 100644 index 537c50683e..0000000000 --- a/packages/medusa/src/loaders/helpers/subscribers/__fixtures__/subscribers/order-notifier.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - SubscriberArgs, - SubscriberConfig, -} from "../../../../../types/subscribers" - -export default async function orderNotifier({ - data, - eventName, - container, - pluginOptions, -}: SubscriberArgs) { - return Promise.resolve() -} - -export const config: SubscriberConfig = { - event: ["order.placed", "order.canceled", "order.completed"], -} diff --git a/packages/medusa/src/loaders/helpers/subscribers/__fixtures__/subscribers/product-updater.ts b/packages/medusa/src/loaders/helpers/subscribers/__fixtures__/subscribers/product-updater.ts deleted file mode 100644 index 67e536783b..0000000000 --- a/packages/medusa/src/loaders/helpers/subscribers/__fixtures__/subscribers/product-updater.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { - SubscriberArgs, - SubscriberConfig, -} from "../../../../../types/subscribers" - -export default async function productUpdater({ - data, - eventName, - container, - pluginOptions, -}: SubscriberArgs) { - return Promise.resolve() -} - -export const config: SubscriberConfig = { - event: "product.updated", - context: { - subscriberId: "product-updater", - }, -} diff --git a/packages/medusa/src/loaders/helpers/subscribers/__fixtures__/subscribers/variant-created.ts b/packages/medusa/src/loaders/helpers/subscribers/__fixtures__/subscribers/variant-created.ts deleted file mode 100644 index 874290d550..0000000000 --- a/packages/medusa/src/loaders/helpers/subscribers/__fixtures__/subscribers/variant-created.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - SubscriberArgs, - SubscriberConfig, -} from "../../../../../types/subscribers" - -export default async function ({ - data, - eventName, - container, - pluginOptions, -}: SubscriberArgs) { - return Promise.resolve() -} - -export const config: SubscriberConfig = { - event: "variant.created", -} diff --git a/packages/medusa/src/loaders/helpers/subscribers/__mocks__/index.ts b/packages/medusa/src/loaders/helpers/subscribers/__mocks__/index.ts deleted file mode 100644 index 85a5b11fb1..0000000000 --- a/packages/medusa/src/loaders/helpers/subscribers/__mocks__/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const eventBusServiceMock = { - subscribe: jest.fn().mockImplementation((...args) => { - return Promise.resolve(args) - }), -} - -export const containerMock = { - // mock .resolve method so if its called with "eventBusService" it returns the mock - resolve: jest.fn().mockImplementation((name: string) => { - if (name === "eventBusModuleService") { - return eventBusServiceMock - } else { - return {} - } - }), -} diff --git a/packages/medusa/src/loaders/helpers/subscribers/__tests__/index.spec.ts b/packages/medusa/src/loaders/helpers/subscribers/__tests__/index.spec.ts deleted file mode 100644 index 77927b4aca..0000000000 --- a/packages/medusa/src/loaders/helpers/subscribers/__tests__/index.spec.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { MedusaContainer } from "@medusajs/types" -import { join } from "path" -import { containerMock, eventBusServiceMock } from "../__mocks__" -import { SubscriberLoader } from "../index" - -describe("SubscriberLoader", () => { - const rootDir = join(__dirname, "../__fixtures__", "subscribers") - - const pluginOptions = { - important_data: { - enabled: true, - }, - } - - let registeredPaths: string[] = [] - - beforeAll(async () => { - jest.clearAllMocks() - - const paths = await new SubscriberLoader( - rootDir, - containerMock as unknown as MedusaContainer, - pluginOptions, - "id-load-subscribers" - ).load() - - if (paths) { - registeredPaths = [...registeredPaths, ...paths] - } - }) - - it("should register each subscriber in the '/subscribers' folder", async () => { - // As '/subscribers' contains 3 subscribers, we expect the number of registered paths to be 3 - expect(registeredPaths.length).toEqual(3) - }) - - it("should have registered subscribers for 5 events", async () => { - /** - * The 'product-updater.ts' subscriber is registered for the following events: - * - "product.created" - * The 'order-updater.ts' subscriber is registered for the following events: - * - "order.placed" - * - "order.canceled" - * - "order.completed" - * The 'variant-created.ts' subscriber is registered for the following events: - * - "variant.created" - * - * This means that we expect the eventBusServiceMock.subscribe method to have - * been called times, once for 'product-updater.ts', once for 'variant-created.ts', - * and 3 times for 'order-updater.ts'. - */ - expect(eventBusServiceMock.subscribe).toHaveBeenCalledTimes(5) - }) - - it("should have registered subscribers with the correct props", async () => { - /** - * The 'product-updater.ts' subscriber is registered - * with a explicit subscriberId of "product-updater". - */ - expect(eventBusServiceMock.subscribe).toHaveBeenCalledWith( - "product.updated", - expect.any(Function), - { - subscriberId: "product-updater", - } - ) - - /** - * The 'order-updater.ts' subscriber is registered - * without an explicit subscriberId, which means that - * the loader tries to infer one from either the handler - * functions name or the file name. In this case, the - * handler function is named 'orderUpdater' and is used - * to infer the subscriberId. - */ - expect(eventBusServiceMock.subscribe).toHaveBeenCalledWith( - "order.placed", - expect.any(Function), - { - subscriberId: "order-notifier", - } - ) - - expect(eventBusServiceMock.subscribe).toHaveBeenCalledWith( - "order.canceled", - expect.any(Function), - { - subscriberId: "order-notifier", - } - ) - - expect(eventBusServiceMock.subscribe).toHaveBeenCalledWith( - "order.completed", - expect.any(Function), - { - subscriberId: "order-notifier", - } - ) - - /** - * The 'variant-created.ts' subscriber is registered - * without an explicit subscriberId, and with an anonymous - * handler function. This means that the loader tries to - * infer the subscriberId from the file name, which in this - * case is 'variant-created.ts'. - */ - expect(eventBusServiceMock.subscribe).toHaveBeenCalledWith( - "variant.created", - expect.any(Function), - { - subscriberId: "variant-created", - } - ) - }) -}) diff --git a/packages/medusa/src/loaders/helpers/subscribers/index.ts b/packages/medusa/src/loaders/helpers/subscribers/index.ts deleted file mode 100644 index cc74e51416..0000000000 --- a/packages/medusa/src/loaders/helpers/subscribers/index.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { MedusaContainer, Subscriber } from "@medusajs/types" -import { kebabCase } from "@medusajs/utils" -import { readdir } from "fs/promises" -import { extname, join, sep } from "path" - -import { SubscriberArgs, SubscriberConfig } from "../../../types/subscribers" -import logger from "../../logger" -import { IEventBusModuleService } from "@medusajs/types" - -type SubscriberHandler = (args: SubscriberArgs) => Promise - -type SubscriberModule = { - config: SubscriberConfig - handler: SubscriberHandler -} - -export class SubscriberLoader { - protected container_: MedusaContainer - protected pluginOptions_: Record - protected activityId_: string - protected rootDir_: string - protected excludes: RegExp[] = [ - /\.DS_Store/, - /(\.ts\.map|\.js\.map|\.d\.ts)/, - /^_[^/\\]*(\.[^/\\]+)?$/, - ] - - protected subscriberDescriptors_: Map> = - new Map() - - constructor( - rootDir: string, - container: MedusaContainer, - options: Record = {}, - activityId: string - ) { - this.rootDir_ = rootDir - this.pluginOptions_ = options - this.container_ = container - this.activityId_ = activityId - } - - private validateSubscriber( - subscriber: any, - path: string - ): subscriber is { - default: SubscriberHandler - config: SubscriberConfig - } { - const handler = subscriber.default - - if (!handler || typeof handler !== "function") { - /** - * If the handler is not a function, we can't use it - */ - logger.warn(`The subscriber in ${path} is not a function.`) - return false - } - - const config = subscriber.config - - if (!config) { - /** - * If the subscriber is missing a config, we can't use it - */ - logger.warn(`The subscriber in ${path} is missing a config.`) - return false - } - - if (!config.event) { - /** - * If the subscriber is missing an event, we can't use it. - * In production we throw an error, else we log a warning - */ - if (process.env.NODE_ENV === "production") { - throw new Error(`The subscriber in ${path} is missing an event.`) - } else { - logger.warn(`The subscriber in ${path} is missing an event.`) - } - - return false - } - - if ( - typeof config.event !== "string" && - !Array.isArray(config.event) && - !config.event.every((e: unknown) => typeof e === "string") - ) { - /** - * If the subscribers event is not a string or an array of strings, we can't use it - */ - logger.warn( - `The subscriber in ${path} has an invalid event. The event must be a string or an array of strings.` - ) - return false - } - - return true - } - - private async createDescriptor(absolutePath: string, entry: string) { - return await import(absolutePath).then((module_) => { - const isValid = this.validateSubscriber(module_, absolutePath) - - if (!isValid) { - return - } - - this.subscriberDescriptors_.set(absolutePath, { - config: module_.config, - handler: module_.default, - }) - }) - } - - private async createMap(dirPath: string) { - await Promise.all( - await readdir(dirPath, { withFileTypes: true }).then(async (entries) => { - return entries - .filter((entry) => { - if ( - this.excludes.length && - this.excludes.some((exclude) => exclude.test(entry.name)) - ) { - return false - } - - return true - }) - .map(async (entry) => { - const fullPath = join(dirPath, entry.name) - - if (entry.isDirectory()) { - return this.createMap(fullPath) - } - - return await this.createDescriptor(fullPath, entry.name) - }) - }) - ) - } - - private inferIdentifier( - fileName: string, - config: SubscriberConfig, - handler: SubscriberHandler - ) { - const { context } = config - - /** - * If subscriberId is provided, use that - */ - if (context?.subscriberId) { - return context.subscriberId - } - - const handlerName = handler.name - - /** - * If the handler is not anonymous, use the name - */ - if ( - handlerName && - !(handlerName.startsWith("default") || handlerName.startsWith("_default")) - ) { - return kebabCase(handlerName) - } - - /** - * If the handler is anonymous, use the file name - */ - const idFromFile = - fileName.split(sep).pop()?.replace(extname(fileName), "") ?? "" - - return kebabCase(idFromFile) - } - - private createSubscriber({ - fileName, - config, - handler, - }: { - fileName: string - config: SubscriberConfig - handler: SubscriberHandler - }) { - const eventBusService: IEventBusModuleService = this.container_.resolve( - ModuleRegistrationName.EVENT_BUS - ) - - const { event } = config - const events = Array.isArray(event) ? event : [event] - - const subscriber = async (data: T, eventName: string) => { - return handler({ - eventName, - data, - container: this.container_, - pluginOptions: this.pluginOptions_, - }) - } - - const subscriberId = this.inferIdentifier(fileName, config, handler) - - for (const e of events) { - const obj = { - ...(config.context ?? {}), - subscriberId, - } - - eventBusService.subscribe(e, subscriber as Subscriber, obj) - } - } - - async load() { - let hasSubscriberDir = false - - try { - await readdir(this.rootDir_) - hasSubscriberDir = true - } catch (err) { - hasSubscriberDir = false - } - - if (!hasSubscriberDir) { - return - } - - await this.createMap(this.rootDir_) - - const map = this.subscriberDescriptors_ - - for (const [fileName, { config, handler }] of map.entries()) { - this.createSubscriber({ - fileName, - config, - handler, - }) - } - - /** - * Return the file paths of the registered subscribers, to prevent the - * backwards compatible loader from trying to register them. - */ - return [...map.keys()] - } -} diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index 4321fc3241..62c58ca10c 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -13,11 +13,10 @@ import loadConfig from "./config" import expressLoader from "./express" import featureFlagsLoader from "./feature-flags" import { registerProjectWorkflows } from "./helpers/register-workflows" -import medusaProjectApisLoader from "./load-medusa-project-apis" import Logger from "./logger" import loadMedusaApp from "./medusa-app" import pgConnectionLoader from "./pg-connection" -// import subscribersLoader from "./subscribers" +import subscribersLoader from "./subscribers" type Options = { directory: string @@ -57,7 +56,7 @@ async function loadEntrypoints( await adminLoader({ app: expressApp, configModule }) - // subscribersLoader({ container }) + subscribersLoader({ container }) await apiLoader({ container, @@ -110,14 +109,6 @@ export default async ({ featureFlagRouter ) - await medusaProjectApisLoader({ - rootDirectory, - container, - app: expressApp, - configModule, - activityId: "medusa-project-apis", - }) - await createDefaultsWorkflow(container).run() const shutdown = async () => { diff --git a/packages/medusa/src/loaders/load-medusa-project-apis.ts b/packages/medusa/src/loaders/load-medusa-project-apis.ts deleted file mode 100644 index a98e7a355f..0000000000 --- a/packages/medusa/src/loaders/load-medusa-project-apis.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { promiseAll } from "@medusajs/utils" -import { Express } from "express" -import glob from "glob" -import { trackInstallation } from "medusa-telemetry" -import { EOL } from "os" -import path from "path" -import { Logger, MedusaContainer } from "../types/global" -import { getResolvedPlugins } from "./helpers/resolve-plugins" -import { RoutesLoader } from "./helpers/routing" -import { SubscriberLoader } from "./helpers/subscribers" -import logger from "./logger" -import { ConfigModule } from "@medusajs/types" - -type Options = { - rootDirectory: string - container: MedusaContainer - configModule: ConfigModule - app: Express - activityId: string -} - -type PluginDetails = { - resolve: string - name: string - id: string - options: Record - version: string -} - -export const MEDUSA_PROJECT_NAME = "project-plugin" - -/** - * Registers all services in the services directory - */ -export default async ({ - rootDirectory, - container, - app, - configModule, - activityId, -}: Options): Promise => { - const resolved = getResolvedPlugins(rootDirectory, configModule) || [] - - const shouldStartAPI = configModule.projectConfig.worker_mode !== "worker" - - await promiseAll( - resolved.map(async (pluginDetails) => { - if (shouldStartAPI) { - await registerApi( - pluginDetails, - app, - container, - configModule, - activityId - ) - } - await registerSubscribers(pluginDetails, container, activityId) - }) - ) - - await promiseAll( - resolved.map(async (pluginDetails) => runLoaders(pluginDetails, container)) - ) - - if (configModule.projectConfig.redis_url) { - await Promise.all( - resolved.map(async (pluginDetails) => { - // await registerScheduledJobs(pluginDetails, container) - // TODO: Decide how scheduled jobs will be loaded and handled - }) - ) - } else { - logger.warn( - "You don't have Redis configured. Scheduled jobs will not be enabled." - ) - } - - resolved.forEach((plugin) => trackInstallation(plugin.name, "plugin")) -} - -async function runLoaders( - pluginDetails: PluginDetails, - container: MedusaContainer -): Promise { - const loaderFiles = glob.sync( - `${pluginDetails.resolve}/loaders/[!__]*.js`, - {} - ) - await promiseAll( - loaderFiles.map(async (loader) => { - try { - const module = require(loader).default - if (typeof module === "function") { - await module(container, pluginDetails.options) - } - } catch (err) { - const logger = container.resolve("logger") - logger.warn(`Running loader failed: ${err.message}`) - return Promise.resolve() - } - }) - ) -} - -/** - * Registers the plugin's api routes. - */ -async function registerApi( - pluginDetails: PluginDetails, - app: Express, - container: MedusaContainer, - configmodule: ConfigModule, - activityId: string -): Promise { - const logger = container.resolve("logger") - const projectName = - pluginDetails.name === MEDUSA_PROJECT_NAME - ? "your Medusa project" - : `${pluginDetails.name}` - - logger.progress(activityId, `Registering custom endpoints for ${projectName}`) - - try { - /** - * Register the plugin's API routes using the file based routing. - */ - await new RoutesLoader({ - app, - rootDir: path.join(pluginDetails.resolve, "api"), - activityId: activityId, - configModule: configmodule, - }).load() - } catch (err) { - logger.warn( - `An error occurred while registering API Routes in ${projectName}${ - err.stack ? EOL + err.stack : "" - }` - ) - } - - return app -} - -/** - * Registers a plugin's subscribers at the right location in our container. - * Subscribers are registered directly in the container. - * @param {object} pluginDetails - the plugin details including plugin options, - * version, id, resolved path, etc. See resolvePlugin - * @param {object} container - the container where the services will be - * registered - * @return {void} - */ -async function registerSubscribers( - pluginDetails: PluginDetails, - container: MedusaContainer, - activityId: string -): Promise { - await new SubscriberLoader( - path.join(pluginDetails.resolve, "subscribers"), - container, - pluginDetails.options, - activityId - ).load() -} diff --git a/packages/medusa/src/loaders/subscribers.ts b/packages/medusa/src/loaders/subscribers.ts index 0c7eb6a017..88350475ad 100644 --- a/packages/medusa/src/loaders/subscribers.ts +++ b/packages/medusa/src/loaders/subscribers.ts @@ -2,21 +2,24 @@ import glob from "glob" import path from "path" import { asFunction } from "awilix" import { MedusaContainer } from "../types/global" +import { MedusaError } from "@medusajs/utils" /** * Registers all subscribers in the subscribers directory */ export default ({ container }: { container: MedusaContainer }) => { - const isTest = process.env.NODE_ENV === "test" - - const corePath = isTest - ? "../subscribers/__mocks__/*.js" - : "../subscribers/*.js" + const corePath = "../subscribers/*.js" const coreFull = path.join(__dirname, corePath) const core = glob.sync(coreFull, { cwd: __dirname }) core.forEach((fn) => { const loaded = require(fn).default + if (!loaded) { + throw new MedusaError( + MedusaError.Types.UNEXPECTED_STATE, + `Subscriber ${fn} does not have a default export` + ) + } container.build(asFunction((cradle) => new loaded(cradle)).singleton()) }) diff --git a/packages/medusa/src/subscribers/configurable-notifications.ts b/packages/medusa/src/subscribers/configurable-notifications.ts new file mode 100644 index 0000000000..95e1663a8e --- /dev/null +++ b/packages/medusa/src/subscribers/configurable-notifications.ts @@ -0,0 +1,64 @@ +import { IEventBusService, INotificationModuleService } from "@medusajs/types" +import { get } from "lodash" + +type InjectedDependencies = { + notificationModuleService: INotificationModuleService + eventBusModuleService: IEventBusService +} + +// TODO: The config should be loaded dynamically from medusa-config.js +// TODO: We can use a more powerful templating syntax to allow for eg. combining fields. +const config = [ + { + event: "order.created", + template: "order-created-template", + channel: "email", + to: "order.email", + resource_id: "order.id", + data: { + order_id: "order.id", + }, + }, +] + +class ConfigurableNotificationsSubscriber { + private readonly eventBusModuleService_: IEventBusService + private readonly notificationModuleService_: INotificationModuleService + + constructor({ + eventBusModuleService, + notificationModuleService, + }: InjectedDependencies) { + this.eventBusModuleService_ = eventBusModuleService + this.notificationModuleService_ = notificationModuleService + + config.forEach((eventHandler) => { + this.eventBusModuleService_.subscribe( + eventHandler.event, + async (data: any) => { + const payload = data.data + + const notificationData = { + template: eventHandler.template, + channel: eventHandler.channel, + to: get(payload, eventHandler.to), + trigger_type: eventHandler.event, + resource_id: get(payload, eventHandler.resource_id), + data: Object.entries(eventHandler.data).reduce( + (acc, [key, value]) => { + acc[key] = get(payload, value) + return acc + }, + {} + ), + } + + await this.notificationModuleService_.create(notificationData) + return + } + ) + }) + } +} + +export default ConfigurableNotificationsSubscriber diff --git a/packages/medusa/src/subscribers/payment-webhook.ts b/packages/medusa/src/subscribers/payment-webhook.ts index 3863b603ed..1e4a7c995f 100644 --- a/packages/medusa/src/subscribers/payment-webhook.ts +++ b/packages/medusa/src/subscribers/payment-webhook.ts @@ -14,18 +14,21 @@ type SerializedBuffer = { type InjectedDependencies = { paymentModuleService: IPaymentModuleService - eventBusService: IEventBusService + eventBusModuleService: IEventBusService } class PaymentWebhookSubscriber { - private readonly eventBusService_: IEventBusService + private readonly eventBusModuleService_: IEventBusService private readonly paymentModuleService_: IPaymentModuleService - constructor({ eventBusService, paymentModuleService }: InjectedDependencies) { - this.eventBusService_ = eventBusService + constructor({ + eventBusModuleService, + paymentModuleService, + }: InjectedDependencies) { + this.eventBusModuleService_ = eventBusModuleService this.paymentModuleService_ = paymentModuleService - this.eventBusService_.subscribe( + this.eventBusModuleService_.subscribe( PaymentWebhookEvents.WebhookReceived, this.processEvent as Subscriber ) diff --git a/packages/modules/api-key/src/types/index.ts b/packages/modules/api-key/src/types/index.ts index f3b2746996..ea71f85559 100644 --- a/packages/modules/api-key/src/types/index.ts +++ b/packages/modules/api-key/src/types/index.ts @@ -3,7 +3,7 @@ import { IEventBusModuleService, Logger } from "@medusajs/types" export type InitializeModuleInjectableDependencies = { logger?: Logger - eventBusService?: IEventBusModuleService + eventBusModuleService?: IEventBusModuleService } export type CreateApiKeyDTO = { diff --git a/packages/modules/currency/src/types/index.ts b/packages/modules/currency/src/types/index.ts index fdac085753..d692c28400 100644 --- a/packages/modules/currency/src/types/index.ts +++ b/packages/modules/currency/src/types/index.ts @@ -2,5 +2,5 @@ import { IEventBusModuleService, Logger } from "@medusajs/types" export type InitializeModuleInjectableDependencies = { logger?: Logger - eventBusService?: IEventBusModuleService + eventBusModuleService?: IEventBusModuleService } diff --git a/packages/modules/fulfillment/src/types/index.ts b/packages/modules/fulfillment/src/types/index.ts index 758ea00de6..c80d690b5a 100644 --- a/packages/modules/fulfillment/src/types/index.ts +++ b/packages/modules/fulfillment/src/types/index.ts @@ -7,7 +7,7 @@ import { export type InitializeModuleInjectableDependencies = { logger?: Logger - eventBusService?: IEventBusModuleService + eventBusModuleService?: IEventBusModuleService } export const FulfillmentIdentifiersRegistrationName = diff --git a/packages/modules/order/src/types/index.ts b/packages/modules/order/src/types/index.ts index 3a35723392..640e25dc05 100644 --- a/packages/modules/order/src/types/index.ts +++ b/packages/modules/order/src/types/index.ts @@ -14,5 +14,5 @@ export * from "./utils" export type InitializeModuleInjectableDependencies = { logger?: Logger - eventBusService?: IEventBusModuleService + eventBusModuleService?: IEventBusModuleService } diff --git a/packages/modules/providers/notification-local/integration-tests/__tests__/services.spec.ts b/packages/modules/providers/notification-local/integration-tests/__tests__/services.spec.ts index aecec1ef69..ed0fca0872 100644 --- a/packages/modules/providers/notification-local/integration-tests/__tests__/services.spec.ts +++ b/packages/modules/providers/notification-local/integration-tests/__tests__/services.spec.ts @@ -30,7 +30,7 @@ describe("Local notification provider", () => { expect(logSpy).toHaveBeenCalled() expect(logSpy).toHaveBeenCalledWith( - 'Attempting to send a notification to: test@medusajs.com on the channel: email with template: some-template and data: {"username":"john-doe"}' + `Attempting to send a notification to: 'test@medusajs.com' on the channel: 'email' with template: 'some-template' and data: '{\"username\":\"john-doe\"}'` ) }) }) diff --git a/packages/modules/providers/notification-local/src/services/local.ts b/packages/modules/providers/notification-local/src/services/local.ts index 6210272597..af99b57fa3 100644 --- a/packages/modules/providers/notification-local/src/services/local.ts +++ b/packages/modules/providers/notification-local/src/services/local.ts @@ -38,9 +38,9 @@ export class LocalNotificationService extends AbstractNotificationProviderServic } const message = - `Attempting to send a notification to: ${notification.to}` + - ` on the channel: ${notification.channel} with template: ${notification.template}` + - ` and data: ${JSON.stringify(notification.data)}` + `Attempting to send a notification to: '${notification.to}'` + + ` on the channel: '${notification.channel}' with template: '${notification.template}'` + + ` and data: '${JSON.stringify(notification.data)}'` this.logger_.info(message) return {} diff --git a/packages/modules/stock-location-next/src/services/stock-location-module.ts b/packages/modules/stock-location-next/src/services/stock-location-module.ts index f2be165779..0dd07ee875 100644 --- a/packages/modules/stock-location-next/src/services/stock-location-module.ts +++ b/packages/modules/stock-location-next/src/services/stock-location-module.ts @@ -26,7 +26,7 @@ import { UpsertStockLocationInput } from "@medusajs/types" import { promiseAll } from "@medusajs/utils" type InjectedDependencies = { - eventBusService: IEventBusService + eventBusModuleService: IEventBusService baseRepository: DAL.RepositoryService stockLocationService: ModulesSdkTypes.InternalModuleService stockLocationAddressService: ModulesSdkTypes.InternalModuleService @@ -52,14 +52,14 @@ export default class StockLocationModuleService< >(StockLocation, generateMethodForModels, entityNameToLinkableKeysMap) implements IStockLocationServiceNext { - protected readonly eventBusService_: IEventBusService + protected readonly eventBusModuleService_: IEventBusService protected baseRepository_: DAL.RepositoryService protected readonly stockLocationService_: ModulesSdkTypes.InternalModuleService protected readonly stockLocationAddressService_: ModulesSdkTypes.InternalModuleService constructor( { - eventBusService, + eventBusModuleService, baseRepository, stockLocationService, stockLocationAddressService, @@ -72,7 +72,7 @@ export default class StockLocationModuleService< this.baseRepository_ = baseRepository this.stockLocationService_ = stockLocationService this.stockLocationAddressService_ = stockLocationAddressService - this.eventBusService_ = eventBusService + this.eventBusModuleService_ = eventBusModuleService } __joinerConfig(): ModuleJoinerConfig { diff --git a/packages/modules/store/src/types/index.ts b/packages/modules/store/src/types/index.ts index 93ec9a4524..bfd3dc482c 100644 --- a/packages/modules/store/src/types/index.ts +++ b/packages/modules/store/src/types/index.ts @@ -3,7 +3,7 @@ import { IEventBusModuleService, Logger } from "@medusajs/types" export type InitializeModuleInjectableDependencies = { logger?: Logger - eventBusService?: IEventBusModuleService + eventBusModuleService?: IEventBusModuleService } export type UpdateStoreInput = StoreTypes.UpdateStoreDTO & { id: string }