chore(medusa,orchestration,link-modules,modules-sdk): internal services as modules (#4925)

This commit is contained in:
Carlos R. L. Rodrigues
2023-09-04 11:36:05 -03:00
committed by GitHub
parent 85fa90d9c6
commit a4906d0ac0
15 changed files with 348 additions and 110 deletions

View File

@@ -0,0 +1,8 @@
---
"@medusajs/orchestration": minor
"@medusajs/link-modules": minor
"@medusajs/modules-sdk": minor
"@medusajs/medusa": minor
---
Use MedusaApp on core and initial JoinerConfig for internal services

View File

@@ -2,6 +2,7 @@ const path = require("path")
const express = require("express")
const importFrom = require("import-from")
const chokidar = require("chokidar")
const { WorkflowManager } = require("@medusajs/orchestration")
process.env.DEV_MODE = !!process[Symbol.for("ts-node.register.instance")]
process.env.NODE_ENV = process.env.DEV_MODE && "development"
@@ -107,6 +108,8 @@ const watchFiles = () => {
}
}
WorkflowManager.unregisterAll()
await bootstrapApp()
IS_RELOADING = false

View File

@@ -0,0 +1,52 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { LINKS } from "../links"
export const ProductShippingProfile: ModuleJoinerConfig = {
serviceName: LINKS.ProductShippingProfile,
isLink: true,
databaseConfig: {
tableName: "product_shipping_profile",
idPrefix: "psprof",
},
alias: [
{
name: "product_shipping_profile",
},
],
primaryKeys: ["id", "product_id", "profile_id"],
relationships: [
{
serviceName: Modules.PRODUCT,
primaryKey: "id",
foreignKey: "product_id",
alias: "product",
},
{
serviceName: "shippingProfileService",
primaryKey: "id",
foreignKey: "profile_id",
alias: "shipping_profile",
},
],
extends: [
{
serviceName: Modules.PRODUCT,
relationship: {
serviceName: LINKS.ProductShippingProfile,
primaryKey: "product_id",
foreignKey: "id",
alias: "shipping_profile",
},
},
{
serviceName: "shippingProfileService",
relationship: {
serviceName: LINKS.ProductShippingProfile,
primaryKey: "profile_id",
foreignKey: "id",
alias: "product_link",
},
},
],
}

View File

@@ -14,4 +14,12 @@ export const LINKS = {
Modules.PRICING,
"money_amount_id"
),
// Internal services
ProductShippingProfile: composeLinkName(
Modules.PRODUCT,
"variant_id",
"shippingProfileService",
"profile_id"
),
}

View File

@@ -0,0 +1,77 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
export const joinerConfig: ModuleJoinerConfig[] = [
{
serviceName: "cartService",
primaryKeys: ["id"],
linkableKeys: ["cart_id"],
alias: [
{
name: "cart",
},
],
relationships: [
{
serviceName: Modules.PRODUCT,
primaryKey: "id",
foreignKey: "variant_id",
alias: "variant",
args: {
methodSuffix: "Variants",
},
},
{
serviceName: "regionService",
primaryKey: "id",
foreignKey: "region_id",
alias: "region",
},
{
serviceName: "customerService",
primaryKey: "id",
foreignKey: "customer_id",
alias: "customer",
},
],
},
{
serviceName: "shippingProfileService",
primaryKeys: ["id"],
linkableKeys: ["profile_id"],
alias: [
{
name: "shipping_profile",
},
{
name: "shipping_profiles",
},
],
},
{
serviceName: "regionService",
primaryKeys: ["id"],
linkableKeys: ["region_id"],
alias: [
{
name: "region",
},
{
name: "regions",
},
],
},
{
serviceName: "customerService",
primaryKeys: ["id"],
linkableKeys: ["customer_id"],
alias: [
{
name: "customer",
},
{
name: "customers",
},
],
},
]

View File

@@ -0,0 +1,10 @@
import { FlagSettings } from "../../types/feature-flags"
const IsolateProductDomainFeatureFlag: FlagSettings = {
key: "isolate_product_domain",
default_val: false,
env_key: "MEDUSA_FF_ISOLATE_PRODUCT_DOMAIN",
description: "[WIP] Isolate product domain dependencies from the core",
}
export default IsolateProductDomainFeatureFlag

View File

