Skip to content

Commit

Permalink
Merge branch 'develop' into fix/location-ui-fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
fPolic authored May 13, 2024
2 parents eb8941f + 6362342 commit 69c3504
Show file tree
Hide file tree
Showing 174 changed files with 1,364 additions and 2,815 deletions.
14 changes: 12 additions & 2 deletions integration-tests/modules/__tests__/modules/load-standalone.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import { MedusaApp, Modules } from "@medusajs/modules-sdk"
import { IProductModuleService } from "@medusajs/types"

jest.setTimeout(30000)

Expand All @@ -15,10 +17,18 @@ medusaIntegrationTestRunner({
})

it("Should migrate database and initialize Product module using connection string from environment variable ", async function () {
const { initialize, runMigrations } = require("@medusajs/product")
const { modules, runMigrations } = await MedusaApp({
modulesConfig: {
[Modules.PRODUCT]: true,
},
})

await runMigrations()

const product = await initialize()
const product = modules[
Modules.PRODUCT
] as unknown as IProductModuleService

const productList = await product.list()

expect(productList).toEqual(expect.arrayContaining([]))
Expand Down
1 change: 0 additions & 1 deletion packages/core/modules-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,3 @@ export * from "./medusa-app"
export * from "./medusa-module"
export * from "./remote-link"
export * from "./remote-query"
export * from "./utils/initialize-factory"
8 changes: 3 additions & 5 deletions packages/core/modules-sdk/src/loaders/register-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,9 @@ function getInternalModuleResolution(
// If user added a module and it's overridable, we resolve that instead
const isStr = isString(moduleConfig)
if (isStr || (isObj && moduleConfig.resolve)) {
resolutionPath = !moduleExports
? resolveCwd(isStr ? moduleConfig : (moduleConfig.resolve as string))
: // Explicitly assign an empty string, later, we will check if the value is exactly false.
// This allows to continue the module loading while using the module exports instead of re importing the module itself during the process.
""
resolutionPath = resolveCwd(
isStr ? moduleConfig : (moduleConfig.resolve as string)
)
}

const moduleDeclaration = isObj ? moduleConfig : {}
Expand Down
291 changes: 265 additions & 26 deletions packages/core/modules-sdk/src/loaders/utils/load-internal.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import {
Constructor,
InternalModuleDeclaration,
Logger,
MedusaContainer,
MODULE_RESOURCE_TYPE,
ModuleExports,
ModuleLoaderFunction,
ModuleResolution,
} from "@medusajs/types"
import {
ContainerRegistrationKeys,
createMedusaContainer,
MedusaModuleType,
ModulesSdkUtils,
} from "@medusajs/utils"
import { asFunction, asValue } from "awilix"
import { join, resolve } from "path"
import { statSync } from "fs"
import { readdir } from "fs/promises"

type ModuleResource = {
services: Function[]
models: Function[]
repositories: Function[]
loaders: ModuleLoaderFunction[]
moduleService: Constructor<any>
normalizedPath: string
}

export async function loadInternalModule(
container: MedusaContainer,
Expand All @@ -36,6 +51,8 @@ export async function loadInternalModule(
const modulePath = resolution.resolutionPath as string

if (resolution.moduleExports) {
// TODO:
// If we want to benefit from the auto load mechanism, even if the module exports is provided, we need to ask for the module path
loadedModule = resolution.moduleExports
} else {
loadedModule = await import(modulePath)
Expand All @@ -56,7 +73,17 @@ export async function loadInternalModule(
return { error }
}

if (!loadedModule?.service) {
let moduleResources = {} as ModuleResource

if (resolution.resolutionPath) {
moduleResources = await loadResources(
loadedModule?.loaders ?? [],
resolution,
logger
)
}

if (!loadedModule?.service && !moduleResources.moduleService) {
container.register({
[registrationName]: asValue(undefined),
})
Expand All @@ -69,9 +96,11 @@ export async function loadInternalModule(
}

if (migrationOnly) {
const moduleService_ = moduleResources.moduleService ?? loadedModule.service

// Partially loaded module, only register the service __joinerConfig function to be able to resolve it later
const moduleService = {
__joinerConfig: loadedModule.service.prototype.__joinerConfig,
__joinerConfig: moduleService_.prototype.__joinerConfig,
}
container.register({
[registrationName]: asValue(moduleService),
Expand Down Expand Up @@ -100,32 +129,21 @@ export async function loadInternalModule(
)
}

const moduleLoaders = loadedModule?.loaders ?? []
try {
for (const loader of moduleLoaders) {
await loader(
{
container: localContainer,
logger,
options: resolution.options,
dataLoaderOnly: loaderOnly,
},
resolution.moduleDeclaration as InternalModuleDeclaration
)
}
} catch (err) {
container.register({
[registrationName]: asValue(undefined),
})
const loaders = moduleResources.loaders ?? loadedModule?.loaders ?? []
const error = await runLoaders(loaders, {
container,
localContainer,
logger,
resolution,
loaderOnly,
registrationName,
})

return {
error: new Error(
`Loaders for module ${resolution.definition.label} failed: ${err.message}`
),
}
if (error) {
return error
}

const moduleService = loadedModule.service
const moduleService = moduleResources.moduleService ?? loadedModule.service

container.register({
[registrationName]: asFunction((cradle) => {
Expand Down Expand Up @@ -155,8 +173,229 @@ export async function loadModuleMigrations(
loadedModule =
moduleExports ?? (await import(resolution.resolutionPath as string))

return [loadedModule.runMigrations, loadedModule.revertMigration]
let runMigrations = loadedModule.runMigrations
let revertMigration = loadedModule.revertMigration

// Generate migration scripts if they are not present
if (!runMigrations || !revertMigration) {
const moduleResources = await loadResources(
loadedModule?.loaders ?? [],
resolution,
console as unknown as Logger
)

const migrationScriptOptions = {
moduleName: resolution.definition.key,
models: moduleResources.models,
pathToMigrations: moduleResources.normalizedPath + "/dist/migrations",
}

runMigrations ??= ModulesSdkUtils.buildMigrationScript(
migrationScriptOptions
)

revertMigration ??= ModulesSdkUtils.buildRevertMigrationScript(
migrationScriptOptions
)
}

return [runMigrations, revertMigration]
} catch {
return [undefined, undefined]
}
}

async function importAllFromDir(path: string) {
let filesToLoad: string[] = []

await readdir(path).then((files) => {
files.forEach((file) => {
if (file !== "index.js" && file.endsWith(".js")) {
const filePath = join(path, file)
const stats = statSync(filePath)

if (stats.isDirectory()) {
// TODO: should we handle that? dont think so but I put that here for discussion
} else if (stats.isFile()) {
filesToLoad.push(filePath)
}
}
})

return filesToLoad
})

return (
await Promise.all(filesToLoad.map((filePath) => import(filePath)))
).flatMap((value) => {
return Object.values(value)
})
}

async function loadResources(
loadedModuleLoaders: ModuleLoaderFunction[],
moduleResolution: ModuleResolution,
logger: Logger
): Promise<ModuleResource> {
const modulePath = moduleResolution.resolutionPath as string
let normalizedPath = modulePath.replace("dist/", "").replace("index.js", "")
normalizedPath = resolve(normalizedPath)

try {
const defaultOnFail = () => {
return []
}

const [moduleService, services, models, repositories] = await Promise.all([
import(modulePath).then((moduleExports) => moduleExports.default.service),
importAllFromDir(resolve(normalizedPath, "dist", "services")).catch(
defaultOnFail
),
importAllFromDir(resolve(normalizedPath, "dist", "models")).catch(
defaultOnFail
),
importAllFromDir(resolve(normalizedPath, "dist", "repositories")).catch(
defaultOnFail
),
])

const cleanupResources = (resources) => {
return Object.values(resources).filter(
(resource): resource is Function => {
return typeof resource === "function"
}
)
}

const potentialServices = [...new Set(cleanupResources(services))]
const potentialModels = [...new Set(cleanupResources(models))]
const potentialRepositories = [...new Set(cleanupResources(repositories))]

const finalLoaders = prepareLoaders({
loadedModuleLoaders,
models: potentialModels,
repositories: potentialRepositories,
services: potentialServices,
moduleResolution,
migrationPath: normalizedPath + "/dist/migrations",
})

return {
services: potentialServices,
models: potentialModels,
repositories: potentialRepositories,
loaders: finalLoaders,
moduleService,
normalizedPath
}
} catch (e) {
logger.warn(
`Unable to load resources for module ${modulePath} automagically. ${e.message}`
)

return {} as ModuleResource
}
}

async function runLoaders(
loaders: Function[] = [],
{
localContainer,
container,
logger,
resolution,
loaderOnly,
registrationName,
}
): Promise<void | { error: Error }> {
try {
for (const loader of loaders) {
await loader(
{
container: localContainer,
logger,
options: resolution.options,
dataLoaderOnly: loaderOnly,
},
resolution.moduleDeclaration as InternalModuleDeclaration
)
}
} catch (err) {
container.register({
[registrationName]: asValue(undefined),
})

return {
error: new Error(
`Loaders for module ${resolution.definition.label} failed: ${err.message}`
),
}
}
}

function prepareLoaders({
loadedModuleLoaders,
models,
repositories,
services,
moduleResolution,
migrationPath,
}) {
const finalLoaders: ModuleLoaderFunction[] = []

const toObjectReducer = (acc, curr) => {
acc[curr.name] = curr
return acc
}

/*
* If no connectionLoader function is provided, create a default connection loader.
* TODO: Validate naming convention
*/
const connectionLoaderName = "connectionLoader"
const containerLoader = "containerLoader"

const hasConnectionLoader = loadedModuleLoaders.some(
(l) => l.name === connectionLoaderName
)

if (!hasConnectionLoader && models.length > 0) {
const connectionLoader = ModulesSdkUtils.mikroOrmConnectionLoaderFactory({
moduleName: moduleResolution.definition.key,
moduleModels: models,
migrationsPath: migrationPath, //normalizedPath + "/dist/migrations",
})
finalLoaders.push(connectionLoader)
}

const hasContainerLoader = loadedModuleLoaders.some(
(l) => l.name === containerLoader
)

if (!hasContainerLoader) {
const containerLoader = ModulesSdkUtils.moduleContainerLoaderFactory({
moduleModels: models.reduce(toObjectReducer, {}),
moduleRepositories: repositories.reduce(toObjectReducer, {}),
moduleServices: services.reduce(toObjectReducer, {}),
})
finalLoaders.push(containerLoader)
}

finalLoaders.push(
...loadedModuleLoaders.filter((loader) => {
if (
loader.name !== connectionLoaderName &&
loader.name !== containerLoader
) {
return true
}

return (
(loader.name === containerLoader && hasContainerLoader) ||
(loader.name === connectionLoaderName && hasConnectionLoader)
)
})
)

return finalLoaders
}
Loading

0 comments on commit 69c3504

Please sign in to comment.