feat(providers): locking redis (#9544)

This commit is contained in:
Carlos R. L. Rodrigues
2024-10-15 12:40:24 -03:00
committed by GitHub
parent e77a2ff032
commit 4a03bdbb86
49 changed files with 1764 additions and 483 deletions
@@ -82,11 +82,11 @@ async function loadModule(
return
}
return await loadInternalModule(
return await loadInternalModule({
container,
resolution,
logger,
migrationOnly,
loaderOnly
)
loaderOnly,
})
}
@@ -0,0 +1,11 @@
import { ModuleExports } from "@medusajs/types"
import { ModuleService } from "./services/module-service"
import { Module } from "@medusajs/utils"
const moduleExports: ModuleExports = {
service: ModuleService,
}
export * from "./services/module-service"
export default Module("module-with-providers", moduleExports)
@@ -0,0 +1,8 @@
import { ModuleProviderService } from "./services/provider-service"
import { ModuleProvider } from "@medusajs/utils"
export * from "./services/provider-service"
export default ModuleProvider("provider-1", {
services: [ModuleProviderService],
})
@@ -0,0 +1,3 @@
export class ModuleProviderService {
static identifier = "provider-1"
}
@@ -0,0 +1,8 @@
import { ModuleProvider2Service } from "./services/provider-service"
import { ModuleProvider } from "@medusajs/utils"
export * from "./services/provider-service"
export default ModuleProvider("provider-2", {
services: [ModuleProvider2Service],
})
@@ -0,0 +1 @@
export class ModuleProvider2Service {}
@@ -0,0 +1,9 @@
import { InternalModuleDeclaration } from "@medusajs/types"
export class ModuleService {
constructor(
public container: Record<any, any>,
public moduleOptions: Record<any, any>,
public moduleDeclaration: InternalModuleDeclaration
) {}
}
@@ -1,5 +1,5 @@
import { IModuleService, ModuleResolution } from "@medusajs/types"
import { upperCaseFirst } from "@medusajs/utils"
import { createMedusaContainer, upperCaseFirst } from "@medusajs/utils"
import { join } from "path"
import {
ModuleWithDmlMixedWithoutJoinerConfigFixtures,
@@ -7,79 +7,253 @@ import {
ModuleWithJoinerConfigFixtures,
ModuleWithoutJoinerConfigFixtures,
} from "../__fixtures__"
import { loadResources } from "../load-internal"
import {
getProviderRegistrationKey,
loadInternalModule,
loadResources,
} from "../load-internal"
import { ModuleProviderService as ModuleServiceWithProviderProvider1 } from "../__fixtures__/module-with-providers/provider-1"
import { ModuleProvider2Service as ModuleServiceWithProviderProvider2 } from "../__fixtures__/module-with-providers/provider-2"
import { ModuleService as ModuleServiceWithProvider } from "../__fixtures__/module-with-providers"
describe("load internal - load resources", () => {
describe("when loading the module resources from a path", () => {
test("should return the correct resources and generate the correct joiner config from a mix of DML entities and mikro orm entities", async () => {
const { ModuleService, EntityModel, dmlEntity } =
ModuleWithDmlMixedWithoutJoinerConfigFixtures
describe("load internal", () => {
describe("loadResources", () => {
describe("when loading the module resources from a path", () => {
test("should return the correct resources and generate the correct joiner config from a mix of DML entities and mikro orm entities", async () => {
const { ModuleService, EntityModel, dmlEntity } =
ModuleWithDmlMixedWithoutJoinerConfigFixtures
const moduleResolution: ModuleResolution = {
resolutionPath: join(
__dirname,
"../__fixtures__/module-with-dml-mixed-without-joiner-config"
),
definition: {
key: "module-with-dml-mixed-without-joiner-config",
label: "Module with DML mixed without joiner config",
defaultPackage: false,
defaultModuleDeclaration: {
scope: "internal",
resources: "shared",
const moduleResolution: ModuleResolution = {
resolutionPath: join(
__dirname,
"../__fixtures__/module-with-dml-mixed-without-joiner-config"
),
definition: {
key: "module-with-dml-mixed-without-joiner-config",
label: "Module with DML mixed without joiner config",
defaultPackage: false,
defaultModuleDeclaration: {
scope: "internal",
resources: "shared",
},
},
},
}
}
expect(
(ModuleService.prototype as IModuleService).__joinerConfig
).toBeUndefined()
expect(
(ModuleService.prototype as IModuleService).__joinerConfig
).toBeUndefined()
const resources = await loadResources({
moduleResolution,
discoveryPath: moduleResolution.resolutionPath as string,
const resources = await loadResources({
moduleResolution,
discoveryPath: moduleResolution.resolutionPath as string,
})
expect(resources).toBeDefined()
expect(resources.services).toHaveLength(1)
expect(resources.services[0]).toEqual(ModuleService)
expect(resources.models).toHaveLength(2)
expect(resources.models).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: upperCaseFirst(dmlEntity.name) }),
expect.objectContaining({ name: upperCaseFirst(EntityModel.name) }),
])
)
expect(resources.repositories).toHaveLength(0)
expect(resources.loaders).toHaveLength(2)
expect(resources.loaders).toEqual([
expect.objectContaining({ name: "connectionLoader" }),
expect.objectContaining({ name: "containerLoader" }),
])
expect(resources.moduleService).toEqual(ModuleService)
expect(
(resources.moduleService.prototype as IModuleService).__joinerConfig
).toBeDefined()
const generatedJoinerConfig = (
resources.moduleService.prototype as IModuleService
).__joinerConfig?.()!
expect(generatedJoinerConfig).toEqual(
expect.objectContaining({
serviceName: "module-with-dml-mixed-without-joiner-config",
primaryKeys: ["id"],
linkableKeys: {
dml_entity_id: "DmlEntity",
entity_model_id: "EntityModel",
},
alias: [
{
name: ["dml_entity", "dml_entities"],
entity: "DmlEntity",
args: {
methodSuffix: "DmlEntities",
},
},
{
name: ["entity_model", "entity_models"],
entity: "EntityModel",
args: {
methodSuffix: "EntityModels",
},
},
],
})
)
})
expect(resources).toBeDefined()
expect(resources.services).toHaveLength(1)
expect(resources.services[0]).toEqual(ModuleService)
expect(resources.models).toHaveLength(2)
expect(resources.models).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: upperCaseFirst(dmlEntity.name) }),
expect.objectContaining({ name: upperCaseFirst(EntityModel.name) }),
test("should return the correct resources and generate the correct joiner config from DML entities", async () => {
const { ModuleService, entityModel, dmlEntity } =
ModuleWithDmlWithoutJoinerConfigFixtures
const moduleResolution: ModuleResolution = {
resolutionPath: join(
__dirname,
"../__fixtures__/module-with-dml-without-joiner-config"
),
definition: {
key: "module-with-dml-without-joiner-config",
label: "Module with DML without joiner config",
defaultPackage: false,
defaultModuleDeclaration: {
scope: "internal",
resources: "shared",
},
},
}
expect(
(ModuleService.prototype as IModuleService).__joinerConfig
).toBeUndefined()
const resources = await loadResources({
moduleResolution,
discoveryPath: moduleResolution.resolutionPath as string,
})
expect(resources).toBeDefined()
expect(resources.services).toHaveLength(1)
expect(resources.services[0]).toEqual(ModuleService)
expect(resources.models).toHaveLength(2)
expect(resources.models).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: upperCaseFirst(dmlEntity.name) }),
expect.objectContaining({ name: upperCaseFirst(entityModel.name) }),
])
)
expect(resources.repositories).toHaveLength(0)
expect(resources.loaders).toHaveLength(2)
expect(resources.loaders).toEqual([
expect.objectContaining({ name: "connectionLoader" }),
expect.objectContaining({ name: "containerLoader" }),
])
)
expect(resources.repositories).toHaveLength(0)
expect(resources.loaders).toHaveLength(2)
expect(resources.loaders).toEqual([
expect.objectContaining({ name: "connectionLoader" }),
expect.objectContaining({ name: "containerLoader" }),
])
expect(resources.moduleService).toEqual(ModuleService)
expect(resources.moduleService).toEqual(ModuleService)
expect(
(resources.moduleService.prototype as IModuleService).__joinerConfig
).toBeDefined()
expect(
(resources.moduleService.prototype as IModuleService).__joinerConfig
).toBeDefined()
const generatedJoinerConfig = (
resources.moduleService.prototype as IModuleService
).__joinerConfig?.()!
const generatedJoinerConfig = (
resources.moduleService.prototype as IModuleService
).__joinerConfig?.()!
expect(generatedJoinerConfig).toEqual(
expect.objectContaining({
serviceName: "module-with-dml-mixed-without-joiner-config",
expect(generatedJoinerConfig).toEqual(
expect.objectContaining({
serviceName: "module-with-dml-without-joiner-config",
primaryKeys: ["id"],
linkableKeys: {
entity_model_id: "EntityModel",
dml_entity_id: "DmlEntity",
},
alias: [
{
name: ["entity_model", "entity_models"],
entity: "EntityModel",
args: {
methodSuffix: "EntityModels",
},
},
{
name: ["dml_entity", "dml_entities"],
entity: "DmlEntity",
args: {
methodSuffix: "DmlEntities",
},
},
],
})
)
})
test("should return the correct resources and generate the correct joiner config from mikro orm entities", async () => {
const { ModuleService, EntityModel, Entity2 } =
ModuleWithoutJoinerConfigFixtures
const moduleResolution: ModuleResolution = {
resolutionPath: join(
__dirname,
"../__fixtures__/module-without-joiner-config"
),
definition: {
key: "module-without-joiner-config",
label: "Module without joiner config",
defaultPackage: false,
defaultModuleDeclaration: {
scope: "internal",
resources: "shared",
},
},
}
expect(
(ModuleService.prototype as IModuleService).__joinerConfig
).toBeUndefined()
const resources = await loadResources({
moduleResolution,
discoveryPath: moduleResolution.resolutionPath as string,
})
expect(resources).toBeDefined()
expect(resources.services).toHaveLength(1)
expect(resources.services[0]).toEqual(ModuleService)
expect(resources.models).toHaveLength(2)
expect(resources.models).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: upperCaseFirst(EntityModel.name) }),
expect.objectContaining({ name: upperCaseFirst(Entity2.name) }),
])
)
expect(resources.repositories).toHaveLength(0)
expect(resources.loaders).toHaveLength(2)
expect(resources.loaders).toEqual([
expect.objectContaining({ name: "connectionLoader" }),
expect.objectContaining({ name: "containerLoader" }),
])
expect(resources.moduleService).toEqual(ModuleService)
expect(
(resources.moduleService.prototype as IModuleService).__joinerConfig
).toBeDefined()
const generatedJoinerConfig = (
resources.moduleService.prototype as IModuleService
).__joinerConfig?.()!
expect(generatedJoinerConfig).toEqual({
serviceName: "module-without-joiner-config",
primaryKeys: ["id"],
linkableKeys: {
dml_entity_id: "DmlEntity",
entity2_id: "Entity2",
entity_model_id: "EntityModel",
},
schema: "",
alias: [
{
name: ["dml_entity", "dml_entities"],
entity: "DmlEntity",
name: ["entity2", "entity2s"],
entity: "Entity2",
args: {
methodSuffix: "DmlEntities",
methodSuffix: "Entity2s",
},
},
{
@@ -91,240 +265,181 @@ describe("load internal - load resources", () => {
},
],
})
)
})
test("should return the correct resources and generate the correct joiner config from DML entities", async () => {
const { ModuleService, entityModel, dmlEntity } =
ModuleWithDmlWithoutJoinerConfigFixtures
const moduleResolution: ModuleResolution = {
resolutionPath: join(
__dirname,
"../__fixtures__/module-with-dml-without-joiner-config"
),
definition: {
key: "module-with-dml-without-joiner-config",
label: "Module with DML without joiner config",
defaultPackage: false,
defaultModuleDeclaration: {
scope: "internal",
resources: "shared",
},
},
}
expect(
(ModuleService.prototype as IModuleService).__joinerConfig
).toBeUndefined()
const resources = await loadResources({
moduleResolution,
discoveryPath: moduleResolution.resolutionPath as string,
})
expect(resources).toBeDefined()
expect(resources.services).toHaveLength(1)
expect(resources.services[0]).toEqual(ModuleService)
expect(resources.models).toHaveLength(2)
expect(resources.models).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: upperCaseFirst(dmlEntity.name) }),
expect.objectContaining({ name: upperCaseFirst(entityModel.name) }),
])
)
expect(resources.repositories).toHaveLength(0)
expect(resources.loaders).toHaveLength(2)
expect(resources.loaders).toEqual([
expect.objectContaining({ name: "connectionLoader" }),
expect.objectContaining({ name: "containerLoader" }),
])
expect(resources.moduleService).toEqual(ModuleService)
test("should return the correct resources and use the given joiner config", async () => {
const { ModuleService, EntityModel, Entity2 } =
ModuleWithJoinerConfigFixtures
expect(
(resources.moduleService.prototype as IModuleService).__joinerConfig
).toBeDefined()
const generatedJoinerConfig = (
resources.moduleService.prototype as IModuleService
).__joinerConfig?.()!
expect(generatedJoinerConfig).toEqual(
expect.objectContaining({
serviceName: "module-with-dml-without-joiner-config",
primaryKeys: ["id"],
linkableKeys: {
entity_model_id: "EntityModel",
dml_entity_id: "DmlEntity",
const moduleResolution: ModuleResolution = {
resolutionPath: join(
__dirname,
"../__fixtures__/module-with-joiner-config"
),
definition: {
key: "module-without-joiner-config",
label: "Module without joiner config",
defaultPackage: false,
defaultModuleDeclaration: {
scope: "internal",
resources: "shared",
},
},
}
expect(
(ModuleService.prototype as IModuleService).__joinerConfig
).toBeDefined()
const resources = await loadResources({
moduleResolution,
discoveryPath: moduleResolution.resolutionPath as string,
})
expect(resources).toBeDefined()
expect(resources.services).toHaveLength(1)
expect(resources.services[0]).toEqual(ModuleService)
expect(resources.models).toHaveLength(2)
expect(resources.models).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: upperCaseFirst(EntityModel.name) }),
expect.objectContaining({ name: upperCaseFirst(Entity2.name) }),
])
)
expect(resources.repositories).toHaveLength(0)
expect(resources.loaders).toHaveLength(2)
expect(resources.loaders).toEqual([
expect.objectContaining({ name: "connectionLoader" }),
expect.objectContaining({ name: "containerLoader" }),
])
expect(resources.moduleService).toEqual(ModuleService)
const generatedJoinerConfig = (
resources.moduleService.prototype as IModuleService
).__joinerConfig?.()!
expect(generatedJoinerConfig).toEqual({
serviceName: "module-service",
primaryKeys: ["id"],
linkableKeys: {},
schema: "",
alias: [
{
name: ["entity_model", "entity_models"],
entity: "EntityModel",
name: ["custom_name"],
entity: "Custom",
args: {
methodSuffix: "EntityModels",
},
},
{
name: ["dml_entity", "dml_entities"],
entity: "DmlEntity",
args: {
methodSuffix: "DmlEntities",
methodSuffix: "Customs",
},
},
],
})
)
})
test("should return the correct resources and generate the correct joiner config from mikro orm entities", async () => {
const { ModuleService, EntityModel, Entity2 } =
ModuleWithoutJoinerConfigFixtures
const moduleResolution: ModuleResolution = {
resolutionPath: join(
__dirname,
"../__fixtures__/module-without-joiner-config"
),
definition: {
key: "module-without-joiner-config",
label: "Module without joiner config",
defaultPackage: false,
defaultModuleDeclaration: {
scope: "internal",
resources: "shared",
},
},
}
expect(
(ModuleService.prototype as IModuleService).__joinerConfig
).toBeUndefined()
const resources = await loadResources({
moduleResolution,
discoveryPath: moduleResolution.resolutionPath as string,
})
expect(resources).toBeDefined()
expect(resources.services).toHaveLength(1)
expect(resources.services[0]).toEqual(ModuleService)
expect(resources.models).toHaveLength(2)
expect(resources.models).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: upperCaseFirst(EntityModel.name) }),
expect.objectContaining({ name: upperCaseFirst(Entity2.name) }),
])
)
expect(resources.repositories).toHaveLength(0)
expect(resources.loaders).toHaveLength(2)
expect(resources.loaders).toEqual([
expect.objectContaining({ name: "connectionLoader" }),
expect.objectContaining({ name: "containerLoader" }),
])
expect(resources.moduleService).toEqual(ModuleService)
expect(
(resources.moduleService.prototype as IModuleService).__joinerConfig
).toBeDefined()
const generatedJoinerConfig = (
resources.moduleService.prototype as IModuleService
).__joinerConfig?.()!
expect(generatedJoinerConfig).toEqual({
serviceName: "module-without-joiner-config",
primaryKeys: ["id"],
linkableKeys: {
entity2_id: "Entity2",
entity_model_id: "EntityModel",
},
schema: "",
alias: [
{
name: ["entity2", "entity2s"],
entity: "Entity2",
args: {
methodSuffix: "Entity2s",
},
},
{
name: ["entity_model", "entity_models"],
entity: "EntityModel",
args: {
methodSuffix: "EntityModels",
},
},
],
})
})
test("should return the correct resources and use the given joiner config", async () => {
const { ModuleService, EntityModel, Entity2 } =
ModuleWithJoinerConfigFixtures
const moduleResolution: ModuleResolution = {
resolutionPath: join(
__dirname,
"../__fixtures__/module-with-joiner-config"
),
definition: {
key: "module-without-joiner-config",
label: "Module without joiner config",
defaultPackage: false,
defaultModuleDeclaration: {
scope: "internal",
resources: "shared",
},
},
}
expect(
(ModuleService.prototype as IModuleService).__joinerConfig
).toBeDefined()
const resources = await loadResources({
moduleResolution,
discoveryPath: moduleResolution.resolutionPath as string,
})
expect(resources).toBeDefined()
expect(resources.services).toHaveLength(1)
expect(resources.services[0]).toEqual(ModuleService)
expect(resources.models).toHaveLength(2)
expect(resources.models).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: upperCaseFirst(EntityModel.name) }),
expect.objectContaining({ name: upperCaseFirst(Entity2.name) }),
])
)
expect(resources.repositories).toHaveLength(0)
expect(resources.loaders).toHaveLength(2)
expect(resources.loaders).toEqual([
expect.objectContaining({ name: "connectionLoader" }),
expect.objectContaining({ name: "containerLoader" }),
])
expect(resources.moduleService).toEqual(ModuleService)
const generatedJoinerConfig = (
resources.moduleService.prototype as IModuleService
).__joinerConfig?.()!
expect(generatedJoinerConfig).toEqual({
serviceName: "module-service",
primaryKeys: ["id"],
linkableKeys: {},
schema: "",
alias: [
{
name: ["custom_name"],
entity: "Custom",
args: {
methodSuffix: "Customs",
},
},
],
})
})
})
describe("loadInternalModule", () => {
test("should load the module and its providers using their identifier", async () => {
const moduleResolution: ModuleResolution = {
resolutionPath: join(
__dirname,
"../__fixtures__/module-with-providers"
),
moduleDeclaration: {
scope: "internal",
resources: "shared",
},
definition: {
key: "module-with-providers",
label: "Module with providers",
defaultPackage: false,
defaultModuleDeclaration: {
scope: "internal",
resources: "shared",
},
},
options: {
providers: [
{
resolve: join(
__dirname,
"../__fixtures__/module-with-providers/provider-1"
),
id: "provider-1-id",
options: {
api_key: "test",
},
},
],
},
}
const container = createMedusaContainer()
await loadInternalModule({
container: container,
resolution: moduleResolution,
logger: console as any,
})
const moduleService = container.resolve(moduleResolution.definition.key)
const provider = (moduleService as any).container[
getProviderRegistrationKey(
ModuleServiceWithProviderProvider1.identifier
)
]
expect(moduleService).toBeInstanceOf(ModuleServiceWithProvider)
expect(provider).toBeInstanceOf(ModuleServiceWithProviderProvider1)
})
test("should load the module and its providers using the provided id", async () => {
const moduleResolution: ModuleResolution = {
resolutionPath: join(
__dirname,
"../__fixtures__/module-with-providers"
),
moduleDeclaration: {
scope: "internal",
resources: "shared",
},
definition: {
key: "module-with-providers",
label: "Module with providers",
defaultPackage: false,
defaultModuleDeclaration: {
scope: "internal",
resources: "shared",
},
},
options: {
providers: [
{
resolve: join(
__dirname,
"../__fixtures__/module-with-providers/provider-2"
),
id: "provider-2-id",
options: {
api_key: "test",
},
},
],
},
}
const container = createMedusaContainer()
await loadInternalModule({
container: container,
resolution: moduleResolution,
logger: console as any,
})
const moduleService = container.resolve(moduleResolution.definition.key)
const provider = (moduleService as any).container[
getProviderRegistrationKey(moduleResolution.options!.providers![0].id)
]
expect(moduleService).toBeInstanceOf(ModuleServiceWithProvider)
expect(provider).toBeInstanceOf(ModuleServiceWithProviderProvider2)
})
})
})
@@ -7,6 +7,9 @@ import {
MedusaContainer,
ModuleExports,
ModuleLoaderFunction,
ModuleProvider,
ModuleProviderExports,
ModuleProviderLoaderFunction,
ModuleResolution,
} from "@medusajs/types"
import {
@@ -15,6 +18,8 @@ import {
defineJoinerConfig,
DmlEntity,
dynamicImport,
isString,
MedusaModuleProviderType,
MedusaModuleType,
ModulesSdkUtils,
toMikroOrmEntities,
@@ -29,7 +34,7 @@ type ModuleResource = {
services: Function[]
models: Function[]
repositories: Function[]
loaders: ModuleLoaderFunction[]
loaders: ModuleLoaderFunction[] | ModuleProviderLoaderFunction[]
moduleService: Constructor<any>
normalizedPath: string
}
@@ -39,22 +44,36 @@ type MigrationFunction = (
moduleDeclaration?: InternalModuleDeclaration
) => Promise<void>
type ResolvedModule = ModuleExports & {
discoveryPath: string
}
type ResolvedModuleProvider = ModuleProviderExports & {
discoveryPath: string
}
export const moduleProviderRegistrationKeyPrefix = "__providers__"
/**
* Return the key used to register a module provider in the container
* @param {string} moduleKey
* @return {string}
*/
export function getProviderRegistrationKey(moduleKey: string): string {
return moduleProviderRegistrationKeyPrefix + moduleKey
}
export async function resolveModuleExports({
resolution,
}: {
resolution: ModuleResolution
}): Promise<
| (ModuleExports & {
discoveryPath: string
})
| { error: any }
> {
}): Promise<ResolvedModule | ResolvedModuleProvider | { error: any }> {
let resolvedModuleExports: ModuleExports
try {
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
resolvedModuleExports = resolution.moduleExports
resolvedModuleExports = resolution.moduleExports as ModuleExports
resolvedModuleExports.discoveryPath = resolution.resolutionPath as string
} else {
const module = await dynamicImport(resolution.resolutionPath as string)
@@ -62,10 +81,12 @@ export async function resolveModuleExports({
if ("discoveryPath" in module) {
const reExportedLoadedModule = await dynamicImport(module.discoveryPath)
const discoveryPath = module.discoveryPath
resolvedModuleExports = reExportedLoadedModule.default
resolvedModuleExports =
reExportedLoadedModule.default ?? reExportedLoadedModule
resolvedModuleExports.discoveryPath = discoveryPath as string
} else {
resolvedModuleExports = (module as { default: ModuleExports }).default
resolvedModuleExports =
(module as { default: ModuleExports }).default ?? module
resolvedModuleExports.discoveryPath =
resolution.resolutionPath as string
}
@@ -90,13 +111,79 @@ export async function resolveModuleExports({
}
}
export async function loadInternalModule(
container: MedusaContainer,
resolution: ModuleResolution,
logger: Logger,
migrationOnly?: boolean,
loaderOnly?: boolean
async function loadInternalProvider(
args: {
container: MedusaContainer
resolution: ModuleResolution
logger: Logger
migrationOnly?: boolean
loaderOnly?: boolean
},
providers: ModuleProvider[]
): Promise<{ error?: Error } | void> {
const { container, resolution, logger, migrationOnly } = args
const errors: { error?: Error }[] = []
for (const provider of providers) {
const providerRes = provider.resolve as ModuleProviderExports
const canLoadProvider =
providerRes && (isString(providerRes) || !providerRes?.services)
if (!canLoadProvider) {
continue
}
const res = await loadInternalModule({
container,
resolution: {
...resolution,
moduleExports: !isString(providerRes) ? providerRes : undefined,
definition: {
...resolution.definition,
key: provider.id,
},
resolutionPath: isString(provider.resolve) ? provider.resolve : false,
},
logger,
migrationOnly,
loadingProviders: true,
})
if (res) {
errors.push(res)
}
}
const errorMessages = errors.map((e) => e.error?.message).join("\n")
return errors.length
? {
error: {
name: "ModuleProviderError",
message: `Errors while loading module providers for module ${resolution.definition.key}:\n${errorMessages}`,
stack: errors.map((e) => e.error?.stack).join("\n"),
},
}
: undefined
}
export async function loadInternalModule(args: {
container: MedusaContainer
resolution: ModuleResolution
logger: Logger
migrationOnly?: boolean
loaderOnly?: boolean
loadingProviders?: boolean
}): Promise<{ error?: Error } | void> {
const {
container,
resolution,
logger,
migrationOnly,
loaderOnly,
loadingProviders,
} = args
const keyName = !loaderOnly
? resolution.definition.key
: resolution.definition.key + "__loaderOnly"
@@ -121,7 +208,12 @@ export async function loadInternalModule(
})
}
if (!loadedModule?.service && !moduleResources.moduleService) {
const loadedModule_ = loadedModule as ModuleExports
if (
!loadingProviders &&
!loadedModule_?.service &&
!moduleResources.moduleService
) {
container.register({
[keyName]: asValue(undefined),
})
@@ -133,20 +225,6 @@ 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: moduleService_.prototype.__joinerConfig,
}
container.register({
[keyName]: asValue(moduleService),
})
return
}
const localContainer = createMedusaContainer()
const dependencies = resolution?.dependencies ?? []
@@ -177,6 +255,44 @@ export async function loadInternalModule(
)
}
// if module has providers, load them
let providerOptions: any = undefined
if (!loadingProviders) {
const providers = (resolution?.options?.providers as any[]) ?? []
const res = await loadInternalProvider(
{
...args,
container: localContainer,
},
providers
)
if (res?.error) {
return res
}
} else {
providerOptions = (resolution?.options?.providers as any[]).find(
(p) => p.id === resolution.definition.key
)?.options
}
if (migrationOnly && !loadingProviders) {
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: moduleService_.prototype.__joinerConfig,
}
container.register({
[keyName]: asValue(moduleService),
})
return
}
const loaders = moduleResources.loaders ?? loadedModule?.loaders ?? []
const error = await runLoaders(loaders, {
container,
@@ -185,24 +301,56 @@ export async function loadInternalModule(
resolution,
loaderOnly,
keyName,
providerOptions,
})
if (error) {
return error
}
const moduleService = moduleResources.moduleService ?? loadedModule.service
if (loadingProviders) {
const loadedProvider_ = loadedModule as ModuleProviderExports
container.register({
[keyName]: asFunction((cradle) => {
;(moduleService as any).__type = MedusaModuleType
return new moduleService(
localContainer.cradle,
resolution.options,
resolution.moduleDeclaration
let moduleProviderServices = moduleResources.moduleService
? [moduleResources.moduleService]
: loadedProvider_.services ?? loadedProvider_
if (!moduleProviderServices) {
return
}
for (const moduleProviderService of moduleProviderServices) {
const modProvider_ = moduleProviderService as any
modProvider_.identifier ??= keyName
modProvider_.__type = MedusaModuleProviderType
const registrationKey = getProviderRegistrationKey(
modProvider_.identifier
)
}).singleton(),
})
container.register({
[registrationKey]: asFunction((cradle) => {
;(moduleProviderService as any).__type = MedusaModuleType
return new moduleProviderService(
localContainer.cradle,
resolution.options,
resolution.moduleDeclaration
)
}).singleton(),
})
}
} else {
const moduleService = moduleResources.moduleService ?? loadedModule_.service
container.register({
[keyName]: asFunction((cradle) => {
;(moduleService as any).__type = MedusaModuleType
return new moduleService(
localContainer.cradle,
resolution.options,
resolution.moduleDeclaration
)
}).singleton(),
})
}
if (loaderOnly) {
// The expectation is only to run the loader as standalone, so we do not need to register the service and we need to cleanup all services
@@ -220,46 +368,114 @@ export async function loadModuleMigrations(
revertMigration?: MigrationFunction
generateMigration?: MigrationFunction
}> {
const loadedModule = await resolveModuleExports({
const mainLoadedModule = await resolveModuleExports({
resolution: { ...resolution, moduleExports },
})
if ("error" in loadedModule) {
throw loadedModule.error
}
const loadedServices = [mainLoadedModule] as (
| ResolvedModule
| ResolvedModuleProvider
)[]
try {
let runMigrations = loadedModule.runMigrations
let revertMigration = loadedModule.revertMigration
let generateMigration = loadedModule.generateMigration
if (Array.isArray(resolution?.options?.providers)) {
for (const provider of (resolution.options as any).providers) {
const providerRes = provider.resolve as ModuleProviderExports
if (!runMigrations || !revertMigration) {
const moduleResources = await loadResources({
moduleResolution: resolution,
discoveryPath: loadedModule.discoveryPath,
loadedModuleLoaders: loadedModule?.loaders,
})
const canLoadProvider =
providerRes && (isString(providerRes) || !providerRes?.services)
const migrationScriptOptions = {
moduleName: resolution.definition.key,
models: moduleResources.models,
pathToMigrations: join(moduleResources.normalizedPath, "migrations"),
if (!canLoadProvider) {
continue
}
runMigrations ??= ModulesSdkUtils.buildMigrationScript(
migrationScriptOptions
)
const loadedProvider = await resolveModuleExports({
resolution: {
...resolution,
moduleExports: !isString(providerRes) ? providerRes : undefined,
definition: {
...resolution.definition,
key: provider.id,
},
resolutionPath: isString(provider.resolve) ? provider.resolve : false,
},
})
loadedServices.push(loadedProvider as ResolvedModuleProvider)
}
}
revertMigration ??= ModulesSdkUtils.buildRevertMigrationScript(
migrationScriptOptions
)
if ("error" in mainLoadedModule) {
throw mainLoadedModule.error
}
generateMigration ??= ModulesSdkUtils.buildGenerateMigrationScript(
migrationScriptOptions
)
const runMigrationsFn: ((...args) => Promise<any>)[] = []
const revertMigrationFn: ((...args) => Promise<any>)[] = []
const generateMigrationFn: ((...args) => Promise<any>)[] = []
try {
const migrationScripts: any[] = []
for (const loadedModule of loadedServices) {
let runMigrationsCustom = loadedModule.runMigrations
let revertMigrationCustom = loadedModule.revertMigration
let generateMigrationCustom = loadedModule.generateMigration
runMigrationsCustom && runMigrationsFn.push(runMigrationsCustom)
revertMigrationCustom && revertMigrationFn.push(revertMigrationCustom)
generateMigrationCustom &&
generateMigrationFn.push(generateMigrationCustom)
if (!runMigrationsCustom || !revertMigrationCustom) {
const moduleResources = await loadResources({
moduleResolution: resolution,
discoveryPath: loadedModule.discoveryPath,
loadedModuleLoaders: loadedModule?.loaders,
})
migrationScripts.push({
moduleName: resolution.definition.key,
models: moduleResources.models,
pathToMigrations: join(moduleResources.normalizedPath, "migrations"),
})
}
for (const migrationScriptOptions of migrationScripts) {
const migrationUp =
runMigrationsCustom ??
ModulesSdkUtils.buildMigrationScript(migrationScriptOptions)
runMigrationsFn.push(migrationUp)
const migrationDown =
revertMigrationCustom ??
ModulesSdkUtils.buildRevertMigrationScript(migrationScriptOptions)
revertMigrationFn.push(migrationDown)
const genMigration =
generateMigrationCustom ??
ModulesSdkUtils.buildGenerateMigrationScript(migrationScriptOptions)
generateMigrationFn.push(genMigration)
}
}
return { runMigrations, revertMigration, generateMigration }
const runMigrations = async (...args) => {
for (const migration of runMigrationsFn.filter(Boolean)) {
await migration.apply(migration, args)
}
}
const revertMigration = async (...args) => {
for (const migration of revertMigrationFn.filter(Boolean)) {
await migration.apply(migration, args)
}
}
const generateMigration = async (...args) => {
for (const migration of generateMigrationFn.filter(Boolean)) {
await migration.apply(migration, args)
}
}
return {
runMigrations,
revertMigration,
generateMigration,
}
} catch {
return {}
}
@@ -308,7 +524,7 @@ export async function loadResources({
moduleResolution: ModuleResolution
discoveryPath: string
logger?: Logger
loadedModuleLoaders?: ModuleLoaderFunction[]
loadedModuleLoaders?: ModuleLoaderFunction[] | ModuleProviderLoaderFunction[]
}): Promise<ModuleResource> {
logger ??= console as unknown as Logger
loadedModuleLoaders ??= []
@@ -324,7 +540,8 @@ export async function loadResources({
const [moduleService, services, models, repositories] = await Promise.all([
dynamicImport(modulePath).then((moduleExports) => {
return moduleExports.default.service
const mod = moduleExports.default ?? moduleExports
return mod.service
}),
importAllFromDir(resolve(normalizedPath, "services")).catch(
defaultOnFail
@@ -365,11 +582,14 @@ export async function loadResources({
migrationPath: normalizedPath + "/migrations",
})
generateJoinerConfigIfNecessary({
moduleResolution,
service: moduleService,
models: potentialModels,
})
// if a module service is provided, we generate a joiner config
if (moduleService) {
generateJoinerConfigIfNecessary({
moduleResolution,
service: moduleService,
models: potentialModels,
})
}
return {
services: potentialServices,
@@ -390,7 +610,15 @@ export async function loadResources({
async function runLoaders(
loaders: Function[] = [],
{ localContainer, container, logger, resolution, loaderOnly, keyName }
{
localContainer,
container,
logger,
resolution,
loaderOnly,
keyName,
providerOptions,
}
): Promise<void | { error: Error }> {
try {
for (const loader of loaders) {
@@ -398,8 +626,9 @@ async function runLoaders(
{
container: localContainer,
logger,
options: resolution.options,
options: providerOptions ?? resolution.options,
dataLoaderOnly: loaderOnly,
moduleOptions: providerOptions ? resolution.options : undefined,
},
resolution.moduleDeclaration as InternalModuleDeclaration
)
@@ -418,14 +647,17 @@ async function runLoaders(
}
function prepareLoaders({
loadedModuleLoaders = [] as ModuleLoaderFunction[],
loadedModuleLoaders = [] as
| ModuleLoaderFunction[]
| ModuleProviderLoaderFunction[],
models,
repositories,
services,
moduleResolution,
migrationPath,
}) {
const finalLoaders: ModuleLoaderFunction[] = []
const finalLoaders: (ModuleLoaderFunction | ModuleProviderLoaderFunction)[] =
[]
const toObjectReducer = (acc, curr) => {
acc[curr.name] = curr
+1 -1
View File
@@ -510,7 +510,7 @@ async function MedusaApp_({
modulePath: moduleResolution.resolutionPath as string,
container: sharedContainer,
options: moduleResolution.options,
moduleExports: moduleResolution.moduleExports,
moduleExports: moduleResolution.moduleExports as ModuleExports,
}
if (action === "revert") {