@@ -1,20 +1,28 @@
import { MedusaApp, moduleLoader, registerModules } from "@medusajs/modules-sdk"
import { ContainerRegistrationKeys } from "@medusajs/utils"
import { asValue } from "awilix"
import { Express, NextFunction, Request, Response } from "express"
import { createMedusaContainer } from "medusa-core-utils"
import { track } from "medusa-telemetry"
import { EOL } from "os"
import "reflect-metadata"
import requestIp from "request-ip"
import { Connection } from "typeorm"
import { joinerConfig } from "../joiner-config"
import modulesConfig from "../modules-config"
import { MedusaContainer } from "../types/global"
import { remoteQueryFetchData } from "../utils"
import apiLoader from "./api"
import loadConfig from "./config"
import databaseLoader, { dataSource } from "./database"
import defaultsLoader from "./defaults"
import expressLoader from "./express"
import featureFlagsLoader from "./feature-flags"
import IsolateProductDomainFeatureFlag from "./feature-flags/isolate-product-domain"
import Logger from "./logger"
import modelsLoader from "./models"
import passportLoader from "./passport"
import pgConnectionLoader from "./pg-connection"
import pluginsLoader, { registerPluginModels } from "./plugins"
import redisLoader from "./redis"
import repositoriesLoader from "./repositories"
@@ -23,11 +31,6 @@ import servicesLoader from "./services"
import strategiesLoader from "./strategies"
import subscribersLoader from "./subscribers"
import { moduleLoader, registerModules } from "@medusajs/modules-sdk"
import { createMedusaContainer } from "medusa-core-utils"
import pgConnectionLoader from "./pg-connection"
import { ContainerRegistrationKeys } from "@medusajs/utils"
type Options = {
directory: string
expressApp: Express
@@ -182,5 +185,20 @@ export default async ({
Logger.success(searchActivity, "Indexing event emitted") || {}
track("SEARCH_ENGINE_INDEXING_COMPLETED", { duration: searchAct.duration })
if (featureFlagRouter.isFeatureEnabled(IsolateProductDomainFeatureFlag.key)) {
const { query } = await MedusaApp({
modulesConfig,
servicesConfig: joinerConfig,
remoteFetchData: remoteQueryFetchData(container),
injectedDependencies: {
[ContainerRegistrationKeys.PG_CONNECTION]: container.resolve(
ContainerRegistrationKeys.PG_CONNECTION
),
},
})
container.register("remoteQuery", asValue(query))
}
return { container, dbConnection, app: expressApp }
}

View File

@@ -0,0 +1,9 @@
import { MedusaModuleConfig, Modules } from "@medusajs/modules-sdk"
const modules: MedusaModuleConfig = [
{
module: Modules.PRODUCT,
},
]
export default modules

View File

@@ -11,8 +11,9 @@ export * from "./is-object"
export * from "./is-string"
export * from "./omit-deep"
export * from "./product-category"
export * from "./remote-query-fetch-data"
export * from "./remove-undefined-properties"
export * from "./set-metadata"
export * from "./validate-id"
export * from "./validators/is-type"
export { registerOverriddenValidators, validator } from "./validator"
export * from "./validators/is-type"

View File

@@ -0,0 +1,53 @@
import { MedusaModule, RemoteQuery } from "@medusajs/modules-sdk"
import { MedusaContainer } from "@medusajs/types"
export function remoteQueryFetchData(container: MedusaContainer) {
return async (expand, keyField, ids, relationship) => {
const serviceConfig = expand.serviceConfig
const service = container.resolve(serviceConfig.serviceName, {
allowUnregistered: true,
})
if (MedusaModule.isInstalled(serviceConfig.serviceName)) {
return
}
const filters = {}
const options = {
...RemoteQuery.getAllFieldsAndRelations(expand),
}
if (ids) {
filters[keyField] = ids
}
const hasPagination = Object.keys(options).some((key) =>
["skip"].includes(key)
)
let methodName = hasPagination ? "listAndCount" : "list"
if (relationship?.args?.methodSuffix) {
methodName += relationship.args.methodSuffix
} else if (serviceConfig?.args?.methodSuffix) {
methodName += serviceConfig.args.methodSuffix
}
const result = await service[methodName](filters, options)
if (hasPagination) {
const [data, count] = result
return {
data: {
rows: data,
metadata: {},
},
path: "rows",
}
}
return {
data: result,
} as any
}
}

View File

@@ -36,20 +36,30 @@ export type SharedResources = {
}
}
export async function MedusaApp({
sharedResourcesConfig,
modulesConfigPath,
modulesConfig,
linkModules,
remoteFetchData,
}: {
sharedResourcesConfig?: SharedResources
loadedModules?: LoadedModule[]
modulesConfigPath?: string
modulesConfig?: MedusaModuleConfig
linkModules?: ModuleJoinerConfig | ModuleJoinerConfig[]
remoteFetchData?: RemoteFetchDataCallback
} = {}): Promise<{
export async function MedusaApp(
{
sharedResourcesConfig,
servicesConfig,
modulesConfigPath,
modulesConfigFileName,
modulesConfig,
linkModules,
remoteFetchData,
injectedDependencies,
}: {
sharedResourcesConfig?: SharedResources
loadedModules?: LoadedModule[]
servicesConfig?: ModuleJoinerConfig[]
modulesConfigPath?: string
modulesConfigFileName?: string
modulesConfig?: MedusaModuleConfig
linkModules?: ModuleJoinerConfig | ModuleJoinerConfig[]
remoteFetchData?: RemoteFetchDataCallback
injectedDependencies?: any
} = {
injectedDependencies: {},
}
): Promise<{
modules: Record<string, LoadedModule | LoadedModule[]>
link: RemoteLink | undefined
query: (
@@ -59,10 +69,12 @@ export async function MedusaApp({
}> {
const modules: MedusaModuleConfig =
modulesConfig ??
(await import(process.cwd() + (modulesConfigPath ?? "/modules-config")))
.default
const injectedDependencies: any = {}
(
await import(
modulesConfigPath ??
process.cwd() + (modulesConfigFileName ?? "/modules-config")
)
).default
const dbData = ModulesSdkUtils.loadDatabaseConfig(
"medusa",
@@ -167,7 +179,10 @@ export async function MedusaApp({
console.warn("Error initializing link modules.", err)
}
const remoteQuery = new RemoteQuery(undefined, remoteFetchData)
const remoteQuery = new RemoteQuery({
servicesConfig,
customRemoteFetchData: remoteFetchData,
})
query = async (
query: string | RemoteJoinerQuery,
variables?: Record<string, unknown>

View File

@@ -17,8 +17,17 @@ export class RemoteQuery {
private customRemoteFetchData?: RemoteFetchDataCallback
constructor(
modulesLoaded?: LoadedModule[],
customRemoteFetchData?: RemoteFetchDataCallback
{
modulesLoaded,
customRemoteFetchData,
servicesConfig,
}: {
modulesLoaded?: LoadedModule[]
customRemoteFetchData?: RemoteFetchDataCallback
servicesConfig?: ModuleJoinerConfig[]
} = {
servicesConfig: [],
}
) {
if (!modulesLoaded?.length) {
modulesLoaded = MedusaModule.getLoadedModules().map(
@@ -26,7 +35,6 @@ export class RemoteQuery {
)
}
const servicesConfig: ModuleJoinerConfig[] = []
for (const mod of modulesLoaded) {
if (!mod.__definition.isQueryable) {
continue
@@ -41,7 +49,7 @@ export class RemoteQuery {
}
this.modulesMap.set(serviceName, mod)
servicesConfig.push(mod.__joinerConfig)
servicesConfig!.push(mod.__joinerConfig)
}
this.customRemoteFetchData = customRemoteFetchData
@@ -65,7 +73,7 @@ export class RemoteQuery {
this.remoteJoiner.setFetchDataCallback(remoteFetchData)
}
private static getAllFieldsAndRelations(
public static getAllFieldsAndRelations(
data: any,
prefix = "",
args: Record<string, unknown[]> = {}

View File

@@ -317,13 +317,13 @@ describe("RemoteJoiner", () => {
expect(serviceMock.productService).toHaveBeenCalledTimes(2)
expect(serviceMock.productService).toHaveBeenNthCalledWith(1, {
fields: ["name", "id"],
options: { id: expect.arrayContaining([103, 102]) },
})
expect(serviceMock.productService).toHaveBeenNthCalledWith(2, {
fields: ["handler", "id"],
options: { id: expect.arrayContaining([101, 103]) },
})
expect(serviceMock.productService).toHaveBeenNthCalledWith(2, {
fields: ["name", "id"],
options: { id: expect.arrayContaining([103, 102]) },
})
})
})

View File

@@ -290,46 +290,38 @@ export class RemoteJoiner {
if (!parsedExpands) {
return
}
const stack: [
any[],
Partial<RemoteJoinerQuery>,
Map<string, RemoteExpandProperty>,
string,
Set<string>
][] = [[items, query, parsedExpands, BASE_PATH, new Set()]]
const resolvedPaths = new Set<string>()
const stack: [any[], Partial<RemoteJoinerQuery>, string][] = [
[items, query, BASE_PATH],
]
while (stack.length > 0) {
const [
currentItems,
currentQuery,
currentParsedExpands,
basePath,
resolvedPaths,
] = stack.pop()!
const [currentItems, currentQuery, basePath] = stack.pop()!
for (const [expandedPath, expand] of currentParsedExpands.entries()) {
const isImmediateChildPath = basePath === expand.parent
for (const [expandedPath, expand] of parsedExpands.entries()) {
const isParentPath = expandedPath.startsWith(basePath)
if (!isImmediateChildPath || resolvedPaths.has(expandedPath)) {
if (!isParentPath || resolvedPaths.has(expandedPath)) {
continue
}
resolvedPaths.add(expandedPath)
const property = expand.property || ""
const parentServiceConfig = this.getServiceConfig(
currentQuery.service,
currentQuery.alias
)
await this.expandProperty(currentItems, parentServiceConfig!, expand)
let curItems = currentItems
const expandedPathLevels = expandedPath.split(".")
for (let idx = 1; idx < expandedPathLevels.length - 1; idx++) {
curItems = RemoteJoiner.getNestedItems(
curItems,
expandedPathLevels[idx]
)
}
await this.expandProperty(curItems, expand.parentConfig!, expand)
const nestedItems = RemoteJoiner.getNestedItems(currentItems, property)
if (nestedItems.length > 0) {
const relationship = expand.serviceConfig
let nextProp = currentQuery
if (relationship) {
const relQuery = {
@@ -337,14 +329,7 @@ export class RemoteJoiner {
}
nextProp = relQuery
}
stack.push([
nestedItems,
nextProp,
currentParsedExpands,
expandedPath,
new Set(),
])
stack.push([nestedItems, nextProp, expandedPath])
}
}
}
@@ -538,12 +523,14 @@ export class RemoteJoiner {
}
if (!parsedExpands.has(fullPath)) {
const parentPath = [BASE_PATH, ...currentPath].join(".")
parsedExpands.set(fullPath, {
property: prop,
serviceConfig: currentServiceConfig,
fields,
args,
parent: [BASE_PATH, ...currentPath].join("."),
parent: parentPath,
parentConfig: parsedExpands.get(parentPath).serviceConfig,
})
}
@@ -556,53 +543,41 @@ export class RemoteJoiner {
private groupExpands(
parsedExpands: Map<string, RemoteExpandProperty>
): Map<string, RemoteExpandProperty> {
const sortedParsedExpands = new Map(
Array.from(parsedExpands.entries()).sort()
)
const mergedExpands = new Map<string, RemoteExpandProperty>(
sortedParsedExpands
)
const mergedExpands = new Map<string, RemoteExpandProperty>(parsedExpands)
const mergedPaths = new Map<string, string>()
let lastServiceName = ""
for (const [path, expand] of sortedParsedExpands.entries()) {
for (const [path, expand] of mergedExpands.entries()) {
const currentServiceName = expand.serviceConfig.serviceName
let parentPath = expand.parent
// Check if the parentPath was merged before
while (mergedPaths.has(parentPath)) {
parentPath = mergedPaths.get(parentPath)!
}
const canMerge = currentServiceName === lastServiceName
if (mergedExpands.has(parentPath) && canMerge) {
const parentExpand = mergedExpands.get(parentPath)!
if (parentExpand.serviceConfig.serviceName === currentServiceName) {
const nestedKeys = path.split(".").slice(parentPath.split(".").length)
let targetExpand: any = parentExpand
for (let key of nestedKeys) {
if (!targetExpand.expands) {
targetExpand.expands = {}
}
if (!targetExpand.expands[key]) {
targetExpand.expands[key] = {} as any
}
targetExpand = targetExpand.expands[key]
}
targetExpand.fields = expand.fields
targetExpand.args = expand.args
mergedPaths.set(path, parentPath)
while (parentPath) {
const parentExpand = mergedExpands.get(parentPath)
if (
!parentExpand ||
parentExpand.serviceConfig.serviceName !== currentServiceName
) {
break
}
} else {
lastServiceName = currentServiceName
// Merge the current expand into its parent
const nestedKeys = path.split(".").slice(parentPath.split(".").length)
let targetExpand = parentExpand as Omit<
RemoteExpandProperty,
"expands"
> & { expands?: {} }
for (const key of nestedKeys) {
targetExpand.expands ??= {}
targetExpand = targetExpand.expands[key] ??= {}
}
targetExpand.fields = expand.fields
targetExpand.args = expand.args
mergedExpands.delete(path)
mergedPaths.set(path, parentPath)
parentPath = parentExpand.parent
}
}

View File

@@ -60,6 +60,7 @@ export interface RemoteNestedExpands {
export interface RemoteExpandProperty {
property: string
parent: string
parentConfig?: JoinerServiceConfig
serviceConfig: JoinerServiceConfig
fields: string[]
args?: JoinerArgument[]