Merge branch 'develop' into fix/2152
This commit is contained in:
commit
9298a57be1
44 files changed
+1351
-762
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "@swc/jest",
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `ts`, `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"
|
||||
}
|
||||
}
|
||||
@@ -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<any>("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<any>("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<any>("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<any>("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<any>("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<any>("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<any>("delete/123", {
|
||||
method: "DELETE",
|
||||
})
|
||||
expect(resp).toEqual({ test: "test" })
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Client } from "../client"
|
||||
|
||||
export class Admin {
|
||||
private client: Client
|
||||
constructor(client: Client) {
|
||||
this.client = client
|
||||
}
|
||||
}
|
||||
@@ -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: "<REDACTED>",
|
||||
}
|
||||
}
|
||||
|
||||
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<T extends any>(input: FetchInput, init?: FetchArgs): Promise<T> {
|
||||
return this.fetch_(input, init) as unknown as Promise<T>
|
||||
}
|
||||
|
||||
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}` } : {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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<string, any>,
|
||||
headers?: ClientHeaders
|
||||
) => {
|
||||
return this.client.fetch<any>(`/store/regions`, {
|
||||
query: queryParams,
|
||||
headers,
|
||||
})
|
||||
},
|
||||
retrieve: async (
|
||||
id: string,
|
||||
queryParams?: Record<string, any>,
|
||||
headers?: ClientHeaders
|
||||
) => {
|
||||
return this.client.fetch<any>(`/store/regions/${id}`, {
|
||||
query: queryParams,
|
||||
headers,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
public collection = {
|
||||
list: async (
|
||||
queryParams?: Record<string, any>,
|
||||
headers?: ClientHeaders
|
||||
) => {
|
||||
return this.client.fetch<any>(`/store/collections`, {
|
||||
query: queryParams,
|
||||
headers,
|
||||
})
|
||||
},
|
||||
retrieve: async (
|
||||
id: string,
|
||||
queryParams?: Record<string, any>,
|
||||
headers?: ClientHeaders
|
||||
) => {
|
||||
return this.client.fetch<any>(`/store/collections/${id}`, {
|
||||
query: queryParams,
|
||||
headers,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
public category = {
|
||||
list: async (
|
||||
queryParams?: Record<string, any>,
|
||||
headers?: ClientHeaders
|
||||
) => {
|
||||
return this.client.fetch<any>(`/store/product-categories`, {
|
||||
query: queryParams,
|
||||
headers,
|
||||
})
|
||||
},
|
||||
retrieve: async (
|
||||
id: string,
|
||||
queryParams?: Record<string, any>,
|
||||
headers?: ClientHeaders
|
||||
) => {
|
||||
return this.client.fetch<any>(`/store/product-categories/${id}`, {
|
||||
query: queryParams,
|
||||
headers,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
public product = {
|
||||
list: async (
|
||||
queryParams?: Record<string, any>,
|
||||
headers?: ClientHeaders
|
||||
) => {
|
||||
return this.client.fetch<any>(`/store/products`, {
|
||||
query: queryParams,
|
||||
headers,
|
||||
})
|
||||
},
|
||||
retrieve: async (
|
||||
id: string,
|
||||
queryParams?: Record<string, any>,
|
||||
headers?: ClientHeaders
|
||||
) => {
|
||||
return this.client.fetch<any>(`/store/products/${id}`, {
|
||||
query: queryParams,
|
||||
headers,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
public order = {
|
||||
retrieve: async (
|
||||
id: string,
|
||||
queryParams?: Record<string, any>,
|
||||
headers?: ClientHeaders
|
||||
) => {
|
||||
return this.client.fetch<any>(`/store/orders/${id}`, {
|
||||
query: queryParams,
|
||||
headers,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
public cart = {
|
||||
create: async (body: any, headers?: ClientHeaders) => {
|
||||
return this.client.fetch<any>(`/store/carts`, {
|
||||
headers,
|
||||
method: "POST",
|
||||
body,
|
||||
})
|
||||
},
|
||||
update: async (id: string, body: any, headers?: ClientHeaders) => {
|
||||
return this.client.fetch<any>(`/store/carts/${id}`, {
|
||||
headers,
|
||||
method: "POST",
|
||||
body,
|
||||
})
|
||||
},
|
||||
retrieve: async (
|
||||
id: string,
|
||||
queryParams?: Record<string, any>,
|
||||
headers?: ClientHeaders
|
||||
) => {
|
||||
return this.client.fetch<any>(`/store/carts/${id}`, {
|
||||
query: queryParams,
|
||||
headers,
|
||||
})
|
||||
},
|
||||
createLineItem: async (
|
||||
cartId: string,
|
||||
body: any,
|
||||
headers?: ClientHeaders
|
||||
) => {
|
||||
return this.client.fetch<any>(`/store/carts/${cartId}/line-items`, {
|
||||
headers,
|
||||
method: "POST",
|
||||
body,
|
||||
})
|
||||
},
|
||||
updateLineItem: async (
|
||||
cartId: string,
|
||||
lineItemId: string,
|
||||
body: any,
|
||||
headers?: ClientHeaders
|
||||
) => {
|
||||
return this.client.fetch<any>(
|
||||
`/store/carts/${cartId}/line-items/${lineItemId}`,
|
||||
{
|
||||
headers,
|
||||
method: "POST",
|
||||
body,
|
||||
}
|
||||
)
|
||||
},
|
||||
deleteLineItem: async (
|
||||
cartId: string,
|
||||
lineItemId: string,
|
||||
headers?: ClientHeaders
|
||||
) => {
|
||||
return this.client.fetch<any>(
|
||||
`/store/carts/${cartId}/line-items/${lineItemId}`,
|
||||
{
|
||||
headers,
|
||||
method: "DELETE",
|
||||
}
|
||||
)
|
||||
},
|
||||
addShippingMethod: async (
|
||||
cartId: string,
|
||||
body: any,
|
||||
headers?: ClientHeaders
|
||||
) => {
|
||||
return this.client.fetch<any>(`/store/carts/${cartId}/shipping-methods`, {
|
||||
headers,
|
||||
method: "POST",
|
||||
body,
|
||||
})
|
||||
},
|
||||
complete: async (cartId: string, headers?: ClientHeaders) => {
|
||||
return this.client.fetch<any>(`/store/carts/${cartId}/complete`, {
|
||||
headers,
|
||||
method: "POST",
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
public fulfillment = {
|
||||
listCartOptions: async (
|
||||
queryParams?: Record<string, any>,
|
||||
headers?: ClientHeaders
|
||||
) => {
|
||||
return this.client.fetch<any>(`/store/shipping-options`, {
|
||||
query: queryParams,
|
||||
headers,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
public payment = {
|
||||
listPaymentProviders: async (
|
||||
queryParams?: Record<string, any>,
|
||||
headers?: ClientHeaders
|
||||
) => {
|
||||
return this.client.fetch<any>(`/store/payment-providers`, {
|
||||
query: queryParams,
|
||||
headers,
|
||||
})
|
||||
},
|
||||
|
||||
initiatePaymentSession: async (
|
||||
cart: any,
|
||||
body: Record<string, any>,
|
||||
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<any>(`/store/payment-collections`, {
|
||||
headers,
|
||||
method: "POST",
|
||||
body: collectionBody,
|
||||
})
|
||||
).payment_collection.id
|
||||
}
|
||||
|
||||
return this.client.fetch<any>(
|
||||
`/store/payment-collections/${paymentCollectionId}/payment-sessions`,
|
||||
{
|
||||
headers,
|
||||
method: "POST",
|
||||
body,
|
||||
}
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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<typeof fetch>
|
||||
|
||||
export type ClientHeaders = Record<string, string>
|
||||
|
||||
export type FetchInput = FetchParams[0]
|
||||
|
||||
export type FetchArgs = Omit<RequestInit, "headers" | "body"> & {
|
||||
query?: Record<string, any>
|
||||
headers?: ClientHeaders
|
||||
body?: RequestInit["body"] | Record<string, any>
|
||||
}
|
||||
|
||||
export type ClientFetch = (
|
||||
input: FetchInput,
|
||||
init?: FetchArgs
|
||||
) => Promise<Response>
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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<any>[] = []
|
||||
|
||||
;(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)
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ export const StockLocationModule = {
|
||||
label: "StockLocationService",
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["eventBusService"],
|
||||
dependencies: ["eventBusModuleService"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: "internal",
|
||||
resources: "shared",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<ModuleResource> {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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, {
|
||||
|
||||
-17
@@ -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"],
|
||||
}
|
||||
-20
@@ -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",
|
||||
},
|
||||
}
|
||||
-17
@@ -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",
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
}),
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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<T> = (args: SubscriberArgs<T>) => Promise<void>
|
||||
|
||||
type SubscriberModule<T> = {
|
||||
config: SubscriberConfig
|
||||
handler: SubscriberHandler<T>
|
||||
}
|
||||
|
||||
export class SubscriberLoader {
|
||||
protected container_: MedusaContainer
|
||||
protected pluginOptions_: Record<string, unknown>
|
||||
protected activityId_: string
|
||||
protected rootDir_: string
|
||||
protected excludes: RegExp[] = [
|
||||
/\.DS_Store/,
|
||||
/(\.ts\.map|\.js\.map|\.d\.ts)/,
|
||||
/^_[^/\\]*(\.[^/\\]+)?$/,
|
||||
]
|
||||
|
||||
protected subscriberDescriptors_: Map<string, SubscriberModule<any>> =
|
||||
new Map()
|
||||
|
||||
constructor(
|
||||
rootDir: string,
|
||||
container: MedusaContainer,
|
||||
options: Record<string, unknown> = {},
|
||||
activityId: string
|
||||
) {
|
||||
this.rootDir_ = rootDir
|
||||
this.pluginOptions_ = options
|
||||
this.container_ = container
|
||||
this.activityId_ = activityId
|
||||
}
|
||||
|
||||
private validateSubscriber(
|
||||
subscriber: any,
|
||||
path: string
|
||||
): subscriber is {
|
||||
default: SubscriberHandler<unknown>
|
||||
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<T>(
|
||||
fileName: string,
|
||||
config: SubscriberConfig,
|
||||
handler: SubscriberHandler<T>
|
||||
) {
|
||||
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<T>({
|
||||
fileName,
|
||||
config,
|
||||
handler,
|
||||
}: {
|
||||
fileName: string
|
||||
config: SubscriberConfig
|
||||
handler: SubscriberHandler<T>
|
||||
}) {
|
||||
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()]
|
||||
}
|
||||
}
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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<string, unknown>
|
||||
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<void> => {
|
||||
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<void> {
|
||||
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")
|
||||
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<Express> {
|
||||
const logger = container.resolve<Logger>("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<void> {
|
||||
await new SubscriberLoader(
|
||||
path.join(pluginDetails.resolve, "subscribers"),
|
||||
container,
|
||||
pluginDetails.options,
|
||||
activityId
|
||||
).load()
|
||||
}
|
||||
@@ -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())
|
||||
})
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { IEventBusModuleService, Logger } from "@medusajs/types"
|
||||
|
||||
export type InitializeModuleInjectableDependencies = {
|
||||
logger?: Logger
|
||||
eventBusService?: IEventBusModuleService
|
||||
eventBusModuleService?: IEventBusModuleService
|
||||
}
|
||||
|
||||
export type CreateApiKeyDTO = {
|
||||
|
||||
@@ -2,5 +2,5 @@ import { IEventBusModuleService, Logger } from "@medusajs/types"
|
||||
|
||||
export type InitializeModuleInjectableDependencies = {
|
||||
logger?: Logger
|
||||
eventBusService?: IEventBusModuleService
|
||||
eventBusModuleService?: IEventBusModuleService
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
|
||||
export type InitializeModuleInjectableDependencies = {
|
||||
logger?: Logger
|
||||
eventBusService?: IEventBusModuleService
|
||||
eventBusModuleService?: IEventBusModuleService
|
||||
}
|
||||
|
||||
export const FulfillmentIdentifiersRegistrationName =
|
||||
|
||||
@@ -14,5 +14,5 @@ export * from "./utils"
|
||||
|
||||
export type InitializeModuleInjectableDependencies = {
|
||||
logger?: Logger
|
||||
eventBusService?: IEventBusModuleService
|
||||
eventBusModuleService?: IEventBusModuleService
|
||||
}
|
||||
+1
-1
@@ -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\"}'`
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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<any>
|
||||
stockLocationAddressService: ModulesSdkTypes.InternalModuleService<any>
|
||||
@@ -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<TEntity>
|
||||
protected readonly stockLocationAddressService_: ModulesSdkTypes.InternalModuleService<TStockLocationAddress>
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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 }
|
||||
Reference in new issue
Block a user