Merge branch 'develop' into fix/2152

This commit is contained in:
Harminder Virk
2024-05-16 17:28:54 +05:30
44 changed files with 1351 additions and 762 deletions
+59
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/js-sdk": patch
---
Introduce a js-sdk package for the Medusa API
+1
View File
@@ -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",
+8 -7
View File
@@ -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' }}
@@ -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"}'
@@ -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",
})
)
})
})
})
},
View File
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
transform: {
"^.+\\.[jt]s?$": "@swc/jest",
},
testEnvironment: `node`,
moduleFileExtensions: [`js`, `ts`, `json`],
}
+40
View File
@@ -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" })
})
})
})
+8
View File
@@ -0,0 +1,8 @@
import { Client } from "../client"
export class Admin {
private client: Client
constructor(client: Client) {
this.client = client
}
}
+183
View File
@@ -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}` } : {}
}
}
}
}
+18
View File
@@ -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
+250
View File
@@ -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,
}
)
},
}
}
+37
View File
@@ -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>
+28
View File
@@ -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)
}
+2 -1
View File
@@ -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",
+5 -5
View File
@@ -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, {
@@ -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"],
}
@@ -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",
},
}
@@ -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()]
}
}
+2 -11
View File
@@ -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()
}
+8 -5
View File
@@ -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
)
+1 -1
View File
@@ -3,7 +3,7 @@ import { IEventBusModuleService, Logger } from "@medusajs/types"
export type InitializeModuleInjectableDependencies = {
logger?: Logger
eventBusService?: IEventBusModuleService
eventBusModuleService?: IEventBusModuleService
}
export type CreateApiKeyDTO = {
+1 -1
View File
@@ -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 =
+1 -1
View File
@@ -14,5 +14,5 @@ export * from "./utils"
export type InitializeModuleInjectableDependencies = {
logger?: Logger
eventBusService?: IEventBusModuleService
eventBusModuleService?: IEventBusModuleService
}
@@ -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 {
+1 -1
View File
@@ -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 }
+241 -5
View File
@@ -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"