feature: bundle all modules (#9324)
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
import { MikroORM, Options, SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { createDatabase, dropDatabase } from "pg-god"
|
||||
|
||||
const DB_HOST = process.env.DB_HOST ?? "localhost"
|
||||
const DB_USERNAME = process.env.DB_USERNAME ?? ""
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD ?? ""
|
||||
|
||||
const pgGodCredentials = {
|
||||
user: DB_USERNAME,
|
||||
password: DB_PASSWORD,
|
||||
host: DB_HOST,
|
||||
}
|
||||
|
||||
export function getDatabaseURL(dbName?: string): string {
|
||||
const DB_HOST = process.env.DB_HOST ?? "localhost"
|
||||
const DB_USERNAME = process.env.DB_USERNAME ?? ""
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD
|
||||
const DB_NAME = dbName ?? process.env.DB_TEMP_NAME
|
||||
|
||||
return `postgres://${DB_USERNAME}${
|
||||
DB_PASSWORD ? `:${DB_PASSWORD}` : ""
|
||||
}@${DB_HOST}/${DB_NAME}`
|
||||
}
|
||||
|
||||
export function getMikroOrmConfig({
|
||||
mikroOrmEntities,
|
||||
pathToMigrations,
|
||||
clientUrl,
|
||||
schema,
|
||||
}: {
|
||||
mikroOrmEntities: any[]
|
||||
pathToMigrations?: string
|
||||
clientUrl?: string
|
||||
schema?: string
|
||||
}): Options {
|
||||
const DB_URL = clientUrl ?? getDatabaseURL()
|
||||
|
||||
return {
|
||||
type: "postgresql",
|
||||
clientUrl: DB_URL,
|
||||
entities: Object.values(mikroOrmEntities),
|
||||
schema: schema ?? process.env.MEDUSA_DB_SCHEMA,
|
||||
debug: false,
|
||||
pool: {
|
||||
min: 2,
|
||||
},
|
||||
migrations: {
|
||||
pathTs: pathToMigrations,
|
||||
silent: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export interface TestDatabase {
|
||||
mikroOrmEntities: any[]
|
||||
pathToMigrations?: string
|
||||
schema?: string
|
||||
clientUrl?: string
|
||||
|
||||
orm: MikroORM | null
|
||||
manager: SqlEntityManager | null
|
||||
|
||||
setupDatabase(): Promise<void>
|
||||
clearDatabase(): Promise<void>
|
||||
getManager(): SqlEntityManager
|
||||
forkManager(): SqlEntityManager
|
||||
getOrm(): MikroORM
|
||||
}
|
||||
|
||||
export function getMikroOrmWrapper({
|
||||
mikroOrmEntities,
|
||||
pathToMigrations,
|
||||
clientUrl,
|
||||
schema,
|
||||
}: {
|
||||
mikroOrmEntities: any[]
|
||||
pathToMigrations?: string
|
||||
clientUrl?: string
|
||||
schema?: string
|
||||
}): TestDatabase {
|
||||
return {
|
||||
mikroOrmEntities,
|
||||
pathToMigrations,
|
||||
clientUrl: clientUrl ?? getDatabaseURL(),
|
||||
schema: schema ?? process.env.MEDUSA_DB_SCHEMA,
|
||||
|
||||
orm: null,
|
||||
manager: null,
|
||||
|
||||
getManager() {
|
||||
if (this.manager === null) {
|
||||
throw new Error("manager entity not available")
|
||||
}
|
||||
|
||||
return this.manager
|
||||
},
|
||||
|
||||
forkManager() {
|
||||
if (this.manager === null) {
|
||||
throw new Error("manager entity not available")
|
||||
}
|
||||
|
||||
return this.manager.fork()
|
||||
},
|
||||
|
||||
getOrm() {
|
||||
if (this.orm === null) {
|
||||
throw new Error("orm entity not available")
|
||||
}
|
||||
|
||||
return this.orm
|
||||
},
|
||||
|
||||
async setupDatabase() {
|
||||
const OrmConfig = getMikroOrmConfig({
|
||||
mikroOrmEntities: this.mikroOrmEntities,
|
||||
pathToMigrations: this.pathToMigrations,
|
||||
clientUrl: this.clientUrl,
|
||||
schema: this.schema,
|
||||
})
|
||||
|
||||
// Initializing the ORM
|
||||
this.orm = await MikroORM.init(OrmConfig)
|
||||
|
||||
this.manager = this.orm.em
|
||||
|
||||
try {
|
||||
await this.orm.getSchemaGenerator().ensureDatabase()
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
|
||||
await this.manager?.execute(
|
||||
`CREATE SCHEMA IF NOT EXISTS "${this.schema ?? "public"}";`
|
||||
)
|
||||
|
||||
const pendingMigrations = await this.orm
|
||||
.getMigrator()
|
||||
.getPendingMigrations()
|
||||
|
||||
if (pendingMigrations && pendingMigrations.length > 0) {
|
||||
await this.orm
|
||||
.getMigrator()
|
||||
.up({ migrations: pendingMigrations.map((m) => m.name!) })
|
||||
} else {
|
||||
await this.orm.schema.refreshDatabase() // ensure db exists and is fresh
|
||||
}
|
||||
},
|
||||
|
||||
async clearDatabase() {
|
||||
if (this.orm === null) {
|
||||
throw new Error("ORM not configured")
|
||||
}
|
||||
|
||||
await this.manager?.execute(
|
||||
`DROP SCHEMA IF EXISTS "${this.schema ?? "public"}" CASCADE;`
|
||||
)
|
||||
|
||||
await this.manager?.execute(
|
||||
`CREATE SCHEMA IF NOT EXISTS "${this.schema ?? "public"}";`
|
||||
)
|
||||
|
||||
try {
|
||||
await this.orm.close()
|
||||
} catch {}
|
||||
|
||||
this.orm = null
|
||||
this.manager = null
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const dbTestUtilFactory = (): any => ({
|
||||
pgConnection_: null,
|
||||
|
||||
create: async function (dbName: string) {
|
||||
await createDatabase(
|
||||
{ databaseName: dbName, errorIfExist: false },
|
||||
pgGodCredentials
|
||||
)
|
||||
},
|
||||
|
||||
teardown: async function ({ schema }: { schema?: string } = {}) {
|
||||
if (!this.pgConnection_) {
|
||||
return
|
||||
}
|
||||
|
||||
const runRawQuery = this.pgConnection_.raw.bind(this.pgConnection_)
|
||||
|
||||
schema ??= "public"
|
||||
|
||||
await runRawQuery(`SET session_replication_role = 'replica';`)
|
||||
const { rows: tableNames } = await runRawQuery(`SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = '${schema}';`)
|
||||
|
||||
for (const { table_name } of tableNames) {
|
||||
await runRawQuery(`DELETE
|
||||
FROM ${schema}."${table_name}";`)
|
||||
}
|
||||
|
||||
await runRawQuery(`SET session_replication_role = 'origin';`)
|
||||
},
|
||||
|
||||
shutdown: async function (dbName: string) {
|
||||
await this.pgConnection_?.context?.destroy()
|
||||
await this.pgConnection_?.destroy()
|
||||
|
||||
return await dropDatabase(
|
||||
{ databaseName: dbName, errorIfNonExist: false },
|
||||
pgGodCredentials
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { IEventBusModuleService } from "@medusajs/framework/types"
|
||||
import { EventEmitter } from "events"
|
||||
|
||||
// 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 eventEmitter: EventEmitter = (eventBus as any).eventEmitter_
|
||||
const subscriberPromises: Promise<any>[] = []
|
||||
const originalListeners = eventEmitter.listeners(eventName)
|
||||
|
||||
// If there are no existing listeners, resolve once the event happens. Otherwise, wrap the existing subscribers in a promise and resolve once they are done.
|
||||
if (!eventEmitter.listeners(eventName).length) {
|
||||
let ok
|
||||
const promise = new Promise((resolve) => {
|
||||
ok = resolve
|
||||
})
|
||||
|
||||
subscriberPromises.push(promise)
|
||||
eventEmitter.on(eventName, ok)
|
||||
} else {
|
||||
eventEmitter.listeners(eventName).forEach((listener: any) => {
|
||||
eventEmitter.removeListener(eventName, 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)
|
||||
}
|
||||
|
||||
eventEmitter.on(eventName, newListener)
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.all(subscriberPromises).finally(() => {
|
||||
eventEmitter.removeAllListeners(eventName)
|
||||
originalListeners.forEach((listener) => {
|
||||
eventEmitter.on(eventName, listener as (...args: any) => void)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import randomize from "randomatic"
|
||||
|
||||
class IdMap {
|
||||
ids = {}
|
||||
|
||||
getId(key, prefix = "", length = 10) {
|
||||
if (this.ids[key]) {
|
||||
return this.ids[key]
|
||||
}
|
||||
|
||||
const id = `${prefix && prefix + "_"}${randomize("Aa0", length)}`
|
||||
this.ids[key] = id
|
||||
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new IdMap()
|
||||
export default instance
|
||||
@@ -0,0 +1,9 @@
|
||||
export * as TestDatabaseUtils from "./database"
|
||||
export * as TestEventUtils from "./events"
|
||||
export { default as IdMap } from "./id-map"
|
||||
export * from "./init-modules"
|
||||
export * as JestUtils from "./jest"
|
||||
export * from "./medusa-test-runner"
|
||||
export * from "./medusa-test-runner-utils"
|
||||
export { default as MockEventBusService } from "./mock-event-bus-service"
|
||||
export * from "./module-test-runner"
|
||||
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
ExternalModuleDeclaration,
|
||||
InternalModuleDeclaration,
|
||||
ModuleJoinerConfig,
|
||||
} from "@medusajs/framework/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
createPgConnection,
|
||||
promiseAll,
|
||||
} from "@medusajs/framework/utils"
|
||||
|
||||
export interface InitModulesOptions {
|
||||
injectedDependencies?: Record<string, unknown>
|
||||
databaseConfig: {
|
||||
clientUrl: string
|
||||
schema?: string
|
||||
}
|
||||
modulesConfig: {
|
||||
[key: string]:
|
||||
| string
|
||||
| boolean
|
||||
| Partial<InternalModuleDeclaration | ExternalModuleDeclaration>
|
||||
}
|
||||
joinerConfig?: ModuleJoinerConfig[]
|
||||
preventConnectionDestroyWarning?: boolean
|
||||
}
|
||||
|
||||
export async function initModules({
|
||||
injectedDependencies,
|
||||
databaseConfig,
|
||||
modulesConfig,
|
||||
joinerConfig,
|
||||
preventConnectionDestroyWarning = false,
|
||||
}: InitModulesOptions) {
|
||||
const moduleSdkImports = require("@medusajs/framework/modules-sdk")
|
||||
|
||||
injectedDependencies ??= {}
|
||||
|
||||
let sharedPgConnection =
|
||||
injectedDependencies?.[ContainerRegistrationKeys.PG_CONNECTION]
|
||||
|
||||
let shouldDestroyConnectionAutomatically = !sharedPgConnection
|
||||
if (!sharedPgConnection) {
|
||||
sharedPgConnection = createPgConnection({
|
||||
clientUrl: databaseConfig.clientUrl,
|
||||
schema: databaseConfig.schema,
|
||||
})
|
||||
|
||||
injectedDependencies[ContainerRegistrationKeys.PG_CONNECTION] =
|
||||
sharedPgConnection
|
||||
}
|
||||
|
||||
const medusaApp = await moduleSdkImports.MedusaApp({
|
||||
modulesConfig,
|
||||
servicesConfig: joinerConfig,
|
||||
injectedDependencies,
|
||||
})
|
||||
|
||||
await medusaApp.onApplicationStart()
|
||||
|
||||
async function shutdown() {
|
||||
if (shouldDestroyConnectionAutomatically) {
|
||||
await medusaApp.onApplicationPrepareShutdown()
|
||||
|
||||
await promiseAll([
|
||||
(sharedPgConnection as any).context?.destroy(),
|
||||
(sharedPgConnection as any).destroy(),
|
||||
medusaApp.onApplicationShutdown(),
|
||||
])
|
||||
} else {
|
||||
if (!preventConnectionDestroyWarning) {
|
||||
console.info(
|
||||
`You are using a custom shared connection. The connection won't be destroyed automatically.`
|
||||
)
|
||||
}
|
||||
}
|
||||
moduleSdkImports.MedusaModule.clearInstances()
|
||||
}
|
||||
|
||||
return {
|
||||
medusaApp,
|
||||
shutdown,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { dropDatabase } from "pg-god"
|
||||
|
||||
export function afterAllHookDropDatabase() {
|
||||
const DB_HOST = process.env.DB_HOST ?? "localhost"
|
||||
const DB_USERNAME = process.env.DB_USERNAME ?? "postgres"
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD ?? ""
|
||||
const DB_NAME = process.env.DB_TEMP_NAME || ""
|
||||
|
||||
const pgGodCredentials = {
|
||||
user: DB_USERNAME,
|
||||
password: DB_PASSWORD,
|
||||
host: DB_HOST,
|
||||
}
|
||||
|
||||
afterAll(async () => {
|
||||
try {
|
||||
await dropDatabase({ databaseName: DB_NAME }, pgGodCredentials)
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`This might fail if it is run during the unit tests since there is no database to drop. Otherwise, please check what is the issue. ${e.message}`
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import express from "express"
|
||||
import getPort from "get-port"
|
||||
import { resolve } from "path"
|
||||
import { MedusaContainer } from "@medusajs/framework/types"
|
||||
import { applyEnvVarsToProcess } from "./utils"
|
||||
import { promiseAll, GracefulShutdownServer } from "@medusajs/framework/utils"
|
||||
|
||||
async function bootstrapApp({
|
||||
cwd,
|
||||
env = {},
|
||||
}: { cwd?: string; env?: Record<any, any> } = {}) {
|
||||
const app = express()
|
||||
applyEnvVarsToProcess(env)
|
||||
|
||||
const loaders = require("@medusajs/medusa/loaders/index").default
|
||||
|
||||
const { container, shutdown } = await loaders({
|
||||
directory: resolve(cwd || process.cwd()),
|
||||
expressApp: app,
|
||||
})
|
||||
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT) : await getPort()
|
||||
|
||||
return {
|
||||
shutdown,
|
||||
container,
|
||||
app,
|
||||
port: PORT,
|
||||
}
|
||||
}
|
||||
|
||||
export async function startApp({
|
||||
cwd,
|
||||
env = {},
|
||||
}: { cwd?: string; env?: Record<any, any> } = {}): Promise<{
|
||||
shutdown: () => Promise<void>
|
||||
container: MedusaContainer
|
||||
port: number
|
||||
}> {
|
||||
const {
|
||||
app,
|
||||
port,
|
||||
container,
|
||||
shutdown: medusaShutdown,
|
||||
} = await bootstrapApp({
|
||||
cwd,
|
||||
env,
|
||||
})
|
||||
|
||||
let expressServer
|
||||
|
||||
const shutdown = async () => {
|
||||
await promiseAll([expressServer?.shutdown(), medusaShutdown()])
|
||||
|
||||
if (typeof global !== "undefined" && global?.gc) {
|
||||
global.gc()
|
||||
}
|
||||
}
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
const server = app
|
||||
.listen(port)
|
||||
.on("error", async (err) => {
|
||||
await shutdown()
|
||||
return reject(err)
|
||||
})
|
||||
.on("listening", () => {
|
||||
process.send?.(port)
|
||||
|
||||
resolve({
|
||||
shutdown,
|
||||
container,
|
||||
port,
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: fix that once we find the appropriate place to put this util
|
||||
expressServer = GracefulShutdownServer.create(server)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* cleanup temporary created resources for the migrations
|
||||
* @internal I didnt find a god place to put that, should we eventually add a close function
|
||||
* to the planner to handle that part? so that you would do planner.close() and it will handle the cleanup
|
||||
* automatically just like we usually do for the classic migrations actions
|
||||
*/
|
||||
export async function clearInstances() {
|
||||
const { MedusaModule } = require("@medusajs/framework/modules-sdk")
|
||||
MedusaModule.clearInstances()
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { getConfigFile } from "@medusajs/framework/utils"
|
||||
|
||||
export async function configLoaderOverride(
|
||||
entryDirectory: string,
|
||||
override: { clientUrl: string; debug?: boolean }
|
||||
) {
|
||||
const { configManager } = await import("@medusajs/framework/config")
|
||||
const { configModule, error } = getConfigFile<
|
||||
ReturnType<typeof configManager.loadConfig>
|
||||
>(entryDirectory, "medusa-config.js")
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message || "Error during config loading")
|
||||
}
|
||||
|
||||
configModule.projectConfig.databaseDriverOptions
|
||||
configModule.projectConfig.databaseUrl = override.clientUrl
|
||||
configModule.projectConfig.databaseLogging = !!override.debug
|
||||
configModule.projectConfig.databaseDriverOptions =
|
||||
override.clientUrl.includes("localhost")
|
||||
? {}
|
||||
: {
|
||||
connection: {
|
||||
ssl: { rejectUnauthorized: false },
|
||||
},
|
||||
idle_in_transaction_session_timeout: 20000,
|
||||
}
|
||||
|
||||
configManager.loadConfig({
|
||||
projectConfig: configModule,
|
||||
baseDir: entryDirectory,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./bootstrap-app"
|
||||
export * from "./clear-instances"
|
||||
export * from "./config"
|
||||
export * from "./use-db"
|
||||
export * from "./utils"
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { MedusaAppLoader } from "@medusajs/framework"
|
||||
import { join } from "path"
|
||||
import { MedusaContainer } from "@medusajs/framework/types"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
||||
|
||||
/**
|
||||
* Initiates the database connection
|
||||
*/
|
||||
export async function initDb() {
|
||||
const { pgConnectionLoader, featureFlagsLoader } = await import(
|
||||
"@medusajs/framework"
|
||||
)
|
||||
|
||||
const pgConnection = pgConnectionLoader()
|
||||
await featureFlagsLoader()
|
||||
|
||||
return pgConnection
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates the database
|
||||
*/
|
||||
export async function migrateDatabase(appLoader: MedusaAppLoader) {
|
||||
try {
|
||||
await appLoader.runModulesMigrations()
|
||||
} catch (err) {
|
||||
console.error("Something went wrong while running the migrations")
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs links with the databse
|
||||
*/
|
||||
export async function syncLinks(
|
||||
appLoader: MedusaAppLoader,
|
||||
directory: string,
|
||||
container: MedusaContainer
|
||||
) {
|
||||
try {
|
||||
await loadCustomLinks(directory, container)
|
||||
|
||||
const planner = await appLoader.getLinksExecutionPlanner()
|
||||
const actionPlan = await planner.createPlan()
|
||||
actionPlan.forEach((action) => {
|
||||
console.log(`Sync links: "${action.action}" ${action.tableName}`)
|
||||
})
|
||||
await planner.executePlan(actionPlan)
|
||||
} catch (err) {
|
||||
console.error("Something went wrong while syncing links")
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCustomLinks(directory: string, container: MedusaContainer) {
|
||||
// TODO: move to framework once settle down
|
||||
const {
|
||||
getResolvedPlugins,
|
||||
} = require("@medusajs/medusa/loaders/helpers/resolve-plugins")
|
||||
|
||||
const configModule = container.resolve(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
)
|
||||
const plugins = getResolvedPlugins(directory, configModule, true) || []
|
||||
const linksSourcePaths = plugins.map((plugin) =>
|
||||
join(plugin.resolve, "links")
|
||||
)
|
||||
|
||||
const { LinkLoader } = await import("@medusajs/framework")
|
||||
await new LinkLoader(linksSourcePaths).load()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { isObject } from "@medusajs/framework/utils"
|
||||
|
||||
export function applyEnvVarsToProcess(env?: Record<any, any>) {
|
||||
if (isObject(env)) {
|
||||
Object.entries(env).forEach(([k, v]) => (process.env[k] = v))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { MedusaAppOutput } from "@medusajs/framework/modules-sdk"
|
||||
import { ContainerLike, MedusaContainer } from "@medusajs/framework/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
createMedusaContainer,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { asValue } from "awilix"
|
||||
import { dbTestUtilFactory, getDatabaseURL } from "./database"
|
||||
import {
|
||||
applyEnvVarsToProcess,
|
||||
clearInstances,
|
||||
configLoaderOverride,
|
||||
initDb,
|
||||
migrateDatabase,
|
||||
startApp,
|
||||
syncLinks,
|
||||
} from "./medusa-test-runner-utils"
|
||||
|
||||
export interface MedusaSuiteOptions {
|
||||
dbConnection: any // knex instance
|
||||
getContainer: () => MedusaContainer
|
||||
api: any
|
||||
dbUtils: {
|
||||
create: (dbName: string) => Promise<void>
|
||||
teardown: (options: { schema?: string }) => Promise<void>
|
||||
shutdown: (dbName: string) => Promise<void>
|
||||
}
|
||||
dbConfig: {
|
||||
dbName: string
|
||||
schema: string
|
||||
clientUrl: string
|
||||
}
|
||||
getMedusaApp: () => MedusaAppOutput
|
||||
}
|
||||
|
||||
export function medusaIntegrationTestRunner({
|
||||
moduleName,
|
||||
dbName,
|
||||
medusaConfigFile,
|
||||
schema = "public",
|
||||
env = {},
|
||||
debug = false,
|
||||
inApp = false,
|
||||
testSuite,
|
||||
}: {
|
||||
moduleName?: string
|
||||
env?: Record<string, any>
|
||||
dbName?: string
|
||||
medusaConfigFile?: string
|
||||
schema?: string
|
||||
debug?: boolean
|
||||
inApp?: boolean
|
||||
testSuite: (options: MedusaSuiteOptions) => void
|
||||
}) {
|
||||
const tempName = parseInt(process.env.JEST_WORKER_ID || "1")
|
||||
moduleName = moduleName ?? Math.random().toString(36).substring(7)
|
||||
dbName ??= `medusa-${moduleName.toLowerCase()}-integration-${tempName}`
|
||||
|
||||
let dbConfig = {
|
||||
dbName,
|
||||
clientUrl: getDatabaseURL(dbName),
|
||||
schema,
|
||||
debug,
|
||||
}
|
||||
|
||||
const cwd = medusaConfigFile ?? process.cwd()
|
||||
|
||||
let shutdown = async () => void 0
|
||||
const dbUtils = dbTestUtilFactory()
|
||||
let globalContainer: ContainerLike
|
||||
let apiUtils: any
|
||||
let loadedApplication: any
|
||||
|
||||
let options = {
|
||||
api: new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (target, prop) => {
|
||||
return apiUtils[prop]
|
||||
},
|
||||
}
|
||||
),
|
||||
dbConnection: new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (target, prop) => {
|
||||
return dbUtils.pgConnection_[prop]
|
||||
},
|
||||
}
|
||||
),
|
||||
getMedusaApp: () => loadedApplication,
|
||||
getContainer: () => globalContainer,
|
||||
dbConfig: {
|
||||
dbName,
|
||||
schema,
|
||||
clientUrl: dbConfig.clientUrl,
|
||||
},
|
||||
dbUtils,
|
||||
} as MedusaSuiteOptions
|
||||
|
||||
let isFirstTime = true
|
||||
|
||||
const beforeAll_ = async () => {
|
||||
await configLoaderOverride(cwd, dbConfig)
|
||||
applyEnvVarsToProcess(env)
|
||||
|
||||
const { logger, container, MedusaAppLoader } = await import(
|
||||
"@medusajs/framework"
|
||||
)
|
||||
|
||||
const appLoader = new MedusaAppLoader()
|
||||
container.register({
|
||||
[ContainerRegistrationKeys.LOGGER]: asValue(logger),
|
||||
})
|
||||
|
||||
try {
|
||||
console.log(`Creating database ${dbName}`)
|
||||
await dbUtils.create(dbName)
|
||||
dbUtils.pgConnection_ = await initDb()
|
||||
} catch (error) {
|
||||
console.error("Error initializing database", error?.message)
|
||||
throw error
|
||||
}
|
||||
|
||||
console.log(`Migrating database with core migrations and links ${dbName}`)
|
||||
await migrateDatabase(appLoader)
|
||||
await syncLinks(appLoader, cwd, container)
|
||||
await clearInstances()
|
||||
|
||||
let containerRes: MedusaContainer = container
|
||||
let serverShutdownRes: () => any
|
||||
let portRes: number
|
||||
|
||||
loadedApplication = await appLoader.load()
|
||||
|
||||
try {
|
||||
const {
|
||||
shutdown = () => void 0,
|
||||
container: appContainer,
|
||||
port,
|
||||
} = await startApp({
|
||||
cwd,
|
||||
env,
|
||||
})
|
||||
|
||||
containerRes = appContainer
|
||||
serverShutdownRes = shutdown
|
||||
portRes = port
|
||||
} catch (error) {
|
||||
console.error("Error starting the app", error?.message)
|
||||
throw error
|
||||
}
|
||||
|
||||
/**
|
||||
* Run application migrations and sync links when inside
|
||||
* an application
|
||||
*/
|
||||
if (inApp) {
|
||||
console.log(`Migrating database with core migrations and links ${dbName}`)
|
||||
await migrateDatabase(appLoader)
|
||||
await syncLinks(appLoader, cwd, containerRes)
|
||||
}
|
||||
|
||||
const { default: axios } = (await import("axios")) as any
|
||||
|
||||
const cancelTokenSource = axios.CancelToken.source()
|
||||
|
||||
globalContainer = containerRes
|
||||
shutdown = async () => {
|
||||
await serverShutdownRes()
|
||||
cancelTokenSource.cancel("Request canceled by shutdown")
|
||||
}
|
||||
|
||||
apiUtils = axios.create({
|
||||
baseURL: `http://localhost:${portRes}`,
|
||||
cancelToken: cancelTokenSource.token,
|
||||
})
|
||||
}
|
||||
|
||||
const beforeEach_ = async () => {
|
||||
// The beforeAll already run everything, so lets not re run the loaders for the first iteration
|
||||
if (isFirstTime) {
|
||||
isFirstTime = false
|
||||
return
|
||||
}
|
||||
|
||||
const container = options.getContainer()
|
||||
const copiedContainer = createMedusaContainer({}, container)
|
||||
|
||||
try {
|
||||
const { MedusaAppLoader } = await import("@medusajs/framework")
|
||||
|
||||
const medusaAppLoader = new MedusaAppLoader({
|
||||
container: copiedContainer,
|
||||
})
|
||||
await medusaAppLoader.runModulesLoader()
|
||||
} catch (error) {
|
||||
console.error("Error runner modules loaders", error?.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const afterEach_ = async () => {
|
||||
try {
|
||||
await dbUtils.teardown({ schema })
|
||||
} catch (error) {
|
||||
console.error("Error tearing down database:", error?.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return describe("", () => {
|
||||
beforeAll(beforeAll_)
|
||||
beforeEach(beforeEach_)
|
||||
afterEach(afterEach_)
|
||||
afterAll(async () => {
|
||||
await dbUtils.shutdown(dbName)
|
||||
await shutdown()
|
||||
})
|
||||
|
||||
testSuite(options!)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
EventBusTypes,
|
||||
IEventBusModuleService,
|
||||
Message,
|
||||
Subscriber,
|
||||
} from "@medusajs/framework/types"
|
||||
|
||||
export default class EventBusService implements IEventBusModuleService {
|
||||
async emit<T>(
|
||||
data: Message<T> | Message<T>[],
|
||||
options: Record<string, unknown>
|
||||
): Promise<void> {}
|
||||
|
||||
subscribe(event: string | symbol, subscriber: Subscriber): this {
|
||||
return this
|
||||
}
|
||||
|
||||
unsubscribe(
|
||||
event: string | symbol,
|
||||
subscriber: Subscriber,
|
||||
context?: EventBusTypes.SubscriberContext
|
||||
): this {
|
||||
return this
|
||||
}
|
||||
|
||||
releaseGroupedEvents(eventGroupId: string): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
clearGroupedEvents(eventGroupId: string): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
DmlEntity,
|
||||
loadModels,
|
||||
Modules,
|
||||
ModulesSdkUtils,
|
||||
normalizeImportPathWithSource,
|
||||
toMikroOrmEntities,
|
||||
} from "@medusajs/framework/utils"
|
||||
import * as fs from "fs"
|
||||
import { getDatabaseURL, getMikroOrmWrapper, TestDatabase } from "./database"
|
||||
import { initModules, InitModulesOptions } from "./init-modules"
|
||||
import { default as MockEventBusService } from "./mock-event-bus-service"
|
||||
|
||||
export interface SuiteOptions<TService = unknown> {
|
||||
MikroOrmWrapper: TestDatabase
|
||||
medusaApp: any
|
||||
service: TService
|
||||
dbConfig: {
|
||||
schema: string
|
||||
clientUrl: string
|
||||
}
|
||||
}
|
||||
|
||||
function createMikroOrmWrapper(options: {
|
||||
moduleModels?: (Function | DmlEntity<any, any>)[]
|
||||
resolve?: string
|
||||
dbConfig: any
|
||||
}): {
|
||||
MikroOrmWrapper: TestDatabase
|
||||
models: (Function | DmlEntity<any, any>)[]
|
||||
} {
|
||||
let moduleModels: (Function | DmlEntity<any, any>)[] =
|
||||
options.moduleModels ?? []
|
||||
|
||||
if (!options.moduleModels) {
|
||||
const basePath = normalizeImportPathWithSource(
|
||||
options.resolve ?? process.cwd()
|
||||
)
|
||||
|
||||
const modelsPath = fs.existsSync(`${basePath}/dist/models`)
|
||||
? "/dist/models"
|
||||
: fs.existsSync(`${basePath}/models`)
|
||||
? "/models"
|
||||
: ""
|
||||
|
||||
if (modelsPath) {
|
||||
moduleModels = loadModels(`${basePath}${modelsPath}`)
|
||||
} else {
|
||||
moduleModels = []
|
||||
}
|
||||
}
|
||||
|
||||
moduleModels = toMikroOrmEntities(moduleModels)
|
||||
|
||||
const MikroOrmWrapper = getMikroOrmWrapper({
|
||||
mikroOrmEntities: moduleModels,
|
||||
clientUrl: options.dbConfig.clientUrl,
|
||||
schema: options.dbConfig.schema,
|
||||
})
|
||||
|
||||
return { MikroOrmWrapper, models: moduleModels }
|
||||
}
|
||||
|
||||
export function moduleIntegrationTestRunner<TService = any>({
|
||||
moduleName,
|
||||
moduleModels,
|
||||
moduleOptions = {},
|
||||
moduleDependencies,
|
||||
joinerConfig = [],
|
||||
schema = "public",
|
||||
debug = false,
|
||||
testSuite,
|
||||
resolve,
|
||||
injectedDependencies = {},
|
||||
}: {
|
||||
moduleName: string
|
||||
moduleModels?: any[]
|
||||
moduleOptions?: Record<string, any>
|
||||
moduleDependencies?: string[]
|
||||
joinerConfig?: any[]
|
||||
schema?: string
|
||||
dbName?: string
|
||||
injectedDependencies?: Record<string, any>
|
||||
resolve?: string
|
||||
debug?: boolean
|
||||
testSuite: (options: SuiteOptions<TService>) => void
|
||||
}) {
|
||||
const moduleSdkImports = require("@medusajs/framework/modules-sdk")
|
||||
|
||||
process.env.LOG_LEVEL = "error"
|
||||
|
||||
const tempName = parseInt(process.env.JEST_WORKER_ID || "1")
|
||||
const dbName = `medusa-${moduleName.toLowerCase()}-integration-${tempName}`
|
||||
|
||||
const dbConfig = {
|
||||
clientUrl: getDatabaseURL(dbName),
|
||||
schema,
|
||||
debug,
|
||||
}
|
||||
|
||||
// Use a unique connection for all the entire suite
|
||||
const connection = ModulesSdkUtils.createPgConnection(dbConfig)
|
||||
|
||||
const { MikroOrmWrapper, models } = createMikroOrmWrapper({
|
||||
moduleModels,
|
||||
resolve,
|
||||
dbConfig,
|
||||
})
|
||||
|
||||
moduleModels = models
|
||||
|
||||
const modulesConfig_ = {
|
||||
[moduleName]: {
|
||||
definition: moduleSdkImports.ModulesDefinition[moduleName],
|
||||
resolve,
|
||||
dependencies: moduleDependencies,
|
||||
options: {
|
||||
database: dbConfig,
|
||||
...moduleOptions,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const moduleOptions_: InitModulesOptions = {
|
||||
injectedDependencies: {
|
||||
[ContainerRegistrationKeys.PG_CONNECTION]: connection,
|
||||
[Modules.EVENT_BUS]: new MockEventBusService(),
|
||||
[ContainerRegistrationKeys.LOGGER]: console,
|
||||
...injectedDependencies,
|
||||
},
|
||||
modulesConfig: modulesConfig_,
|
||||
databaseConfig: dbConfig,
|
||||
joinerConfig,
|
||||
preventConnectionDestroyWarning: true,
|
||||
}
|
||||
|
||||
let shutdown: () => Promise<void>
|
||||
let moduleService
|
||||
let medusaApp = {}
|
||||
|
||||
const options = {
|
||||
MikroOrmWrapper,
|
||||
medusaApp: new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (target, prop) => {
|
||||
return medusaApp[prop]
|
||||
},
|
||||
}
|
||||
),
|
||||
service: new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (target, prop) => {
|
||||
return moduleService[prop]
|
||||
},
|
||||
}
|
||||
),
|
||||
dbConfig: {
|
||||
schema,
|
||||
clientUrl: dbConfig.clientUrl,
|
||||
},
|
||||
} as SuiteOptions<TService>
|
||||
|
||||
const beforeEach_ = async () => {
|
||||
if (moduleModels.length) {
|
||||
await MikroOrmWrapper.setupDatabase()
|
||||
}
|
||||
const output = await initModules(moduleOptions_)
|
||||
shutdown = output.shutdown
|
||||
medusaApp = output.medusaApp
|
||||
moduleService = output.medusaApp.modules[moduleName]
|
||||
}
|
||||
|
||||
const afterEach_ = async () => {
|
||||
if (moduleModels.length) {
|
||||
await MikroOrmWrapper.clearDatabase()
|
||||
}
|
||||
await shutdown()
|
||||
moduleService = {}
|
||||
medusaApp = {}
|
||||
}
|
||||
|
||||
return describe("", () => {
|
||||
beforeEach(beforeEach_)
|
||||
afterEach(afterEach_)
|
||||
afterAll(async () => {
|
||||
await (connection as any).context?.destroy()
|
||||
await (connection as any).destroy()
|
||||
})
|
||||
|
||||
testSuite(options)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user