feat(*): Modules export entities and fields (#5242)
This commit is contained in:
committed by
GitHub
parent
eeceec791c
commit
130cbc1f43
@@ -16,11 +16,11 @@ export const InventoryModule = {
|
||||
__joinerConfig: {
|
||||
serviceName: "inventoryService",
|
||||
primaryKeys: ["id"],
|
||||
linkableKeys: [
|
||||
"inventory_item_id",
|
||||
"inventory_level_id",
|
||||
"reservation_item_id",
|
||||
],
|
||||
linkableKeys: {
|
||||
inventory_item_id: "InventoryItem",
|
||||
inventory_level_id: "InventoryLevel",
|
||||
reservation_item_id: "ReservationItem",
|
||||
},
|
||||
},
|
||||
|
||||
softDelete: jest.fn(() => {}),
|
||||
|
||||
@@ -16,7 +16,7 @@ export const ProductModule = {
|
||||
__joinerConfig: {
|
||||
serviceName: "productService",
|
||||
primaryKeys: ["id", "handle"],
|
||||
linkableKeys: ["product_id", "variant_id"],
|
||||
linkableKeys: { product_id: "Product", variant_id: "ProductVariant" },
|
||||
alias: [],
|
||||
},
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export const StockLocationModule = {
|
||||
__joinerConfig: {
|
||||
serviceName: "stockLocationService",
|
||||
primaryKeys: ["id"],
|
||||
linkableKeys: ["stock_location_id"],
|
||||
linkableKeys: { stock_location_id: "StockLocation" },
|
||||
alias: [],
|
||||
},
|
||||
|
||||
|
||||
111
packages/modules-sdk/src/__tests__/clean-graphql-schema.spec.ts
Normal file
111
packages/modules-sdk/src/__tests__/clean-graphql-schema.spec.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { cleanGraphQLSchema } from "../utils/clean-graphql-schema"
|
||||
|
||||
describe("Clean Graphql Schema", function () {
|
||||
it("Should keep the schema intact if all entities are available", function () {
|
||||
const schemaStr = `
|
||||
type Product {
|
||||
id: ID!
|
||||
title: String!
|
||||
variants: [Variant]!
|
||||
}
|
||||
type Variant {
|
||||
id: ID!
|
||||
title: String!
|
||||
product_id: ID!
|
||||
product: Product!
|
||||
}
|
||||
`
|
||||
const { schema, notFound } = cleanGraphQLSchema(schemaStr)
|
||||
|
||||
expect(schema.replace(/\s/g, "")).toEqual(schemaStr.replace(/\s/g, ""))
|
||||
expect(notFound).toEqual({})
|
||||
})
|
||||
|
||||
it("Should remove fields where the relation doesn't exist", function () {
|
||||
const schemaStr = `
|
||||
type Product {
|
||||
id: ID!
|
||||
title: String!
|
||||
variants: [Variant!]!
|
||||
profile: ShippingProfile!
|
||||
}
|
||||
`
|
||||
const expectedStr = `
|
||||
type Product {
|
||||
id: ID!
|
||||
title: String!
|
||||
}
|
||||
`
|
||||
const { schema, notFound } = cleanGraphQLSchema(schemaStr)
|
||||
|
||||
expect(schema.replace(/\s/g, "")).toEqual(expectedStr.replace(/\s/g, ""))
|
||||
expect(notFound).toEqual({
|
||||
Product: { variants: "Variant", profile: "ShippingProfile" },
|
||||
})
|
||||
})
|
||||
|
||||
it("Should remove fields where the relation doesn't exist and flag extended entity where the main entity doesn't exist", function () {
|
||||
const schemaStr = `
|
||||
scalar JSON
|
||||
type Product {
|
||||
id: ID!
|
||||
title: String!
|
||||
variants: [Variant!]!
|
||||
profile: ShippingProfile!
|
||||
}
|
||||
|
||||
extend type Variant {
|
||||
metadata: JSON
|
||||
}
|
||||
`
|
||||
const expectedStr = `
|
||||
scalar JSON
|
||||
type Product {
|
||||
id: ID!
|
||||
title: String!
|
||||
}
|
||||
`
|
||||
const { schema, notFound } = cleanGraphQLSchema(schemaStr)
|
||||
|
||||
expect(schema.replace(/\s/g, "")).toEqual(expectedStr.replace(/\s/g, ""))
|
||||
expect(notFound).toEqual({
|
||||
Product: { variants: "Variant", profile: "ShippingProfile" },
|
||||
Variant: { __extended: "" },
|
||||
})
|
||||
})
|
||||
|
||||
it("Should remove fields from extend where the relation doesn't exist", function () {
|
||||
const schemaStr = `
|
||||
scalar JSON
|
||||
type Product {
|
||||
id: ID!
|
||||
title: String!
|
||||
variants: [Variant!]!
|
||||
profile: ShippingProfile!
|
||||
}
|
||||
|
||||
extend type Product {
|
||||
variants: [Variant!]!
|
||||
profile: ShippingProfile!
|
||||
metadata: JSON
|
||||
}
|
||||
`
|
||||
const expectedStr = `
|
||||
scalar JSON
|
||||
type Product {
|
||||
id: ID!
|
||||
title: String!
|
||||
}
|
||||
|
||||
extend type Product {
|
||||
metadata: JSON
|
||||
}
|
||||
`
|
||||
const { schema, notFound } = cleanGraphQLSchema(schemaStr)
|
||||
|
||||
expect(schema.replace(/\s/g, "")).toEqual(expectedStr.replace(/\s/g, ""))
|
||||
expect(notFound).toEqual({
|
||||
Product: { variants: "Variant", profile: "ShippingProfile" },
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,5 @@
|
||||
import { mergeTypeDefs } from "@graphql-tools/merge"
|
||||
import { makeExecutableSchema } from "@graphql-tools/schema"
|
||||
import { RemoteFetchDataCallback } from "@medusajs/orchestration"
|
||||
import {
|
||||
ExternalModuleDeclaration,
|
||||
@@ -12,13 +14,14 @@ import {
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
isObject,
|
||||
ModulesSdkUtils,
|
||||
isObject,
|
||||
} from "@medusajs/utils"
|
||||
import { MODULE_PACKAGE_NAMES, Modules } from "./definitions"
|
||||
import { MedusaModule } from "./medusa-module"
|
||||
import { RemoteLink } from "./remote-link"
|
||||
import { RemoteQuery } from "./remote-query"
|
||||
import { cleanGraphQLSchema } from "./utils"
|
||||
|
||||
export type MedusaModuleConfig = {
|
||||
[key: string | Modules]:
|
||||
@@ -46,72 +49,19 @@ export type SharedResources = {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}): Promise<{
|
||||
modules: Record<string, LoadedModule | LoadedModule[]>
|
||||
link: RemoteLink | undefined
|
||||
query: (
|
||||
query: string | RemoteJoinerQuery | object,
|
||||
variables?: Record<string, unknown>
|
||||
) => Promise<any>
|
||||
}> {
|
||||
const modules: MedusaModuleConfig =
|
||||
modulesConfig ??
|
||||
(
|
||||
await import(
|
||||
modulesConfigPath ??
|
||||
process.cwd() + (modulesConfigFileName ?? "/modules-config")
|
||||
)
|
||||
).default
|
||||
|
||||
const dbData = ModulesSdkUtils.loadDatabaseConfig(
|
||||
"medusa",
|
||||
sharedResourcesConfig as ModuleServiceInitializeOptions,
|
||||
true
|
||||
)!
|
||||
|
||||
if (
|
||||
dbData.clientUrl &&
|
||||
!injectedDependencies[ContainerRegistrationKeys.PG_CONNECTION]
|
||||
) {
|
||||
injectedDependencies[ContainerRegistrationKeys.PG_CONNECTION] =
|
||||
ModulesSdkUtils.createPgConnection({
|
||||
...(sharedResourcesConfig?.database ?? {}),
|
||||
...dbData,
|
||||
})
|
||||
}
|
||||
|
||||
const allModules: Record<string, LoadedModule | LoadedModule[]> = {}
|
||||
|
||||
async function loadModules(modulesConfig, injectedDependencies) {
|
||||
const allModules = {}
|
||||
await Promise.all(
|
||||
Object.keys(modules).map(async (moduleName) => {
|
||||
const mod = modules[moduleName] as MedusaModuleConfig
|
||||
|
||||
Object.keys(modulesConfig).map(async (moduleName) => {
|
||||
const mod = modulesConfig[moduleName]
|
||||
let path: string
|
||||
let declaration: any = {}
|
||||
let definition: ModuleDefinition | undefined = undefined
|
||||
|
||||
if (isObject(mod)) {
|
||||
const mod_ = mod as unknown as InternalModuleDeclaration
|
||||
path = mod_.resolve ?? MODULE_PACKAGE_NAMES[moduleName]
|
||||
|
||||
definition = mod_.definition
|
||||
declaration = { ...mod }
|
||||
delete declaration.definition
|
||||
} else {
|
||||
@@ -119,7 +69,6 @@ export async function MedusaApp({
|
||||
}
|
||||
|
||||
declaration.scope ??= MODULE_SCOPE.INTERNAL
|
||||
|
||||
if (
|
||||
declaration.scope === MODULE_SCOPE.INTERNAL &&
|
||||
!declaration.resources
|
||||
@@ -133,7 +82,7 @@ export async function MedusaApp({
|
||||
declaration,
|
||||
undefined,
|
||||
injectedDependencies,
|
||||
(isObject(mod) ? mod.definition : undefined) as ModuleDefinition
|
||||
definition
|
||||
)) as LoadedModule
|
||||
|
||||
if (allModules[moduleName] && !Array.isArray(allModules[moduleName])) {
|
||||
@@ -145,33 +94,117 @@ export async function MedusaApp({
|
||||
} else {
|
||||
allModules[moduleName] = loaded[moduleName]
|
||||
}
|
||||
|
||||
return loaded
|
||||
})
|
||||
)
|
||||
return allModules
|
||||
}
|
||||
|
||||
let link: RemoteLink | undefined = undefined
|
||||
let query: (
|
||||
query: string | RemoteJoinerQuery | object,
|
||||
variables?: Record<string, unknown>
|
||||
) => Promise<any>
|
||||
|
||||
async function initializeLinks(linkModules, injectedDependencies) {
|
||||
try {
|
||||
const { initialize: initializeLinks } = await import(
|
||||
"@medusajs/link-modules" as string
|
||||
)
|
||||
await initializeLinks({}, linkModules, injectedDependencies)
|
||||
|
||||
link = new RemoteLink()
|
||||
return new RemoteLink()
|
||||
} catch (err) {
|
||||
console.warn("Error initializing link modules.", err)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function cleanAndMergeSchema(loadedSchema) {
|
||||
const { schema: cleanedSchema, notFound } = cleanGraphQLSchema(loadedSchema)
|
||||
const mergedSchema = mergeTypeDefs(cleanedSchema)
|
||||
return { schema: makeExecutableSchema({ typeDefs: mergedSchema }), notFound }
|
||||
}
|
||||
|
||||
function getLoadedSchema(): string {
|
||||
return MedusaModule.getAllJoinerConfigs()
|
||||
.map((joinerConfig) => joinerConfig?.schema ?? "")
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
function registerCustomJoinerConfigs(servicesConfig: ModuleJoinerConfig[]) {
|
||||
for (const config of servicesConfig) {
|
||||
if (!config.serviceName || config.isReadOnlyLink) {
|
||||
continue
|
||||
}
|
||||
|
||||
MedusaModule.setJoinerConfig(config.serviceName, config)
|
||||
}
|
||||
}
|
||||
|
||||
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: (
|
||||
query: string | RemoteJoinerQuery | object,
|
||||
variables?: Record<string, unknown>
|
||||
) => Promise<any>
|
||||
entitiesMap?: Record<string, any>
|
||||
notFound?: Record<string, Record<string, string>>
|
||||
}> {
|
||||
const modules: MedusaModuleConfig =
|
||||
modulesConfig ??
|
||||
(
|
||||
await import(
|
||||
modulesConfigPath ??
|
||||
process.cwd() + (modulesConfigFileName ?? "/modules-config")
|
||||
)
|
||||
).default
|
||||
const dbData = ModulesSdkUtils.loadDatabaseConfig(
|
||||
"medusa",
|
||||
sharedResourcesConfig as ModuleServiceInitializeOptions,
|
||||
true
|
||||
)!
|
||||
|
||||
registerCustomJoinerConfigs(servicesConfig ?? [])
|
||||
|
||||
if (
|
||||
dbData.clientUrl &&
|
||||
!injectedDependencies[ContainerRegistrationKeys.PG_CONNECTION]
|
||||
) {
|
||||
injectedDependencies[ContainerRegistrationKeys.PG_CONNECTION] =
|
||||
ModulesSdkUtils.createPgConnection({
|
||||
...(sharedResourcesConfig?.database ?? {}),
|
||||
...dbData,
|
||||
})
|
||||
}
|
||||
|
||||
const allModules = await loadModules(modules, injectedDependencies)
|
||||
const link = await initializeLinks(linkModules, injectedDependencies)
|
||||
|
||||
const loadedSchema = getLoadedSchema()
|
||||
const { schema, notFound } = cleanAndMergeSchema(loadedSchema)
|
||||
|
||||
const remoteQuery = new RemoteQuery({
|
||||
servicesConfig,
|
||||
customRemoteFetchData: remoteFetchData,
|
||||
})
|
||||
query = async (
|
||||
const query = async (
|
||||
query: string | RemoteJoinerQuery | object,
|
||||
variables?: Record<string, unknown>
|
||||
) => {
|
||||
@@ -182,5 +215,7 @@ export async function MedusaApp({
|
||||
modules: allModules,
|
||||
link,
|
||||
query,
|
||||
entitiesMap: schema.getTypeMap(),
|
||||
notFound,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ export class MedusaModule {
|
||||
private static instances_: Map<string, any> = new Map()
|
||||
private static modules_: Map<string, ModuleAlias[]> = new Map()
|
||||
private static loading_: Map<string, Promise<any>> = new Map()
|
||||
private static joinerConfig_: Map<string, ModuleJoinerConfig> = new Map()
|
||||
|
||||
public static getLoadedModules(
|
||||
aliases?: Map<string, string>
|
||||
@@ -68,6 +69,7 @@ export class MedusaModule {
|
||||
public static clearInstances(): void {
|
||||
MedusaModule.instances_.clear()
|
||||
MedusaModule.modules_.clear()
|
||||
MedusaModule.joinerConfig_.clear()
|
||||
}
|
||||
|
||||
public static isInstalled(moduleKey: string, alias?: string): boolean {
|
||||
@@ -81,6 +83,22 @@ export class MedusaModule {
|
||||
return MedusaModule.modules_.has(moduleKey)
|
||||
}
|
||||
|
||||
public static getJoinerConfig(moduleKey: string): ModuleJoinerConfig {
|
||||
return MedusaModule.joinerConfig_.get(moduleKey)!
|
||||
}
|
||||
|
||||
public static getAllJoinerConfigs(): ModuleJoinerConfig[] {
|
||||
return [...MedusaModule.joinerConfig_.values()]
|
||||
}
|
||||
|
||||
public static setJoinerConfig(
|
||||
moduleKey: string,
|
||||
config: ModuleJoinerConfig
|
||||
): ModuleJoinerConfig {
|
||||
MedusaModule.joinerConfig_.set(moduleKey, config)
|
||||
return config
|
||||
}
|
||||
|
||||
public static getModuleInstance(
|
||||
moduleKey: string,
|
||||
alias?: string
|
||||
@@ -218,6 +236,7 @@ export class MedusaModule {
|
||||
].__joinerConfig()
|
||||
|
||||
services[keyName].__joinerConfig = joinerConfig
|
||||
MedusaModule.setJoinerConfig(keyName, joinerConfig)
|
||||
}
|
||||
|
||||
MedusaModule.registerModule(keyName, {
|
||||
@@ -329,6 +348,7 @@ export class MedusaModule {
|
||||
].__joinerConfig()
|
||||
|
||||
services[keyName].__joinerConfig = joinerConfig
|
||||
MedusaModule.setJoinerConfig(keyName, joinerConfig)
|
||||
|
||||
if (!joinerConfig.isLink) {
|
||||
throw new Error(
|
||||
|
||||
@@ -147,7 +147,10 @@ export class RemoteLink {
|
||||
|
||||
private getLinkableKeys(mod: LoadedLinkModule) {
|
||||
return (
|
||||
mod.__joinerConfig.linkableKeys ?? mod.__joinerConfig.primaryKeys ?? []
|
||||
(mod.__joinerConfig.linkableKeys &&
|
||||
Object.keys(mod.__joinerConfig.linkableKeys)) ||
|
||||
mod.__joinerConfig.primaryKeys ||
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
91
packages/modules-sdk/src/utils/clean-graphql-schema.ts
Normal file
91
packages/modules-sdk/src/utils/clean-graphql-schema.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { Kind, parse, print, visit } from "graphql"
|
||||
|
||||
export function cleanGraphQLSchema(schema: string): {
|
||||
schema: string
|
||||
notFound: Record<string, Record<string, string>>
|
||||
} {
|
||||
const extractTypeNameAndKind = (type) => {
|
||||
if (type.kind === Kind.NAMED_TYPE) {
|
||||
return [type.name.value, type.kind]
|
||||
}
|
||||
if (type.kind === Kind.NON_NULL_TYPE || type.kind === Kind.LIST_TYPE) {
|
||||
return extractTypeNameAndKind(type.type)
|
||||
}
|
||||
return [null, null]
|
||||
}
|
||||
|
||||
const ast = parse(schema)
|
||||
|
||||
const typeNames = new Set(["String", "Int", "Float", "Boolean", "ID"])
|
||||
const extendedTypes = new Set()
|
||||
|
||||
const kinds = [
|
||||
Kind.OBJECT_TYPE_DEFINITION,
|
||||
Kind.INTERFACE_TYPE_DEFINITION,
|
||||
Kind.ENUM_TYPE_DEFINITION,
|
||||
Kind.SCALAR_TYPE_DEFINITION,
|
||||
Kind.INPUT_OBJECT_TYPE_DEFINITION,
|
||||
Kind.UNION_TYPE_DEFINITION,
|
||||
]
|
||||
ast.definitions.forEach((def: any) => {
|
||||
if (kinds.includes(def.kind)) {
|
||||
typeNames.add(def.name.value)
|
||||
} else if (def.kind === Kind.OBJECT_TYPE_EXTENSION) {
|
||||
extendedTypes.add(def.name.value)
|
||||
}
|
||||
})
|
||||
|
||||
const nonExistingMap: Record<string, Record<string, string>> = {}
|
||||
const parentStack: string[] = []
|
||||
|
||||
/*
|
||||
Traverse the graph mapping all the entities + fields and removing the ones that don't exist.
|
||||
Extensions are not removed, but marked with a "__extended" key if the main entity doesn't exist. (example: Link modules injecting fields into another module)
|
||||
*/
|
||||
const cleanedAst = visit(ast, {
|
||||
ObjectTypeExtension: {
|
||||
enter(node) {
|
||||
const typeName = node.name.value
|
||||
|
||||
parentStack.push(typeName)
|
||||
if (!typeNames.has(typeName)) {
|
||||
nonExistingMap[typeName] ??= {}
|
||||
nonExistingMap[typeName]["__extended"] = ""
|
||||
return null
|
||||
}
|
||||
return
|
||||
},
|
||||
leave() {
|
||||
parentStack.pop()
|
||||
},
|
||||
},
|
||||
ObjectTypeDefinition: {
|
||||
enter(node) {
|
||||
parentStack.push(node.name.value)
|
||||
},
|
||||
leave() {
|
||||
parentStack.pop()
|
||||
},
|
||||
},
|
||||
FieldDefinition: {
|
||||
leave(node) {
|
||||
const [typeName, kind] = extractTypeNameAndKind(node.type)
|
||||
|
||||
if (!typeNames.has(typeName) && kind === Kind.NAMED_TYPE) {
|
||||
const currentParent = parentStack[parentStack.length - 1]
|
||||
|
||||
nonExistingMap[currentParent] ??= {}
|
||||
nonExistingMap[currentParent][node.name.value] = typeName
|
||||
return null
|
||||
}
|
||||
return
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Return the schema and the map of non existing entities and fields
|
||||
return {
|
||||
schema: print(cleanedAst),
|
||||
notFound: nonExistingMap,
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from "./clean-graphql-schema"
|
||||
export * from "./get-fields-and-relations"
|
||||
export * from "./graphql-schema-to-fields"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user