chore(): start moving some packages to the core directory (#7215)
This commit is contained in:
committed by
GitHub
parent
fdee748eed
commit
bbccd6481d
@@ -0,0 +1,26 @@
|
||||
export const InventoryModule = {
|
||||
__definition: {
|
||||
key: "inventoryService",
|
||||
registrationName: "inventoryService",
|
||||
defaultPackage: false,
|
||||
label: "InventoryService",
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: [],
|
||||
defaultModuleDeclaration: {
|
||||
scope: "internal",
|
||||
resources: "shared",
|
||||
},
|
||||
},
|
||||
__joinerConfig: {
|
||||
serviceName: "inventoryService",
|
||||
primaryKeys: ["id"],
|
||||
linkableKeys: {
|
||||
inventory_item_id: "InventoryItem",
|
||||
inventory_level_id: "InventoryLevel",
|
||||
reservation_item_id: "ReservationItem",
|
||||
},
|
||||
},
|
||||
|
||||
softDelete: jest.fn(() => {}),
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
export const InventoryStockLocationLink = {
|
||||
__definition: {
|
||||
key: "inventoryStockLocationLink",
|
||||
registrationName: "inventoryStockLocationLink",
|
||||
defaultPackage: "",
|
||||
label: "inventoryStockLocationLink",
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
defaultModuleDeclaration: {
|
||||
scope: "internal",
|
||||
resources: "shared",
|
||||
},
|
||||
},
|
||||
__joinerConfig: {
|
||||
serviceName: "inventoryStockLocationLink",
|
||||
isLink: true,
|
||||
alias: [
|
||||
{
|
||||
name: "inventory_level_stock_location",
|
||||
},
|
||||
{
|
||||
name: "inventory_level_stock_locations",
|
||||
},
|
||||
],
|
||||
primaryKeys: ["inventory_level_id", "stock_location_id"],
|
||||
relationships: [
|
||||
{
|
||||
serviceName: "inventoryService",
|
||||
primaryKey: "id",
|
||||
foreignKey: "inventory_level_id",
|
||||
alias: "inventory_level",
|
||||
args: {},
|
||||
},
|
||||
{
|
||||
serviceName: "stockLocationService",
|
||||
primaryKey: "id",
|
||||
foreignKey: "stock_location_id",
|
||||
alias: "stock_location",
|
||||
},
|
||||
],
|
||||
extends: [
|
||||
{
|
||||
serviceName: "inventoryService",
|
||||
relationship: {
|
||||
serviceName: "inventoryStockLocationLink",
|
||||
primaryKey: "inventory_level_id",
|
||||
foreignKey: "id",
|
||||
alias: "inventory_location_items",
|
||||
},
|
||||
},
|
||||
{
|
||||
serviceName: "stockLocationService",
|
||||
relationship: {
|
||||
serviceName: "inventoryStockLocationLink",
|
||||
primaryKey: "stock_location_id",
|
||||
foreignKey: "id",
|
||||
alias: "inventory_location_items",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
create: jest.fn(
|
||||
async (
|
||||
primaryKeyOrBulkData: string | string[] | [string | string[], string][],
|
||||
foreignKeyData?: string
|
||||
) => {}
|
||||
),
|
||||
softDelete: jest.fn(() => {}),
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
export const ProductInventoryLinkModule = {
|
||||
__definition: {
|
||||
key: "productVariantInventoryInventoryItemLink",
|
||||
registrationName: "productVariantInventoryInventoryItemLink",
|
||||
defaultPackage: "",
|
||||
label: "productVariantInventoryInventoryItemLink",
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
defaultModuleDeclaration: {
|
||||
scope: "internal",
|
||||
resources: "shared",
|
||||
},
|
||||
},
|
||||
__joinerConfig: {
|
||||
serviceName: "productVariantInventoryInventoryItemLink",
|
||||
isLink: true,
|
||||
databaseConfig: {
|
||||
tableName: "product_variant_inventory_item",
|
||||
},
|
||||
alias: [
|
||||
{
|
||||
name: "product_variant_inventory_item",
|
||||
},
|
||||
{
|
||||
name: "product_variant_inventory_items",
|
||||
},
|
||||
],
|
||||
primaryKeys: ["variant_id", "inventory_item_id"],
|
||||
relationships: [
|
||||
{
|
||||
serviceName: "productService",
|
||||
primaryKey: "id",
|
||||
foreignKey: "variant_id",
|
||||
alias: "variant",
|
||||
args: {},
|
||||
deleteCascade: true,
|
||||
},
|
||||
{
|
||||
serviceName: "inventoryService",
|
||||
primaryKey: "id",
|
||||
foreignKey: "inventory_item_id",
|
||||
alias: "inventory",
|
||||
deleteCascade: true,
|
||||
},
|
||||
],
|
||||
extends: [
|
||||
{
|
||||
serviceName: "productService",
|
||||
relationship: {
|
||||
serviceName: "productVariantInventoryInventoryItemLink",
|
||||
primaryKey: "variant_id",
|
||||
foreignKey: "id",
|
||||
alias: "inventory_items",
|
||||
isList: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
serviceName: "inventoryService",
|
||||
relationship: {
|
||||
serviceName: "productVariantInventoryInventoryItemLink",
|
||||
primaryKey: "inventory_item_id",
|
||||
foreignKey: "id",
|
||||
alias: "variant_link",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
create: jest.fn(
|
||||
async (
|
||||
primaryKeyOrBulkData: string | string[] | [string | string[], string][],
|
||||
foreignKeyData?: string
|
||||
) => {}
|
||||
),
|
||||
softDelete: jest.fn(() => {}),
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export const ProductModule = {
|
||||
__definition: {
|
||||
key: "productService",
|
||||
registrationName: "productModuleService",
|
||||
defaultPackage: false,
|
||||
label: "ProductModuleService",
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["eventBusModuleService"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: "internal",
|
||||
resources: "shared",
|
||||
},
|
||||
},
|
||||
__joinerConfig: {
|
||||
serviceName: "productService",
|
||||
primaryKeys: ["id", "handle"],
|
||||
linkableKeys: { product_id: "Product", variant_id: "ProductVariant" },
|
||||
alias: [],
|
||||
},
|
||||
|
||||
softDelete: jest.fn(() => {}),
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export const StockLocationModule = {
|
||||
__definition: {
|
||||
key: "stockLocationService",
|
||||
registrationName: "stockLocationService",
|
||||
defaultPackage: false,
|
||||
label: "StockLocationService",
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["eventBusService"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: "internal",
|
||||
resources: "shared",
|
||||
},
|
||||
},
|
||||
__joinerConfig: {
|
||||
serviceName: "stockLocationService",
|
||||
primaryKeys: ["id"],
|
||||
linkableKeys: { stock_location_id: "StockLocation" },
|
||||
alias: [],
|
||||
},
|
||||
|
||||
softDelete: jest.fn(() => {}),
|
||||
}
|
||||
@@ -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" },
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,202 @@
|
||||
import { InventoryModule } from "../__mocks__/inventory-module"
|
||||
import { InventoryStockLocationLink } from "../__mocks__/inventory-stock-location-link"
|
||||
import { ProductInventoryLinkModule } from "../__mocks__/product-inventory-link"
|
||||
import { ProductModule } from "../__mocks__/product-module"
|
||||
import { StockLocationModule } from "../__mocks__/stock-location-module"
|
||||
|
||||
import { RemoteLink } from "../remote-link"
|
||||
|
||||
const allModules = [
|
||||
// modules
|
||||
ProductModule,
|
||||
InventoryModule,
|
||||
StockLocationModule,
|
||||
// links
|
||||
ProductInventoryLinkModule,
|
||||
InventoryStockLocationLink,
|
||||
]
|
||||
describe("Remote Link", function () {
|
||||
it("Should get all loaded modules and compose their relationships", async function () {
|
||||
const remoteLink = new RemoteLink(allModules as any)
|
||||
|
||||
const relations = remoteLink.getRelationships()
|
||||
|
||||
const prodInventoryLink = relations.get(
|
||||
"productVariantInventoryInventoryItemLink"
|
||||
)
|
||||
const prodModule = relations.get("productService")
|
||||
const inventoryModule = relations.get("inventoryService")
|
||||
|
||||
expect(prodInventoryLink?.get("variant_id")).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
serviceName: "productService",
|
||||
primaryKey: "id",
|
||||
foreignKey: "variant_id",
|
||||
alias: "variant",
|
||||
deleteCascade: true,
|
||||
isPrimary: false,
|
||||
isForeign: true,
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
expect(prodInventoryLink?.get("inventory_item_id")).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
serviceName: "inventoryService",
|
||||
primaryKey: "id",
|
||||
foreignKey: "inventory_item_id",
|
||||
alias: "inventory",
|
||||
deleteCascade: true,
|
||||
isPrimary: false,
|
||||
isForeign: true,
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
expect(prodModule?.get("variant_id")).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
serviceName: "productVariantInventoryInventoryItemLink",
|
||||
primaryKey: "variant_id",
|
||||
foreignKey: "id",
|
||||
alias: "inventory_items",
|
||||
isList: true,
|
||||
isPrimary: true,
|
||||
isForeign: false,
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
expect(inventoryModule?.get("inventory_item_id")).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
serviceName: "productVariantInventoryInventoryItemLink",
|
||||
primaryKey: "inventory_item_id",
|
||||
foreignKey: "id",
|
||||
alias: "variant_link",
|
||||
isPrimary: true,
|
||||
isForeign: false,
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("Should call the correct link module to create relation between 2 keys", async function () {
|
||||
const remoteLink = new RemoteLink(allModules as any)
|
||||
|
||||
await remoteLink.create([
|
||||
{
|
||||
productService: {
|
||||
variant_id: "var_123",
|
||||
},
|
||||
inventoryService: {
|
||||
inventory_item_id: "inv_123",
|
||||
},
|
||||
},
|
||||
{
|
||||
productService: {
|
||||
variant_id: "var_abc",
|
||||
},
|
||||
inventoryService: {
|
||||
inventory_item_id: "inv_abc",
|
||||
},
|
||||
},
|
||||
{
|
||||
inventoryService: {
|
||||
inventory_level_id: "ilev_123",
|
||||
},
|
||||
stockLocationService: {
|
||||
stock_location_id: "loc_123",
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
expect(ProductInventoryLinkModule.create).toBeCalledWith([
|
||||
["var_123", "inv_123"],
|
||||
["var_abc", "inv_abc"],
|
||||
])
|
||||
expect(InventoryStockLocationLink.create).toBeCalledWith([
|
||||
["ilev_123", "loc_123"],
|
||||
])
|
||||
})
|
||||
|
||||
it("Should call delete in cascade all the modules involved in the link", async function () {
|
||||
const remoteLink = new RemoteLink(allModules as any)
|
||||
|
||||
ProductInventoryLinkModule.softDelete.mockImplementation(() => {
|
||||
return {
|
||||
variant_id: ["var_123"],
|
||||
inventory_item_id: ["inv_123"],
|
||||
}
|
||||
})
|
||||
|
||||
ProductModule.softDelete.mockImplementation(() => {
|
||||
return {
|
||||
product_id: ["prod_123", "prod_abc"],
|
||||
variant_id: ["var_123", "var_abc"],
|
||||
}
|
||||
})
|
||||
|
||||
InventoryModule.softDelete.mockImplementation(() => {
|
||||
return {
|
||||
inventory_item_id: ["inv_123"],
|
||||
inventory_level_id: ["ilev_123"],
|
||||
}
|
||||
})
|
||||
|
||||
InventoryStockLocationLink.softDelete.mockImplementation(() => {
|
||||
return {
|
||||
inventory_level_id: ["ilev_123"],
|
||||
stock_location_id: ["loc_123"],
|
||||
}
|
||||
})
|
||||
|
||||
await remoteLink.delete({
|
||||
productService: {
|
||||
variant_id: "var_123",
|
||||
},
|
||||
})
|
||||
|
||||
expect(ProductInventoryLinkModule.softDelete).toBeCalledTimes(2)
|
||||
expect(ProductModule.softDelete).toBeCalledTimes(1)
|
||||
expect(InventoryModule.softDelete).toBeCalledTimes(1)
|
||||
expect(InventoryStockLocationLink.softDelete).toBeCalledTimes(1)
|
||||
|
||||
expect(ProductInventoryLinkModule.softDelete).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
{ variant_id: ["var_123"] },
|
||||
{ returnLinkableKeys: ["variant_id", "inventory_item_id"] }
|
||||
)
|
||||
|
||||
expect(ProductInventoryLinkModule.softDelete).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{ variant_id: ["var_abc"] },
|
||||
{ returnLinkableKeys: ["variant_id", "inventory_item_id"] }
|
||||
)
|
||||
|
||||
expect(ProductModule.softDelete).toBeCalledWith(
|
||||
{ id: ["var_123"] },
|
||||
{ returnLinkableKeys: ["product_id", "variant_id"] }
|
||||
)
|
||||
|
||||
expect(InventoryModule.softDelete).toBeCalledWith(
|
||||
{ id: ["inv_123"] },
|
||||
{
|
||||
returnLinkableKeys: [
|
||||
"inventory_item_id",
|
||||
"inventory_level_id",
|
||||
"reservation_item_id",
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
expect(InventoryStockLocationLink.softDelete).toBeCalledWith(
|
||||
{
|
||||
inventory_level_id: ["ilev_123"],
|
||||
},
|
||||
{ returnLinkableKeys: ["inventory_level_id", "stock_location_id"] }
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,109 @@
|
||||
import { RemoteQuery } from "../remote-query"
|
||||
|
||||
describe("Remote query", () => {
|
||||
it("should properly handle fields and relations transformation", () => {
|
||||
let expand = {
|
||||
fields: ["name", "age"],
|
||||
expands: {
|
||||
friend: {
|
||||
fields: ["name"],
|
||||
expands: {
|
||||
ball: {
|
||||
fields: ["*"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
let result = RemoteQuery.getAllFieldsAndRelations(expand)
|
||||
|
||||
expect(result).toEqual({
|
||||
select: ["name", "age", "friend.name"],
|
||||
relations: ["friend", "friend.ball"],
|
||||
args: {
|
||||
"": undefined,
|
||||
friend: undefined,
|
||||
"friend.ball": undefined,
|
||||
},
|
||||
})
|
||||
|
||||
expand = {
|
||||
fields: [],
|
||||
expands: {
|
||||
friend: {
|
||||
fields: ["name"],
|
||||
expands: {
|
||||
ball: {
|
||||
fields: ["*"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result = RemoteQuery.getAllFieldsAndRelations(expand)
|
||||
|
||||
expect(result).toEqual({
|
||||
select: ["friend.name"],
|
||||
relations: ["friend", "friend.ball"],
|
||||
args: {
|
||||
"": undefined,
|
||||
friend: undefined,
|
||||
"friend.ball": undefined,
|
||||
},
|
||||
})
|
||||
|
||||
expand = {
|
||||
fields: [],
|
||||
expands: {
|
||||
friend: {
|
||||
fields: ["*"],
|
||||
expands: {
|
||||
ball: {
|
||||
fields: ["*"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result = RemoteQuery.getAllFieldsAndRelations(expand)
|
||||
|
||||
expect(result).toEqual({
|
||||
select: [],
|
||||
relations: ["friend", "friend.ball"],
|
||||
args: {
|
||||
"": undefined,
|
||||
friend: undefined,
|
||||
"friend.ball": undefined,
|
||||
},
|
||||
})
|
||||
|
||||
expand = {
|
||||
fields: [],
|
||||
expands: {
|
||||
friend: {
|
||||
fields: [],
|
||||
expands: {
|
||||
ball: {
|
||||
fields: ["*"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result = RemoteQuery.getAllFieldsAndRelations(expand)
|
||||
|
||||
expect(result).toEqual({
|
||||
select: [],
|
||||
relations: ["friend", "friend.ball"],
|
||||
args: {
|
||||
"": undefined,
|
||||
friend: undefined,
|
||||
"friend.ball": undefined,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,110 @@
|
||||
import { mergeTypeDefs } from "@graphql-tools/merge"
|
||||
import { makeExecutableSchema } from "@graphql-tools/schema"
|
||||
import { graphqlSchemaToFields } from "../../utils"
|
||||
|
||||
const userModule = `
|
||||
type User {
|
||||
id: ID!
|
||||
name: String!
|
||||
blabla: WHATEVER
|
||||
}
|
||||
|
||||
type Post {
|
||||
author: User!
|
||||
}
|
||||
`
|
||||
|
||||
const postModule = `
|
||||
type Post {
|
||||
id: ID!
|
||||
title: String!
|
||||
date: String
|
||||
}
|
||||
|
||||
type User {
|
||||
posts: [Post!]!
|
||||
}
|
||||
|
||||
type WHATEVER {
|
||||
random_field: String
|
||||
post: Post
|
||||
}
|
||||
`
|
||||
|
||||
const mergedSchema = mergeTypeDefs([userModule, postModule])
|
||||
const schema = makeExecutableSchema({
|
||||
typeDefs: mergedSchema,
|
||||
})
|
||||
|
||||
const types = schema.getTypeMap()
|
||||
|
||||
describe("graphqlSchemaToFields", function () {
|
||||
it("Should get all fields of a given entity", async function () {
|
||||
const fields = graphqlSchemaToFields(types, "User")
|
||||
expect(fields).toEqual(expect.arrayContaining(["id", "name"]))
|
||||
})
|
||||
|
||||
it("Should get all fields of a given entity and a relation", async function () {
|
||||
const fields = graphqlSchemaToFields(types, "User", ["posts"])
|
||||
expect(fields).toEqual(
|
||||
expect.arrayContaining([
|
||||
"id",
|
||||
"name",
|
||||
"posts.id",
|
||||
"posts.title",
|
||||
"posts.date",
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("Should get all fields of a given entity and many relations", async function () {
|
||||
const fields = graphqlSchemaToFields(types, "User", [
|
||||
"posts",
|
||||
"blabla",
|
||||
"blabla.post",
|
||||
])
|
||||
expect(fields).toEqual(
|
||||
expect.arrayContaining([
|
||||
"id",
|
||||
"name",
|
||||
"posts.id",
|
||||
"posts.title",
|
||||
"posts.date",
|
||||
"blabla.random_field",
|
||||
"blabla.post.id",
|
||||
"blabla.post.title",
|
||||
"blabla.post.date",
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("Should get all fields of a given entity and many relations limited to the relations given", async function () {
|
||||
const fields = graphqlSchemaToFields(types, "User", ["posts", "blabla"])
|
||||
expect(fields).toEqual(
|
||||
expect.arrayContaining([
|
||||
"id",
|
||||
"name",
|
||||
"posts.id",
|
||||
"posts.title",
|
||||
"posts.date",
|
||||
"blabla.random_field",
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("Should get all fields of a given entity and many relations limited to the relations given if they exists", async function () {
|
||||
const fields = graphqlSchemaToFields(types, "User", [
|
||||
"posts",
|
||||
"doNotExists",
|
||||
])
|
||||
expect(fields).toEqual(
|
||||
expect.arrayContaining([
|
||||
"id",
|
||||
"name",
|
||||
"posts.id",
|
||||
"posts.title",
|
||||
"posts.date",
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,386 @@
|
||||
import {
|
||||
MODULE_RESOURCE_TYPE,
|
||||
MODULE_SCOPE,
|
||||
ModuleDefinition,
|
||||
} from "@medusajs/types"
|
||||
|
||||
import { upperCaseFirst } from "@medusajs/utils"
|
||||
|
||||
export enum LinkModuleUtils {
|
||||
REMOTE_QUERY = "remoteQuery",
|
||||
REMOTE_LINK = "remoteLink",
|
||||
}
|
||||
|
||||
// TODO: Remove this enum and use the one from @medusajs/utils
|
||||
export enum Modules {
|
||||
AUTH = "auth",
|
||||
CACHE = "cacheService",
|
||||
CART = "cart",
|
||||
CUSTOMER = "customer",
|
||||
EVENT_BUS = "eventBus",
|
||||
INVENTORY = "inventoryService",
|
||||
LINK = "linkModules",
|
||||
PAYMENT = "payment",
|
||||
PRICING = "pricingService",
|
||||
PRODUCT = "productService",
|
||||
PROMOTION = "promotion",
|
||||
SALES_CHANNEL = "salesChannel",
|
||||
TAX = "tax",
|
||||
FULFILLMENT = "fulfillment",
|
||||
STOCK_LOCATION = "stockLocationService",
|
||||
USER = "user",
|
||||
WORKFLOW_ENGINE = "workflows",
|
||||
REGION = "region",
|
||||
ORDER = "order",
|
||||
API_KEY = "apiKey",
|
||||
STORE = "store",
|
||||
CURRENCY = "currency",
|
||||
FILE = "file",
|
||||
}
|
||||
|
||||
export enum ModuleRegistrationName {
|
||||
AUTH = "authModuleService",
|
||||
CACHE = "cacheService",
|
||||
CART = "cartModuleService",
|
||||
CUSTOMER = "customerModuleService",
|
||||
EVENT_BUS = "eventBusModuleService",
|
||||
INVENTORY = "inventoryService",
|
||||
PAYMENT = "paymentModuleService",
|
||||
PRICING = "pricingModuleService",
|
||||
PRODUCT = "productModuleService",
|
||||
PROMOTION = "promotionModuleService",
|
||||
SALES_CHANNEL = "salesChannelModuleService",
|
||||
FULFILLMENT = "fulfillmentModuleService",
|
||||
STOCK_LOCATION = "stockLocationService",
|
||||
TAX = "taxModuleService",
|
||||
USER = "userModuleService",
|
||||
WORKFLOW_ENGINE = "workflowsModuleService",
|
||||
REGION = "regionModuleService",
|
||||
ORDER = "orderModuleService",
|
||||
API_KEY = "apiKeyModuleService",
|
||||
STORE = "storeModuleService",
|
||||
CURRENCY = "currencyModuleService",
|
||||
FILE = "fileModuleService",
|
||||
}
|
||||
|
||||
export const MODULE_PACKAGE_NAMES = {
|
||||
[Modules.AUTH]: "@medusajs/auth",
|
||||
[Modules.CACHE]: "@medusajs/cache-inmemory",
|
||||
[Modules.CART]: "@medusajs/cart",
|
||||
[Modules.CUSTOMER]: "@medusajs/customer",
|
||||
[Modules.EVENT_BUS]: "@medusajs/event-bus-local",
|
||||
[Modules.INVENTORY]: "@medusajs/inventory-next", // TODO: To be replaced when current `@medusajs/inventory` is deprecated
|
||||
[Modules.LINK]: "@medusajs/link-modules",
|
||||
[Modules.PAYMENT]: "@medusajs/payment",
|
||||
[Modules.PRICING]: "@medusajs/pricing",
|
||||
[Modules.PRODUCT]: "@medusajs/product",
|
||||
[Modules.PROMOTION]: "@medusajs/promotion",
|
||||
[Modules.SALES_CHANNEL]: "@medusajs/sales-channel",
|
||||
[Modules.FULFILLMENT]: "@medusajs/fulfillment",
|
||||
[Modules.STOCK_LOCATION]: "@medusajs/stock-location-next", // TODO: To be replaced when current `@medusajs/stock-location` is deprecated
|
||||
[Modules.TAX]: "@medusajs/tax",
|
||||
[Modules.USER]: "@medusajs/user",
|
||||
[Modules.WORKFLOW_ENGINE]: "@medusajs/workflow-engine-inmemory",
|
||||
[Modules.REGION]: "@medusajs/region",
|
||||
[Modules.ORDER]: "@medusajs/order",
|
||||
[Modules.API_KEY]: "@medusajs/api-key",
|
||||
[Modules.STORE]: "@medusajs/store",
|
||||
[Modules.CURRENCY]: "@medusajs/currency",
|
||||
[Modules.FILE]: "@medusajs/file",
|
||||
}
|
||||
|
||||
export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } =
|
||||
{
|
||||
[Modules.EVENT_BUS]: {
|
||||
key: Modules.EVENT_BUS,
|
||||
isLegacy: true,
|
||||
registrationName: ModuleRegistrationName.EVENT_BUS,
|
||||
defaultPackage: MODULE_PACKAGE_NAMES[Modules.EVENT_BUS],
|
||||
label: upperCaseFirst(ModuleRegistrationName.EVENT_BUS),
|
||||
isRequired: true,
|
||||
dependencies: ["logger"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.STOCK_LOCATION]: {
|
||||
key: Modules.STOCK_LOCATION,
|
||||
isLegacy: true,
|
||||
registrationName: ModuleRegistrationName.STOCK_LOCATION,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.STOCK_LOCATION),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["eventBusService"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.INVENTORY]: {
|
||||
key: Modules.INVENTORY,
|
||||
isLegacy: true,
|
||||
registrationName: ModuleRegistrationName.INVENTORY,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.INVENTORY),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["eventBusService"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.CACHE]: {
|
||||
key: Modules.CACHE,
|
||||
isLegacy: true,
|
||||
registrationName: ModuleRegistrationName.CACHE,
|
||||
defaultPackage: MODULE_PACKAGE_NAMES[Modules.CACHE],
|
||||
label: upperCaseFirst(ModuleRegistrationName.CACHE),
|
||||
isRequired: true,
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.PRODUCT]: {
|
||||
key: Modules.PRODUCT,
|
||||
registrationName: ModuleRegistrationName.PRODUCT,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.PRODUCT),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: [ModuleRegistrationName.EVENT_BUS, "logger"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.PRICING]: {
|
||||
key: Modules.PRICING,
|
||||
registrationName: ModuleRegistrationName.PRICING,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.PRICING),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["logger"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.PROMOTION]: {
|
||||
key: Modules.PROMOTION,
|
||||
registrationName: ModuleRegistrationName.PROMOTION,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.PROMOTION),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["logger"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.AUTH]: {
|
||||
key: Modules.AUTH,
|
||||
registrationName: ModuleRegistrationName.AUTH,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.AUTH),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["logger"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.WORKFLOW_ENGINE]: {
|
||||
key: Modules.WORKFLOW_ENGINE,
|
||||
registrationName: ModuleRegistrationName.WORKFLOW_ENGINE,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.WORKFLOW_ENGINE),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["logger"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.SALES_CHANNEL]: {
|
||||
key: Modules.SALES_CHANNEL,
|
||||
registrationName: ModuleRegistrationName.SALES_CHANNEL,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.SALES_CHANNEL),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["logger"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.FULFILLMENT]: {
|
||||
key: Modules.FULFILLMENT,
|
||||
registrationName: ModuleRegistrationName.FULFILLMENT,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.FULFILLMENT),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["logger", "eventBusService"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.CART]: {
|
||||
key: Modules.CART,
|
||||
registrationName: ModuleRegistrationName.CART,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.CART),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["logger"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.CUSTOMER]: {
|
||||
key: Modules.CUSTOMER,
|
||||
registrationName: ModuleRegistrationName.CUSTOMER,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.CUSTOMER),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["logger"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.PAYMENT]: {
|
||||
key: Modules.PAYMENT,
|
||||
registrationName: ModuleRegistrationName.PAYMENT,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.PAYMENT),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["logger"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.USER]: {
|
||||
key: Modules.USER,
|
||||
registrationName: ModuleRegistrationName.USER,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.USER),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: [ModuleRegistrationName.EVENT_BUS, "logger"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.REGION]: {
|
||||
key: Modules.REGION,
|
||||
registrationName: ModuleRegistrationName.REGION,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.REGION),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["logger"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.ORDER]: {
|
||||
key: Modules.ORDER,
|
||||
registrationName: ModuleRegistrationName.ORDER,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.ORDER),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["logger", "eventBusService"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.TAX]: {
|
||||
key: Modules.TAX,
|
||||
registrationName: ModuleRegistrationName.TAX,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.TAX),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["logger", "eventBusService"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.API_KEY]: {
|
||||
key: Modules.API_KEY,
|
||||
registrationName: ModuleRegistrationName.API_KEY,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.API_KEY),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["logger"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.STORE]: {
|
||||
key: Modules.STORE,
|
||||
registrationName: ModuleRegistrationName.STORE,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.STORE),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["logger"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.CURRENCY]: {
|
||||
key: Modules.CURRENCY,
|
||||
registrationName: ModuleRegistrationName.CURRENCY,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.CURRENCY),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["logger"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
[Modules.FILE]: {
|
||||
key: Modules.FILE,
|
||||
registrationName: ModuleRegistrationName.FILE,
|
||||
defaultPackage: false,
|
||||
label: upperCaseFirst(ModuleRegistrationName.FILE),
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
dependencies: ["logger"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const MODULE_DEFINITIONS: ModuleDefinition[] =
|
||||
Object.values(ModulesDefinition)
|
||||
|
||||
export default MODULE_DEFINITIONS
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from "@medusajs/types/dist/modules-sdk"
|
||||
export * from "./definitions"
|
||||
export * from "./loaders"
|
||||
export * from "./medusa-app"
|
||||
export * from "./medusa-module"
|
||||
export * from "./remote-link"
|
||||
export * from "./remote-query"
|
||||
export * from "./utils/initialize-factory"
|
||||
@@ -0,0 +1,13 @@
|
||||
const loader = ({}) => {
|
||||
throw new Error("loader")
|
||||
}
|
||||
|
||||
const service = class TestService {}
|
||||
const migrations = []
|
||||
const loaders = [loader]
|
||||
|
||||
export default {
|
||||
service,
|
||||
migrations,
|
||||
loaders,
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
const service = class TestService {}
|
||||
const migrations = []
|
||||
const loaders = []
|
||||
const models = []
|
||||
|
||||
export default {
|
||||
service,
|
||||
migrations,
|
||||
loaders,
|
||||
models,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const migrations = []
|
||||
const loaders = []
|
||||
|
||||
export default {
|
||||
migrations,
|
||||
loaders,
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
const service = class TestService {}
|
||||
|
||||
export default {
|
||||
services: [service],
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
const service = class TestService {}
|
||||
|
||||
export const defaultExport = {
|
||||
services: [service],
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
loaders: [],
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export const trackInstallation = jest.fn()
|
||||
export const trackFeatureFlag = jest.fn()
|
||||
@@ -0,0 +1,324 @@
|
||||
import {
|
||||
InternalModuleDeclaration,
|
||||
MODULE_RESOURCE_TYPE,
|
||||
MODULE_SCOPE,
|
||||
} from "@medusajs/types"
|
||||
|
||||
import { MedusaModule } from "../../medusa-module"
|
||||
import { asValue } from "awilix"
|
||||
|
||||
const mockRegisterMedusaModule = jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
moduleKey: {
|
||||
definition: {
|
||||
key: "moduleKey",
|
||||
registrationName: "moduleKey",
|
||||
},
|
||||
moduleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const mockModuleLoader = jest.fn().mockImplementation(({ container }) => {
|
||||
container.register({
|
||||
moduleKey: asValue({}),
|
||||
})
|
||||
return Promise.resolve({})
|
||||
})
|
||||
|
||||
jest.mock("./../../loaders", () => ({
|
||||
registerMedusaModule: jest
|
||||
.fn()
|
||||
.mockImplementation((...args) => mockRegisterMedusaModule()),
|
||||
moduleLoader: jest
|
||||
.fn()
|
||||
.mockImplementation((...args) => mockModuleLoader.apply(this, args)),
|
||||
}))
|
||||
|
||||
describe("Medusa Modules", () => {
|
||||
beforeEach(() => {
|
||||
MedusaModule.clearInstances()
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should create singleton instances", async () => {
|
||||
await MedusaModule.bootstrap({
|
||||
moduleKey: "moduleKey",
|
||||
defaultPath: "@path",
|
||||
declaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: "@path",
|
||||
options: {
|
||||
abc: 123,
|
||||
},
|
||||
} as InternalModuleDeclaration,
|
||||
})
|
||||
|
||||
expect(mockRegisterMedusaModule).toBeCalledTimes(1)
|
||||
expect(mockModuleLoader).toBeCalledTimes(1)
|
||||
|
||||
await MedusaModule.bootstrap({
|
||||
moduleKey: "moduleKey",
|
||||
defaultPath: "@path",
|
||||
declaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: "@path",
|
||||
options: {
|
||||
abc: 123,
|
||||
},
|
||||
} as InternalModuleDeclaration,
|
||||
})
|
||||
|
||||
await MedusaModule.bootstrap({
|
||||
moduleKey: "moduleKey",
|
||||
defaultPath: "@path",
|
||||
declaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: "@path",
|
||||
options: {
|
||||
different_options: "abc",
|
||||
},
|
||||
} as InternalModuleDeclaration,
|
||||
})
|
||||
|
||||
expect(mockRegisterMedusaModule).toBeCalledTimes(2)
|
||||
expect(mockModuleLoader).toBeCalledTimes(2)
|
||||
})
|
||||
|
||||
it("should prevent the module being loaded multiple times under concurrent requests", async () => {
|
||||
const load: any = []
|
||||
|
||||
for (let i = 5; i--; ) {
|
||||
load.push(
|
||||
MedusaModule.bootstrap({
|
||||
moduleKey: "moduleKey",
|
||||
defaultPath: "@path",
|
||||
declaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: "@path",
|
||||
options: {
|
||||
abc: 123,
|
||||
},
|
||||
} as InternalModuleDeclaration,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const intances = Promise.all(load)
|
||||
|
||||
expect(mockRegisterMedusaModule).toBeCalledTimes(1)
|
||||
expect(mockModuleLoader).toBeCalledTimes(1)
|
||||
expect(intances[(await intances).length - 1]).toBe(intances[0])
|
||||
})
|
||||
|
||||
it("getModuleInstance should return the first instance of the module if there is none flagged as 'main'", async () => {
|
||||
const moduleA = await MedusaModule.bootstrap({
|
||||
moduleKey: "moduleKey",
|
||||
defaultPath: "@path",
|
||||
declaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: "@path",
|
||||
options: {
|
||||
abc: 123,
|
||||
},
|
||||
} as InternalModuleDeclaration,
|
||||
})
|
||||
|
||||
const moduleB = await MedusaModule.bootstrap({
|
||||
moduleKey: "moduleKey",
|
||||
defaultPath: "@path",
|
||||
declaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: "@path",
|
||||
options: {
|
||||
different_options: "abc",
|
||||
},
|
||||
} as InternalModuleDeclaration,
|
||||
})
|
||||
|
||||
expect(MedusaModule.getModuleInstance("moduleKey")).toEqual(moduleA)
|
||||
})
|
||||
|
||||
it("should return the module flagged as 'main' when multiple instances are available", async () => {
|
||||
const moduleA = await MedusaModule.bootstrap({
|
||||
moduleKey: "moduleKey",
|
||||
defaultPath: "@path",
|
||||
declaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: "@path",
|
||||
options: {
|
||||
abc: 123,
|
||||
},
|
||||
} as InternalModuleDeclaration,
|
||||
})
|
||||
|
||||
const moduleB = await MedusaModule.bootstrap({
|
||||
moduleKey: "moduleKey",
|
||||
defaultPath: "@path",
|
||||
declaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: "@path",
|
||||
main: true,
|
||||
options: {
|
||||
different_options: "abc",
|
||||
},
|
||||
} as InternalModuleDeclaration,
|
||||
})
|
||||
|
||||
expect(MedusaModule.getModuleInstance("moduleKey")).toEqual(moduleB)
|
||||
})
|
||||
|
||||
it("should retrieve the module by their given alias", async () => {
|
||||
const moduleA = await MedusaModule.bootstrap({
|
||||
moduleKey: "moduleKey",
|
||||
defaultPath: "@path",
|
||||
declaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: "@path",
|
||||
alias: "mod_A",
|
||||
options: {
|
||||
abc: 123,
|
||||
},
|
||||
} as InternalModuleDeclaration,
|
||||
})
|
||||
|
||||
const moduleB = await MedusaModule.bootstrap({
|
||||
moduleKey: "moduleKey",
|
||||
defaultPath: "@path",
|
||||
declaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: "@path",
|
||||
main: true,
|
||||
alias: "mod_B",
|
||||
options: {
|
||||
different_options: "abc",
|
||||
},
|
||||
} as InternalModuleDeclaration,
|
||||
})
|
||||
|
||||
const moduleC = await MedusaModule.bootstrap({
|
||||
moduleKey: "moduleKey",
|
||||
defaultPath: "@path",
|
||||
declaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: "@path",
|
||||
alias: "mod_C",
|
||||
options: {
|
||||
moduleC: true,
|
||||
},
|
||||
} as InternalModuleDeclaration,
|
||||
})
|
||||
|
||||
// main
|
||||
expect(MedusaModule.getModuleInstance("moduleKey")).toEqual(moduleB)
|
||||
|
||||
expect(MedusaModule.getModuleInstance("moduleKey", "mod_A")).toEqual(
|
||||
moduleA
|
||||
)
|
||||
expect(MedusaModule.getModuleInstance("moduleKey", "mod_B")).toEqual(
|
||||
moduleB
|
||||
)
|
||||
expect(MedusaModule.getModuleInstance("moduleKey", "mod_C")).toEqual(
|
||||
moduleC
|
||||
)
|
||||
})
|
||||
|
||||
it("should prevent two main modules being set as 'main'", async () => {
|
||||
await MedusaModule.bootstrap({
|
||||
moduleKey: "moduleKey",
|
||||
defaultPath: "@path",
|
||||
declaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: "@path",
|
||||
alias: "mod_A",
|
||||
options: {
|
||||
abc: 123,
|
||||
},
|
||||
} as InternalModuleDeclaration,
|
||||
})
|
||||
|
||||
await MedusaModule.bootstrap({
|
||||
moduleKey: "moduleKey",
|
||||
defaultPath: "@path",
|
||||
declaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: "@path",
|
||||
main: true,
|
||||
alias: "mod_B",
|
||||
options: {
|
||||
different_options: "abc",
|
||||
},
|
||||
} as InternalModuleDeclaration,
|
||||
})
|
||||
|
||||
const moduleC = MedusaModule.bootstrap({
|
||||
moduleKey: "moduleKey",
|
||||
defaultPath: "@path",
|
||||
declaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: "@path",
|
||||
main: true,
|
||||
alias: "mod_C",
|
||||
options: {
|
||||
moduleC: true,
|
||||
},
|
||||
} as InternalModuleDeclaration,
|
||||
})
|
||||
|
||||
expect(moduleC).rejects.toThrow(
|
||||
"Module moduleKey already have a 'main' registered."
|
||||
)
|
||||
})
|
||||
|
||||
it("should prevent the same alias be used for different instances of the same module", async () => {
|
||||
await MedusaModule.bootstrap({
|
||||
moduleKey: "moduleKey",
|
||||
defaultPath: "@path",
|
||||
declaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: "@path",
|
||||
alias: "module_alias",
|
||||
options: {
|
||||
different_options: "abc",
|
||||
},
|
||||
} as InternalModuleDeclaration,
|
||||
})
|
||||
|
||||
const moduleC = MedusaModule.bootstrap({
|
||||
moduleKey: "moduleKey",
|
||||
defaultPath: "@path",
|
||||
declaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: "@path",
|
||||
alias: "module_alias",
|
||||
options: {
|
||||
moduleC: true,
|
||||
},
|
||||
} as InternalModuleDeclaration,
|
||||
})
|
||||
|
||||
expect(moduleC).rejects.toThrow(
|
||||
"Module moduleKey already registed as 'module_alias'. Please choose a different alias."
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,279 @@
|
||||
import {
|
||||
MODULE_RESOURCE_TYPE,
|
||||
MODULE_SCOPE,
|
||||
ModuleResolution,
|
||||
} from "@medusajs/types"
|
||||
import { createMedusaContainer } from "@medusajs/utils"
|
||||
import { EOL } from "os"
|
||||
import { moduleLoader } from "../module-loader"
|
||||
|
||||
const logger = {
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
} as any
|
||||
|
||||
describe("modules loader", () => {
|
||||
let container
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
container = createMedusaContainer()
|
||||
})
|
||||
|
||||
it("should register the service as undefined in the container when no resolution path is given", async () => {
|
||||
const moduleResolutions: Record<string, ModuleResolution> = {
|
||||
testService: {
|
||||
resolutionPath: false,
|
||||
definition: {
|
||||
registrationName: "testService",
|
||||
key: "testService",
|
||||
defaultPackage: "testService",
|
||||
label: "TestService",
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
moduleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
await moduleLoader({ container, moduleResolutions, logger })
|
||||
|
||||
const testService = container.resolve(
|
||||
moduleResolutions.testService.definition.key
|
||||
)
|
||||
expect(testService).toBe(undefined)
|
||||
})
|
||||
|
||||
it("should register the service ", async () => {
|
||||
const moduleResolutions: Record<string, ModuleResolution> = {
|
||||
testService: {
|
||||
resolutionPath: "@modules/default",
|
||||
definition: {
|
||||
registrationName: "testService",
|
||||
key: "testService",
|
||||
defaultPackage: "testService",
|
||||
label: "TestService",
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
moduleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
await moduleLoader({ container, moduleResolutions, logger })
|
||||
|
||||
const testService = container.resolve(
|
||||
moduleResolutions.testService.definition.key,
|
||||
{}
|
||||
)
|
||||
|
||||
/*
|
||||
expect(trackInstallation).toHaveBeenCalledWith(
|
||||
{
|
||||
module: moduleResolutions.testService.definition.key,
|
||||
resolution: moduleResolutions.testService.resolutionPath,
|
||||
},
|
||||
"module"
|
||||
)
|
||||
*/
|
||||
expect(testService).toBeTruthy()
|
||||
expect(typeof testService).toEqual("object")
|
||||
})
|
||||
|
||||
it("should run the defined loaders and logs the errors if something fails", async () => {
|
||||
const moduleResolutions: Record<string, ModuleResolution> = {
|
||||
testService: {
|
||||
resolutionPath: "@modules/brokenloader",
|
||||
definition: {
|
||||
registrationName: "testService",
|
||||
key: "testService",
|
||||
defaultPackage: "testService",
|
||||
label: "TestService",
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
moduleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
await moduleLoader({ container, moduleResolutions, logger })
|
||||
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
`Could not resolve module: TestService. Error: Loaders for module TestService failed: loader${EOL}`
|
||||
)
|
||||
})
|
||||
|
||||
it("should log the errors if no service is defined", async () => {
|
||||
const moduleResolutions: Record<string, ModuleResolution> = {
|
||||
testService: {
|
||||
resolutionPath: "@modules/no-service",
|
||||
definition: {
|
||||
registrationName: "testService",
|
||||
key: "testService",
|
||||
defaultPackage: "testService",
|
||||
label: "TestService",
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
moduleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
await moduleLoader({ container, moduleResolutions, logger })
|
||||
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
`Could not resolve module: TestService. Error: No service found in module. Make sure your module exports a service.${EOL}`
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error if no service is defined and the module is required", async () => {
|
||||
expect.assertions(1)
|
||||
const moduleResolutions: Record<string, ModuleResolution> = {
|
||||
testService: {
|
||||
resolutionPath: "@modules/no-service",
|
||||
definition: {
|
||||
registrationName: "testService",
|
||||
key: "testService",
|
||||
defaultPackage: "testService",
|
||||
label: "TestService",
|
||||
isRequired: true,
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
moduleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
try {
|
||||
await moduleLoader({ container, moduleResolutions, logger })
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
"No service found in module. Make sure your module exports a service."
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw an error if the default package isn't found and the module is required", async () => {
|
||||
expect.assertions(1)
|
||||
const moduleResolutions: Record<string, ModuleResolution> = {
|
||||
testService: {
|
||||
resolutionPath: "@medusajs/testService",
|
||||
definition: {
|
||||
registrationName: "testService",
|
||||
key: "testService",
|
||||
defaultPackage: "@medusajs/testService",
|
||||
label: "TestService",
|
||||
isRequired: true,
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
moduleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
try {
|
||||
await moduleLoader({ container, moduleResolutions, logger })
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
`Make sure you have installed the default package: @medusajs/testService`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw an error if no scope is defined on the module declaration", async () => {
|
||||
expect.assertions(1)
|
||||
const moduleResolutions: Record<string, ModuleResolution> = {
|
||||
testService: {
|
||||
resolutionPath: "@modules/no-service",
|
||||
definition: {
|
||||
registrationName: "testService",
|
||||
key: "testService",
|
||||
defaultPackage: "testService",
|
||||
label: "TestService",
|
||||
isRequired: true,
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
// @ts-ignore
|
||||
moduleDeclaration: {
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
try {
|
||||
await moduleLoader({ container, moduleResolutions, logger })
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
"The module TestService has to define its scope (internal | external)"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw an error if the resources is not set when scope is defined as internal", async () => {
|
||||
expect.assertions(1)
|
||||
const moduleResolutions: Record<string, ModuleResolution> = {
|
||||
testService: {
|
||||
resolutionPath: "@modules/no-service",
|
||||
definition: {
|
||||
registrationName: "testService",
|
||||
key: "testService",
|
||||
defaultPackage: "testService",
|
||||
label: "TestService",
|
||||
isRequired: true,
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
},
|
||||
// @ts-ignore
|
||||
moduleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
},
|
||||
} as any,
|
||||
}
|
||||
|
||||
try {
|
||||
await moduleLoader({ container, moduleResolutions, logger })
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
"The module TestService is missing its resources config"
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,98 @@
|
||||
import { createMedusaContainer } from "@medusajs/utils"
|
||||
import { Lifetime, asFunction } from "awilix"
|
||||
import { moduleProviderLoader } from "../module-provider-loader"
|
||||
|
||||
const logger = {
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
} as any
|
||||
|
||||
describe("modules loader", () => {
|
||||
let container
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
container = createMedusaContainer()
|
||||
})
|
||||
|
||||
it("should register the provider service", async () => {
|
||||
const moduleProviders = [
|
||||
{
|
||||
resolve: "@plugins/default",
|
||||
options: {},
|
||||
},
|
||||
]
|
||||
|
||||
await moduleProviderLoader({ container, providers: moduleProviders })
|
||||
|
||||
const testService = container.resolve("testService")
|
||||
expect(testService).toBeTruthy()
|
||||
expect(testService.constructor.name).toEqual("TestService")
|
||||
})
|
||||
|
||||
it("should register the provider service with custom register fn", async () => {
|
||||
const fn = async (klass, container, details) => {
|
||||
container.register({
|
||||
[`testServiceCustomRegistration`]: asFunction(
|
||||
(cradle) => new klass(cradle, details.options),
|
||||
{
|
||||
lifetime: Lifetime.SINGLETON,
|
||||
}
|
||||
),
|
||||
})
|
||||
}
|
||||
const moduleProviders = [
|
||||
{
|
||||
resolve: "@plugins/default",
|
||||
options: {},
|
||||
},
|
||||
]
|
||||
|
||||
await moduleProviderLoader({
|
||||
container,
|
||||
providers: moduleProviders,
|
||||
registerServiceFn: fn,
|
||||
})
|
||||
|
||||
const testService = container.resolve("testServiceCustomRegistration")
|
||||
expect(testService).toBeTruthy()
|
||||
expect(testService.constructor.name).toEqual("TestService")
|
||||
})
|
||||
|
||||
it("should log the errors if no service is defined", async () => {
|
||||
const moduleProviders = [
|
||||
{
|
||||
resolve: "@plugins/no-service",
|
||||
options: {},
|
||||
},
|
||||
]
|
||||
|
||||
try {
|
||||
await moduleProviderLoader({ container, providers: moduleProviders })
|
||||
} catch (error) {
|
||||
expect(error.message).toBe(
|
||||
"No services found in plugin @plugins/no-service -- make sure your plugin has a default export of services."
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw if no default export is defined", async () => {
|
||||
const moduleProviders = [
|
||||
{
|
||||
resolve: "@plugins/no-default",
|
||||
options: {},
|
||||
},
|
||||
]
|
||||
|
||||
try {
|
||||
await moduleProviderLoader({ container, providers: moduleProviders })
|
||||
} catch (error) {
|
||||
expect(error.message).toBe(
|
||||
"No services found in plugin @plugins/no-default -- make sure your plugin has a default export of services."
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,244 @@
|
||||
import {
|
||||
InternalModuleDeclaration,
|
||||
MODULE_RESOURCE_TYPE,
|
||||
MODULE_SCOPE,
|
||||
ModuleDefinition,
|
||||
} from "@medusajs/types"
|
||||
import { ModulesDefinition } from "../../definitions"
|
||||
import { registerMedusaModule } from "../register-modules"
|
||||
|
||||
const RESOLVED_PACKAGE = "@medusajs/test-service-resolved"
|
||||
jest.mock("resolve-cwd", () => jest.fn(() => RESOLVED_PACKAGE))
|
||||
|
||||
describe("module definitions loader", () => {
|
||||
const defaultDefinition: ModuleDefinition = {
|
||||
key: "testService",
|
||||
registrationName: "testService",
|
||||
defaultPackage: "@medusajs/test-service",
|
||||
label: "TestService",
|
||||
isLegacy: true,
|
||||
isRequired: false,
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
|
||||
// Clear module definitions
|
||||
const allProperties = Object.getOwnPropertyNames(ModulesDefinition)
|
||||
allProperties.forEach((property) => {
|
||||
delete ModulesDefinition[property]
|
||||
})
|
||||
})
|
||||
|
||||
it("Resolves module with default definition given empty config", () => {
|
||||
Object.assign(ModulesDefinition, {
|
||||
[defaultDefinition.key]: defaultDefinition,
|
||||
})
|
||||
|
||||
const res = registerMedusaModule(defaultDefinition.key)
|
||||
|
||||
expect(res[defaultDefinition.key]).toEqual(
|
||||
expect.objectContaining({
|
||||
resolutionPath: defaultDefinition.defaultPackage,
|
||||
definition: defaultDefinition,
|
||||
options: {},
|
||||
moduleDeclaration: {
|
||||
scope: "internal",
|
||||
resources: "shared",
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("Resolves a custom module without pre-defined definition", () => {
|
||||
const res = registerMedusaModule("customModulesABC", {
|
||||
options: {
|
||||
test: 123,
|
||||
},
|
||||
})
|
||||
|
||||
expect(res).toEqual({
|
||||
customModulesABC: expect.objectContaining({
|
||||
resolutionPath: "@medusajs/test-service-resolved",
|
||||
definition: expect.objectContaining({
|
||||
key: "customModulesABC",
|
||||
label: "Custom: customModulesABC",
|
||||
registrationName: "customModulesABC",
|
||||
}),
|
||||
moduleDeclaration: {
|
||||
resources: "shared",
|
||||
scope: "internal",
|
||||
},
|
||||
options: {
|
||||
test: 123,
|
||||
},
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
describe("boolean config", () => {
|
||||
it("Resolves module with no resolution path when given false", () => {
|
||||
Object.assign(ModulesDefinition, {
|
||||
[defaultDefinition.key]: defaultDefinition,
|
||||
})
|
||||
|
||||
const res = registerMedusaModule(defaultDefinition.key, false)
|
||||
|
||||
expect(res[defaultDefinition.key]).toEqual(
|
||||
expect.objectContaining({
|
||||
resolutionPath: false,
|
||||
definition: defaultDefinition,
|
||||
options: {},
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("Fails to resolve module with no resolution path when given false for a required module", () => {
|
||||
expect.assertions(1)
|
||||
Object.assign(ModulesDefinition, {
|
||||
[defaultDefinition.key]: { ...defaultDefinition, isRequired: true },
|
||||
})
|
||||
|
||||
try {
|
||||
registerMedusaModule(defaultDefinition.key, false)
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
`Module: ${defaultDefinition.label} is required`
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it("Module with no resolution path when not given custom resolution path, false as default package and required", () => {
|
||||
const definition = {
|
||||
...defaultDefinition,
|
||||
defaultPackage: false as false,
|
||||
isRequired: true,
|
||||
}
|
||||
|
||||
Object.assign(ModulesDefinition, {
|
||||
[defaultDefinition.key]: definition,
|
||||
})
|
||||
|
||||
const res = registerMedusaModule(defaultDefinition.key)
|
||||
|
||||
expect(res[defaultDefinition.key]).toEqual(
|
||||
expect.objectContaining({
|
||||
resolutionPath: false,
|
||||
definition: definition,
|
||||
options: {},
|
||||
moduleDeclaration: {
|
||||
scope: "internal",
|
||||
resources: "shared",
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe("string config", () => {
|
||||
it("Resolves module with default definition given empty config", () => {
|
||||
Object.assign(ModulesDefinition, {
|
||||
[defaultDefinition.key]: defaultDefinition,
|
||||
})
|
||||
|
||||
const res = registerMedusaModule(
|
||||
defaultDefinition.key,
|
||||
defaultDefinition.defaultPackage
|
||||
)
|
||||
|
||||
expect(res[defaultDefinition.key]).toEqual(
|
||||
expect.objectContaining({
|
||||
resolutionPath: RESOLVED_PACKAGE,
|
||||
definition: defaultDefinition,
|
||||
options: {},
|
||||
moduleDeclaration: {
|
||||
scope: "internal",
|
||||
resources: "shared",
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("object config", () => {
|
||||
it("Resolves resolution path and provides empty options when none are provided", () => {
|
||||
Object.assign(ModulesDefinition, {
|
||||
[defaultDefinition.key]: defaultDefinition,
|
||||
})
|
||||
|
||||
const res = registerMedusaModule(defaultDefinition.key, {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resolve: defaultDefinition.defaultPackage,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
} as InternalModuleDeclaration)
|
||||
|
||||
expect(res[defaultDefinition.key]).toEqual(
|
||||
expect.objectContaining({
|
||||
resolutionPath: RESOLVED_PACKAGE,
|
||||
definition: defaultDefinition,
|
||||
options: {},
|
||||
moduleDeclaration: {
|
||||
scope: "internal",
|
||||
resources: "isolated",
|
||||
resolve: defaultDefinition.defaultPackage,
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("Resolves default resolution path and provides options when only options are provided", () => {
|
||||
Object.assign(ModulesDefinition, {
|
||||
[defaultDefinition.key]: defaultDefinition,
|
||||
})
|
||||
|
||||
const res = registerMedusaModule(defaultDefinition.key, {
|
||||
options: { test: 123 },
|
||||
} as any)
|
||||
|
||||
expect(res[defaultDefinition.key]).toEqual(
|
||||
expect.objectContaining({
|
||||
resolutionPath: defaultDefinition.defaultPackage,
|
||||
definition: defaultDefinition,
|
||||
options: { test: 123 },
|
||||
moduleDeclaration: {
|
||||
scope: "internal",
|
||||
resources: "shared",
|
||||
options: { test: 123 },
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("Resolves resolution path and provides options when only options are provided", () => {
|
||||
Object.assign(ModulesDefinition, {
|
||||
[defaultDefinition.key]: defaultDefinition,
|
||||
})
|
||||
|
||||
const res = registerMedusaModule(defaultDefinition.key, {
|
||||
resolve: defaultDefinition.defaultPackage,
|
||||
options: { test: 123 },
|
||||
scope: "internal",
|
||||
resources: "isolated",
|
||||
} as any)
|
||||
|
||||
expect(res[defaultDefinition.key]).toEqual(
|
||||
expect.objectContaining({
|
||||
resolutionPath: RESOLVED_PACKAGE,
|
||||
definition: defaultDefinition,
|
||||
options: { test: 123 },
|
||||
moduleDeclaration: {
|
||||
scope: "internal",
|
||||
resources: "isolated",
|
||||
resolve: defaultDefinition.defaultPackage,
|
||||
options: { test: 123 },
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./module-loader"
|
||||
export * from "./module-provider-loader"
|
||||
export * from "./register-modules"
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
Logger,
|
||||
MedusaContainer,
|
||||
MODULE_SCOPE,
|
||||
ModuleResolution,
|
||||
} from "@medusajs/types"
|
||||
|
||||
import { asValue } from "awilix"
|
||||
import { EOL } from "os"
|
||||
import { loadInternalModule } from "./utils"
|
||||
|
||||
export const moduleLoader = async ({
|
||||
container,
|
||||
moduleResolutions,
|
||||
logger,
|
||||
migrationOnly,
|
||||
loaderOnly,
|
||||
}: {
|
||||
container: MedusaContainer
|
||||
moduleResolutions: Record<string, ModuleResolution>
|
||||
logger: Logger
|
||||
migrationOnly?: boolean
|
||||
loaderOnly?: boolean
|
||||
}): Promise<void> => {
|
||||
for (const resolution of Object.values(moduleResolutions ?? {})) {
|
||||
const registrationResult = await loadModule(
|
||||
container,
|
||||
resolution,
|
||||
logger!,
|
||||
migrationOnly,
|
||||
loaderOnly
|
||||
)
|
||||
|
||||
if (registrationResult?.error) {
|
||||
const { error } = registrationResult
|
||||
if (resolution.definition.isRequired) {
|
||||
logger?.error(
|
||||
`Could not resolve required module: ${resolution.definition.label}. Error: ${error.message}${EOL}`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
|
||||
logger?.warn(
|
||||
`Could not resolve module: ${resolution.definition.label}. Error: ${error.message}${EOL}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadModule(
|
||||
container: MedusaContainer,
|
||||
resolution: ModuleResolution,
|
||||
logger: Logger,
|
||||
migrationOnly?: boolean,
|
||||
loaderOnly?: boolean
|
||||
): Promise<{ error?: Error } | void> {
|
||||
const modDefinition = resolution.definition
|
||||
const registrationName = modDefinition.registrationName
|
||||
|
||||
const { scope, resources } = resolution.moduleDeclaration ?? ({} as any)
|
||||
|
||||
const canSkip =
|
||||
!resolution.resolutionPath &&
|
||||
!modDefinition.isRequired &&
|
||||
!modDefinition.defaultPackage
|
||||
|
||||
if (scope === MODULE_SCOPE.EXTERNAL && !canSkip) {
|
||||
// TODO: implement external Resolvers
|
||||
// return loadExternalModule(...)
|
||||
throw new Error("External Modules are not supported yet.")
|
||||
}
|
||||
|
||||
if (!scope || (scope === MODULE_SCOPE.INTERNAL && !resources)) {
|
||||
let message = `The module ${resolution.definition.label} has to define its scope (internal | external)`
|
||||
if (scope === MODULE_SCOPE.INTERNAL && !resources) {
|
||||
message = `The module ${resolution.definition.label} is missing its resources config`
|
||||
}
|
||||
|
||||
container.register(registrationName, asValue(undefined))
|
||||
|
||||
return {
|
||||
error: new Error(message),
|
||||
}
|
||||
}
|
||||
|
||||
if (resolution.resolutionPath === false) {
|
||||
container.register(registrationName, asValue(undefined))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return await loadInternalModule(
|
||||
container,
|
||||
resolution,
|
||||
logger,
|
||||
migrationOnly,
|
||||
loaderOnly
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { MedusaContainer, ModuleProvider } from "@medusajs/types"
|
||||
import { isString, lowerCaseFirst, promiseAll } from "@medusajs/utils"
|
||||
import { Lifetime, asFunction } from "awilix"
|
||||
|
||||
export async function moduleProviderLoader({
|
||||
container,
|
||||
providers,
|
||||
registerServiceFn,
|
||||
}: {
|
||||
container: MedusaContainer
|
||||
providers: ModuleProvider[]
|
||||
registerServiceFn?: (
|
||||
klass,
|
||||
container: MedusaContainer,
|
||||
pluginDetails: any
|
||||
) => Promise<void>
|
||||
}) {
|
||||
if (!providers?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
await promiseAll(
|
||||
providers.map(async (pluginDetails) => {
|
||||
await loadModuleProvider(container, pluginDetails, registerServiceFn)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export async function loadModuleProvider(
|
||||
container: MedusaContainer,
|
||||
provider: ModuleProvider,
|
||||
registerServiceFn?: (klass, container, pluginDetails) => Promise<void>
|
||||
) {
|
||||
let loadedProvider: any
|
||||
|
||||
const pluginName = provider.resolve ?? provider.provider_name ?? ""
|
||||
|
||||
try {
|
||||
loadedProvider = provider.resolve
|
||||
|
||||
if (isString(provider.resolve)) {
|
||||
loadedProvider = await import(provider.resolve)
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Unable to find plugin ${pluginName} -- perhaps you need to install its package?`
|
||||
)
|
||||
}
|
||||
|
||||
loadedProvider = (loadedProvider as any).default ?? loadedProvider
|
||||
|
||||
if (!loadedProvider?.services?.length) {
|
||||
throw new Error(
|
||||
`No services found in plugin ${provider.resolve} -- make sure your plugin has a default export of services.`
|
||||
)
|
||||
}
|
||||
|
||||
const services = await promiseAll(
|
||||
loadedProvider.services.map(async (service) => {
|
||||
const name = lowerCaseFirst(service.name)
|
||||
if (registerServiceFn) {
|
||||
// Used to register the specific type of service in the provider
|
||||
await registerServiceFn(service, container, provider.options)
|
||||
} else {
|
||||
container.register({
|
||||
[name]: asFunction(
|
||||
(cradle) => new service(cradle, provider.options),
|
||||
{
|
||||
lifetime: service.LIFE_TIME || Lifetime.SCOPED,
|
||||
}
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return service
|
||||
})
|
||||
)
|
||||
|
||||
return services
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import {
|
||||
ExternalModuleDeclaration,
|
||||
InternalModuleDeclaration,
|
||||
MODULE_RESOURCE_TYPE,
|
||||
MODULE_SCOPE,
|
||||
ModuleDefinition,
|
||||
ModuleExports,
|
||||
ModuleResolution,
|
||||
} from "@medusajs/types"
|
||||
|
||||
import { isObject, isString } from "@medusajs/utils"
|
||||
import resolveCwd from "resolve-cwd"
|
||||
import { ModulesDefinition } from "../definitions"
|
||||
|
||||
export const registerMedusaModule = (
|
||||
moduleKey: string,
|
||||
moduleDeclaration?:
|
||||
| Partial<InternalModuleDeclaration | ExternalModuleDeclaration>
|
||||
| string
|
||||
| false,
|
||||
moduleExports?: ModuleExports,
|
||||
definition?: ModuleDefinition
|
||||
): Record<string, ModuleResolution> => {
|
||||
const moduleResolutions = {} as Record<string, ModuleResolution>
|
||||
|
||||
const modDefinition = definition ?? ModulesDefinition[moduleKey]
|
||||
|
||||
const modDeclaration =
|
||||
moduleDeclaration ??
|
||||
(modDefinition?.defaultModuleDeclaration as InternalModuleDeclaration)
|
||||
|
||||
if (modDeclaration !== false && !modDeclaration) {
|
||||
throw new Error(`Module: ${moduleKey} has no declaration.`)
|
||||
}
|
||||
|
||||
if (
|
||||
isObject(modDeclaration) &&
|
||||
modDeclaration?.scope === MODULE_SCOPE.EXTERNAL
|
||||
) {
|
||||
// TODO: getExternalModuleResolution(...)
|
||||
throw new Error("External Modules are not supported yet.")
|
||||
}
|
||||
|
||||
if (modDefinition === undefined) {
|
||||
moduleResolutions[moduleKey] = getCustomModuleResolution(
|
||||
moduleKey,
|
||||
moduleDeclaration as InternalModuleDeclaration
|
||||
)
|
||||
return moduleResolutions
|
||||
}
|
||||
|
||||
moduleResolutions[moduleKey] = getInternalModuleResolution(
|
||||
modDefinition,
|
||||
moduleDeclaration as InternalModuleDeclaration,
|
||||
moduleExports
|
||||
)
|
||||
|
||||
return moduleResolutions
|
||||
}
|
||||
|
||||
function getCustomModuleResolution(
|
||||
key: string,
|
||||
moduleConfig: InternalModuleDeclaration | string
|
||||
): ModuleResolution {
|
||||
const resolutionPath = resolveCwd(
|
||||
isString(moduleConfig) ? moduleConfig : (moduleConfig.resolve as string)
|
||||
)
|
||||
|
||||
const conf = isObject(moduleConfig)
|
||||
? moduleConfig
|
||||
: ({} as InternalModuleDeclaration)
|
||||
|
||||
const dependencies = conf?.dependencies ?? []
|
||||
|
||||
return {
|
||||
resolutionPath,
|
||||
definition: {
|
||||
key,
|
||||
label: `Custom: ${key}`,
|
||||
isRequired: false,
|
||||
defaultPackage: "",
|
||||
dependencies,
|
||||
registrationName: key,
|
||||
defaultModuleDeclaration: {
|
||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
},
|
||||
},
|
||||
moduleDeclaration: {
|
||||
resources: conf?.resources ?? MODULE_RESOURCE_TYPE.SHARED,
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
},
|
||||
dependencies,
|
||||
options: conf?.options ?? {},
|
||||
}
|
||||
}
|
||||
|
||||
export const registerMedusaLinkModule = (
|
||||
definition: ModuleDefinition,
|
||||
moduleDeclaration: Partial<InternalModuleDeclaration>,
|
||||
moduleExports?: ModuleExports
|
||||
): Record<string, ModuleResolution> => {
|
||||
const moduleResolutions = {} as Record<string, ModuleResolution>
|
||||
|
||||
moduleResolutions[definition.key] = getInternalModuleResolution(
|
||||
definition,
|
||||
moduleDeclaration as InternalModuleDeclaration,
|
||||
moduleExports
|
||||
)
|
||||
|
||||
return moduleResolutions
|
||||
}
|
||||
|
||||
function getInternalModuleResolution(
|
||||
definition: ModuleDefinition,
|
||||
moduleConfig: InternalModuleDeclaration | string | false,
|
||||
moduleExports?: ModuleExports
|
||||
): ModuleResolution {
|
||||
if (typeof moduleConfig === "boolean") {
|
||||
if (!moduleConfig && definition.isRequired) {
|
||||
throw new Error(`Module: ${definition.label} is required`)
|
||||
}
|
||||
|
||||
if (!moduleConfig) {
|
||||
return {
|
||||
resolutionPath: false,
|
||||
definition,
|
||||
dependencies: [],
|
||||
options: {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isObj = isObject(moduleConfig)
|
||||
let resolutionPath = definition.defaultPackage
|
||||
|
||||
// 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.
|
||||
""
|
||||
}
|
||||
|
||||
const moduleDeclaration = isObj ? moduleConfig : {}
|
||||
const additionalDependencies = isObj ? moduleConfig.dependencies || [] : []
|
||||
|
||||
return {
|
||||
resolutionPath,
|
||||
definition,
|
||||
dependencies: [
|
||||
...new Set(
|
||||
(definition.dependencies || []).concat(additionalDependencies)
|
||||
),
|
||||
],
|
||||
moduleDeclaration: {
|
||||
...(definition.defaultModuleDeclaration ?? {}),
|
||||
...moduleDeclaration,
|
||||
},
|
||||
moduleExports,
|
||||
options: isObj ? moduleConfig.options ?? {} : {},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./load-internal"
|
||||
@@ -0,0 +1,162 @@
|
||||
import {
|
||||
InternalModuleDeclaration,
|
||||
Logger,
|
||||
MedusaContainer,
|
||||
MODULE_RESOURCE_TYPE,
|
||||
ModuleExports,
|
||||
ModuleResolution,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
createMedusaContainer,
|
||||
MedusaModuleType,
|
||||
} from "@medusajs/utils"
|
||||
import { asFunction, asValue } from "awilix"
|
||||
|
||||
export async function loadInternalModule(
|
||||
container: MedusaContainer,
|
||||
resolution: ModuleResolution,
|
||||
logger: Logger,
|
||||
migrationOnly?: boolean,
|
||||
loaderOnly?: boolean
|
||||
): Promise<{ error?: Error } | void> {
|
||||
const registrationName = !loaderOnly
|
||||
? resolution.definition.registrationName
|
||||
: resolution.definition.registrationName + "__loaderOnly"
|
||||
|
||||
const { resources } =
|
||||
resolution.moduleDeclaration as InternalModuleDeclaration
|
||||
|
||||
let loadedModule: ModuleExports
|
||||
try {
|
||||
// When loading manually, we pass the exports to be loaded, meaning that we do not need to import the package to find
|
||||
// the exports. This is useful when a package export an initialize function which will bootstrap itself and therefore
|
||||
// does not need to import the package that is currently being loaded as it would create a
|
||||
// circular reference.
|
||||
const modulePath = resolution.resolutionPath as string
|
||||
|
||||
if (resolution.moduleExports) {
|
||||
loadedModule = resolution.moduleExports
|
||||
} else {
|
||||
loadedModule = await import(modulePath)
|
||||
loadedModule = (loadedModule as any).default
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
resolution.definition.isRequired &&
|
||||
resolution.definition.defaultPackage
|
||||
) {
|
||||
return {
|
||||
error: new Error(
|
||||
`Make sure you have installed the default package: ${resolution.definition.defaultPackage}`
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
return { error }
|
||||
}
|
||||
|
||||
if (!loadedModule?.service) {
|
||||
container.register({
|
||||
[registrationName]: asValue(undefined),
|
||||
})
|
||||
|
||||
return {
|
||||
error: new Error(
|
||||
"No service found in module. Make sure your module exports a service."
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
if (migrationOnly) {
|
||||
// Partially loaded module, only register the service __joinerConfig function to be able to resolve it later
|
||||
const moduleService = {
|
||||
__joinerConfig: loadedModule.service.prototype.__joinerConfig,
|
||||
}
|
||||
container.register({
|
||||
[registrationName]: asValue(moduleService),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const localContainer = createMedusaContainer()
|
||||
|
||||
const dependencies = resolution?.dependencies ?? []
|
||||
if (resources === MODULE_RESOURCE_TYPE.SHARED) {
|
||||
dependencies.push(
|
||||
ContainerRegistrationKeys.MANAGER,
|
||||
ContainerRegistrationKeys.CONFIG_MODULE,
|
||||
ContainerRegistrationKeys.LOGGER,
|
||||
ContainerRegistrationKeys.PG_CONNECTION
|
||||
)
|
||||
}
|
||||
|
||||
for (const dependency of dependencies) {
|
||||
localContainer.register(
|
||||
dependency,
|
||||
asFunction(() => {
|
||||
return container.resolve(dependency, { allowUnregistered: true })
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
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),
|
||||
})
|
||||
|
||||
return {
|
||||
error: new Error(
|
||||
`Loaders for module ${resolution.definition.label} failed: ${err.message}`
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const moduleService = loadedModule.service
|
||||
|
||||
container.register({
|
||||
[registrationName]: 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
|
||||
const service = container.resolve(registrationName)
|
||||
await service.__hooks?.onApplicationPrepareShutdown()
|
||||
await service.__hooks?.onApplicationShutdown()
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadModuleMigrations(
|
||||
resolution: ModuleResolution,
|
||||
moduleExports?: ModuleExports
|
||||
): Promise<[Function | undefined, Function | undefined]> {
|
||||
let loadedModule: ModuleExports
|
||||
try {
|
||||
loadedModule =
|
||||
moduleExports ?? (await import(resolution.resolutionPath as string))
|
||||
|
||||
return [loadedModule.runMigrations, loadedModule.revertMigration]
|
||||
} catch {
|
||||
return [undefined, undefined]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
import { mergeTypeDefs } from "@graphql-tools/merge"
|
||||
import { makeExecutableSchema } from "@graphql-tools/schema"
|
||||
import { RemoteFetchDataCallback } from "@medusajs/orchestration"
|
||||
import {
|
||||
ExternalModuleDeclaration,
|
||||
InternalModuleDeclaration,
|
||||
LoadedModule,
|
||||
MedusaContainer,
|
||||
MODULE_RESOURCE_TYPE,
|
||||
MODULE_SCOPE,
|
||||
ModuleDefinition,
|
||||
ModuleExports,
|
||||
ModuleJoinerConfig,
|
||||
ModuleServiceInitializeOptions,
|
||||
RemoteJoinerOptions,
|
||||
RemoteJoinerQuery,
|
||||
RemoteQueryFunction,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
createMedusaContainer,
|
||||
isObject,
|
||||
isString,
|
||||
ModulesSdkUtils,
|
||||
promiseAll,
|
||||
} from "@medusajs/utils"
|
||||
import { asValue } from "awilix"
|
||||
import {
|
||||
MODULE_PACKAGE_NAMES,
|
||||
ModuleRegistrationName,
|
||||
Modules,
|
||||
} from "./definitions"
|
||||
import { MedusaModule } from "./medusa-module"
|
||||
import { RemoteLink } from "./remote-link"
|
||||
import { RemoteQuery } from "./remote-query"
|
||||
import { cleanGraphQLSchema } from "./utils"
|
||||
|
||||
const LinkModulePackage = MODULE_PACKAGE_NAMES[Modules.LINK]
|
||||
|
||||
export type RunMigrationFn = (
|
||||
options?: ModuleServiceInitializeOptions,
|
||||
injectedDependencies?: Record<any, any>
|
||||
) => Promise<void>
|
||||
|
||||
export type MedusaModuleConfig = {
|
||||
[key: string | Modules]:
|
||||
| string
|
||||
| boolean
|
||||
| Partial<InternalModuleDeclaration | ExternalModuleDeclaration>
|
||||
}
|
||||
|
||||
export type SharedResources = {
|
||||
database?: ModuleServiceInitializeOptions["database"] & {
|
||||
/**
|
||||
* {
|
||||
* name?: string
|
||||
* afterCreate?: Function
|
||||
* min?: number
|
||||
* max?: number
|
||||
* refreshIdle?: boolean
|
||||
* idleTimeoutMillis?: number
|
||||
* reapIntervalMillis?: number
|
||||
* returnToHead?: boolean
|
||||
* priorityRange?: number
|
||||
* log?: (message: string, logLevel: string) => void
|
||||
* }
|
||||
*/
|
||||
pool?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadModules(
|
||||
modulesConfig,
|
||||
sharedContainer,
|
||||
migrationOnly = false,
|
||||
loaderOnly = false,
|
||||
workerMode: "shared" | "worker" | "server" = "server"
|
||||
) {
|
||||
const allModules = {}
|
||||
|
||||
await Promise.all(
|
||||
Object.keys(modulesConfig).map(async (moduleName) => {
|
||||
const mod = modulesConfig[moduleName]
|
||||
let path: string
|
||||
let moduleExports: ModuleExports | undefined = undefined
|
||||
let declaration: any = {}
|
||||
let definition: Partial<ModuleDefinition> | undefined = undefined
|
||||
|
||||
if (isObject(mod)) {
|
||||
const mod_ = mod as unknown as InternalModuleDeclaration
|
||||
path = mod_.resolve ?? MODULE_PACKAGE_NAMES[moduleName]
|
||||
definition = mod_.definition
|
||||
moduleExports = !isString(mod_.resolve)
|
||||
? (mod_.resolve as ModuleExports)
|
||||
: undefined
|
||||
declaration = { ...mod }
|
||||
delete declaration.definition
|
||||
} else {
|
||||
path = MODULE_PACKAGE_NAMES[moduleName]
|
||||
}
|
||||
|
||||
declaration.scope ??= MODULE_SCOPE.INTERNAL
|
||||
if (
|
||||
declaration.scope === MODULE_SCOPE.INTERNAL &&
|
||||
!declaration.resources
|
||||
) {
|
||||
declaration.resources = MODULE_RESOURCE_TYPE.SHARED
|
||||
}
|
||||
|
||||
const loaded = (await MedusaModule.bootstrap({
|
||||
moduleKey: moduleName,
|
||||
defaultPath: path,
|
||||
declaration,
|
||||
sharedContainer,
|
||||
moduleDefinition: definition as ModuleDefinition,
|
||||
moduleExports,
|
||||
migrationOnly,
|
||||
loaderOnly,
|
||||
workerMode,
|
||||
})) as LoadedModule
|
||||
|
||||
if (loaderOnly) {
|
||||
return
|
||||
}
|
||||
|
||||
const service = loaded[moduleName]
|
||||
sharedContainer.register({
|
||||
[service.__definition.registrationName]: asValue(service),
|
||||
})
|
||||
|
||||
if (allModules[moduleName] && !Array.isArray(allModules[moduleName])) {
|
||||
allModules[moduleName] = []
|
||||
}
|
||||
|
||||
if (allModules[moduleName]) {
|
||||
;(allModules[moduleName] as LoadedModule[]).push(loaded[moduleName])
|
||||
} else {
|
||||
allModules[moduleName] = loaded[moduleName]
|
||||
}
|
||||
})
|
||||
)
|
||||
return allModules
|
||||
}
|
||||
|
||||
async function initializeLinks({
|
||||
config,
|
||||
linkModules,
|
||||
injectedDependencies,
|
||||
moduleExports,
|
||||
}) {
|
||||
try {
|
||||
const { initialize, runMigrations } =
|
||||
moduleExports ?? (await import(LinkModulePackage))
|
||||
|
||||
const linkResolution = await initialize(
|
||||
config,
|
||||
linkModules,
|
||||
injectedDependencies
|
||||
)
|
||||
|
||||
return { remoteLink: new RemoteLink(), linkResolution, runMigrations }
|
||||
} catch (err) {
|
||||
console.warn("Error initializing link modules.", err)
|
||||
|
||||
return {
|
||||
remoteLink: undefined,
|
||||
linkResolution: undefined,
|
||||
runMigrations: undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isMedusaModule(mod) {
|
||||
return typeof mod?.initialize === "function"
|
||||
}
|
||||
|
||||
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 type MedusaAppOutput = {
|
||||
modules: Record<string, LoadedModule | LoadedModule[]>
|
||||
link: RemoteLink | undefined
|
||||
query: RemoteQueryFunction
|
||||
entitiesMap?: Record<string, any>
|
||||
notFound?: Record<string, Record<string, string>>
|
||||
runMigrations: RunMigrationFn
|
||||
onApplicationShutdown: () => Promise<void>
|
||||
onApplicationPrepareShutdown: () => Promise<void>
|
||||
}
|
||||
|
||||
export type MedusaAppOptions = {
|
||||
workerMode?: "shared" | "worker" | "server"
|
||||
sharedContainer?: MedusaContainer
|
||||
sharedResourcesConfig?: SharedResources
|
||||
loadedModules?: LoadedModule[]
|
||||
servicesConfig?: ModuleJoinerConfig[]
|
||||
modulesConfigPath?: string
|
||||
modulesConfigFileName?: string
|
||||
modulesConfig?: MedusaModuleConfig
|
||||
linkModules?: ModuleJoinerConfig | ModuleJoinerConfig[]
|
||||
remoteFetchData?: RemoteFetchDataCallback
|
||||
injectedDependencies?: any
|
||||
onApplicationStartCb?: () => void
|
||||
/**
|
||||
* Forces the modules bootstrapper to only run the modules loaders and return prematurely
|
||||
*/
|
||||
loaderOnly?: boolean
|
||||
}
|
||||
|
||||
async function MedusaApp_({
|
||||
sharedContainer,
|
||||
sharedResourcesConfig,
|
||||
servicesConfig,
|
||||
modulesConfigPath,
|
||||
modulesConfigFileName,
|
||||
modulesConfig,
|
||||
linkModules,
|
||||
remoteFetchData,
|
||||
injectedDependencies = {},
|
||||
onApplicationStartCb,
|
||||
migrationOnly = false,
|
||||
loaderOnly = false,
|
||||
workerMode = "server",
|
||||
}: MedusaAppOptions & {
|
||||
migrationOnly?: boolean
|
||||
} = {}): Promise<MedusaAppOutput> {
|
||||
const sharedContainer_ = createMedusaContainer({}, sharedContainer)
|
||||
|
||||
const onApplicationShutdown = async () => {
|
||||
await promiseAll([
|
||||
MedusaModule.onApplicationShutdown(),
|
||||
sharedContainer_.dispose(),
|
||||
])
|
||||
}
|
||||
|
||||
const onApplicationPrepareShutdown = async () => {
|
||||
await promiseAll([MedusaModule.onApplicationPrepareShutdown()])
|
||||
}
|
||||
|
||||
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 (
|
||||
sharedResourcesConfig?.database?.connection &&
|
||||
!injectedDependencies[ContainerRegistrationKeys.PG_CONNECTION]
|
||||
) {
|
||||
injectedDependencies[ContainerRegistrationKeys.PG_CONNECTION] =
|
||||
sharedResourcesConfig.database.connection
|
||||
} else if (
|
||||
dbData.clientUrl &&
|
||||
!injectedDependencies[ContainerRegistrationKeys.PG_CONNECTION]
|
||||
) {
|
||||
injectedDependencies[ContainerRegistrationKeys.PG_CONNECTION] =
|
||||
ModulesSdkUtils.createPgConnection({
|
||||
...(sharedResourcesConfig?.database ?? {}),
|
||||
...dbData,
|
||||
})
|
||||
}
|
||||
|
||||
// remove the link module from the modules
|
||||
const linkModule = modules[LinkModulePackage] ?? modules[Modules.LINK]
|
||||
delete modules[LinkModulePackage]
|
||||
delete modules[Modules.LINK]
|
||||
|
||||
let linkModuleOptions = {}
|
||||
|
||||
if (isObject(linkModule)) {
|
||||
linkModuleOptions = linkModule
|
||||
}
|
||||
|
||||
for (const injectedDependency of Object.keys(injectedDependencies)) {
|
||||
sharedContainer_.register({
|
||||
[injectedDependency]: asValue(injectedDependencies[injectedDependency]),
|
||||
})
|
||||
}
|
||||
|
||||
const allModules = await loadModules(
|
||||
modules,
|
||||
sharedContainer_,
|
||||
migrationOnly,
|
||||
loaderOnly,
|
||||
workerMode
|
||||
)
|
||||
|
||||
if (loaderOnly) {
|
||||
return {
|
||||
onApplicationShutdown,
|
||||
onApplicationPrepareShutdown,
|
||||
modules: allModules,
|
||||
link: undefined,
|
||||
query: async () => {
|
||||
throw new Error("Querying not allowed in loaderOnly mode")
|
||||
},
|
||||
runMigrations: async () => {
|
||||
throw new Error("Migrations not allowed in loaderOnly mode")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Share Event bus with link modules
|
||||
injectedDependencies[ModuleRegistrationName.EVENT_BUS] =
|
||||
sharedContainer_.resolve(ModuleRegistrationName.EVENT_BUS, {
|
||||
allowUnregistered: true,
|
||||
})
|
||||
|
||||
const { remoteLink, runMigrations: linkModuleMigration } =
|
||||
await initializeLinks({
|
||||
config: linkModuleOptions,
|
||||
linkModules,
|
||||
injectedDependencies,
|
||||
moduleExports: isMedusaModule(linkModule) ? linkModule : undefined,
|
||||
})
|
||||
|
||||
const loadedSchema = getLoadedSchema()
|
||||
const { schema, notFound } = cleanAndMergeSchema(loadedSchema)
|
||||
|
||||
const remoteQuery = new RemoteQuery({
|
||||
servicesConfig,
|
||||
customRemoteFetchData: remoteFetchData,
|
||||
})
|
||||
|
||||
const query = async (
|
||||
query: string | RemoteJoinerQuery | object,
|
||||
variables?: Record<string, unknown>,
|
||||
options?: RemoteJoinerOptions
|
||||
) => {
|
||||
return await remoteQuery.query(query, variables, options)
|
||||
}
|
||||
|
||||
const runMigrations: RunMigrationFn = async (
|
||||
linkModuleOptions
|
||||
): Promise<void> => {
|
||||
for (const moduleName of Object.keys(allModules)) {
|
||||
const moduleResolution = MedusaModule.getModuleResolutions(moduleName)
|
||||
|
||||
if (!moduleResolution.options?.database) {
|
||||
moduleResolution.options ??= {}
|
||||
moduleResolution.options.database = {
|
||||
...(sharedResourcesConfig?.database ?? {}),
|
||||
}
|
||||
}
|
||||
|
||||
await MedusaModule.migrateUp(
|
||||
moduleResolution.definition.key,
|
||||
moduleResolution.resolutionPath as string,
|
||||
moduleResolution.options,
|
||||
moduleResolution.moduleExports
|
||||
)
|
||||
}
|
||||
|
||||
const linkModuleOpt = { ...(linkModuleOptions ?? {}) }
|
||||
linkModuleOpt.database ??= {
|
||||
...(sharedResourcesConfig?.database ?? {}),
|
||||
}
|
||||
|
||||
linkModuleMigration &&
|
||||
(await linkModuleMigration({
|
||||
options: linkModuleOpt,
|
||||
injectedDependencies,
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
onApplicationShutdown,
|
||||
onApplicationPrepareShutdown,
|
||||
modules: allModules,
|
||||
link: remoteLink,
|
||||
query,
|
||||
entitiesMap: schema.getTypeMap(),
|
||||
notFound,
|
||||
runMigrations,
|
||||
}
|
||||
}
|
||||
|
||||
export async function MedusaApp(
|
||||
options: MedusaAppOptions = {}
|
||||
): Promise<MedusaAppOutput> {
|
||||
try {
|
||||
return await MedusaApp_(options)
|
||||
} finally {
|
||||
MedusaModule.onApplicationStart(options.onApplicationStartCb)
|
||||
}
|
||||
}
|
||||
|
||||
export async function MedusaAppMigrateUp(
|
||||
options: MedusaAppOptions = {}
|
||||
): Promise<void> {
|
||||
const migrationOnly = true
|
||||
|
||||
const { runMigrations } = await MedusaApp_({
|
||||
...options,
|
||||
migrationOnly,
|
||||
})
|
||||
|
||||
await runMigrations().finally(MedusaModule.clearInstances)
|
||||
}
|
||||
@@ -0,0 +1,580 @@
|
||||
import {
|
||||
ExternalModuleDeclaration,
|
||||
IModuleService,
|
||||
InternalModuleDeclaration,
|
||||
LinkModuleDefinition,
|
||||
LoadedModule,
|
||||
MedusaContainer,
|
||||
MODULE_RESOURCE_TYPE,
|
||||
MODULE_SCOPE,
|
||||
ModuleBootstrapDeclaration,
|
||||
ModuleDefinition,
|
||||
ModuleExports,
|
||||
ModuleJoinerConfig,
|
||||
ModuleResolution,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
createMedusaContainer,
|
||||
promiseAll,
|
||||
simpleHash,
|
||||
stringifyCircular,
|
||||
} from "@medusajs/utils"
|
||||
import { EOL } from "os"
|
||||
import {
|
||||
moduleLoader,
|
||||
registerMedusaLinkModule,
|
||||
registerMedusaModule,
|
||||
} from "./loaders"
|
||||
|
||||
import { asValue } from "awilix"
|
||||
import { loadModuleMigrations } from "./loaders/utils"
|
||||
|
||||
const logger: any = {
|
||||
log: (a) => console.log(a),
|
||||
info: (a) => console.log(a),
|
||||
warn: (a) => console.warn(a),
|
||||
error: (a) => console.error(a),
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface MedusaModule {
|
||||
getLoadedModules(
|
||||
aliases?: Map<string, string>
|
||||
): { [key: string]: LoadedModule }[]
|
||||
getModuleInstance(moduleKey: string, alias?: string): LoadedModule
|
||||
}
|
||||
}
|
||||
|
||||
type ModuleAlias = {
|
||||
key: string
|
||||
hash: string
|
||||
isLink: boolean
|
||||
alias?: string
|
||||
main?: boolean
|
||||
}
|
||||
|
||||
export type ModuleBootstrapOptions = {
|
||||
moduleKey: string
|
||||
defaultPath: string
|
||||
declaration?: ModuleBootstrapDeclaration
|
||||
moduleExports?: ModuleExports
|
||||
sharedContainer?: MedusaContainer
|
||||
moduleDefinition?: ModuleDefinition
|
||||
injectedDependencies?: Record<string, any>
|
||||
/**
|
||||
* In this mode, all instances are partially loaded, meaning that the module will not be fully loaded and the services will not be available.
|
||||
* Don't forget to clear the instances (MedusaModule.clearInstances()) after the migration are done.
|
||||
*/
|
||||
migrationOnly?: boolean
|
||||
/**
|
||||
* Forces the modules bootstrapper to only run the modules loaders and return prematurely. This
|
||||
* is meant for modules that have data loader. In a test env, in order to clear all data
|
||||
* and load them back, we need to run those loader again
|
||||
*/
|
||||
loaderOnly?: boolean
|
||||
workerMode?: "shared" | "worker" | "server"
|
||||
}
|
||||
|
||||
export type LinkModuleBootstrapOptions = {
|
||||
definition: LinkModuleDefinition
|
||||
declaration?: InternalModuleDeclaration
|
||||
moduleExports?: ModuleExports
|
||||
injectedDependencies?: Record<string, any>
|
||||
}
|
||||
|
||||
export class MedusaModule {
|
||||
private static instances_: Map<string, { [key: string]: IModuleService }> =
|
||||
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()
|
||||
private static moduleResolutions_: Map<string, ModuleResolution> = new Map()
|
||||
|
||||
public static getLoadedModules(
|
||||
aliases?: Map<string, string>
|
||||
): { [key: string]: LoadedModule }[] {
|
||||
return [...MedusaModule.modules_.entries()].map(([key]) => {
|
||||
if (aliases?.has(key)) {
|
||||
return MedusaModule.getModuleInstance(key, aliases.get(key))
|
||||
}
|
||||
|
||||
return MedusaModule.getModuleInstance(key)
|
||||
})
|
||||
}
|
||||
|
||||
public static onApplicationStart(onApplicationStartCb?: () => void): void {
|
||||
for (const instances of MedusaModule.instances_.values()) {
|
||||
for (const instance of Object.values(instances) as IModuleService[]) {
|
||||
if (instance?.__hooks) {
|
||||
instance.__hooks?.onApplicationStart
|
||||
?.bind(instance)()
|
||||
.then(() => {
|
||||
onApplicationStartCb?.()
|
||||
})
|
||||
.catch(() => {
|
||||
// The module should handle this and log it
|
||||
return void 0
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public static async onApplicationShutdown(): Promise<void> {
|
||||
await promiseAll(
|
||||
[...MedusaModule.instances_.values()]
|
||||
.map((instances) => {
|
||||
return Object.values(instances).map((instance: IModuleService) => {
|
||||
return instance.__hooks?.onApplicationShutdown
|
||||
?.bind(instance)()
|
||||
.catch(() => {
|
||||
// The module should handle this and log it
|
||||
return void 0
|
||||
})
|
||||
})
|
||||
})
|
||||
.flat()
|
||||
)
|
||||
}
|
||||
|
||||
public static async onApplicationPrepareShutdown(): Promise<void> {
|
||||
await promiseAll(
|
||||
[...MedusaModule.instances_.values()]
|
||||
.map((instances) => {
|
||||
return Object.values(instances).map((instance: IModuleService) => {
|
||||
return instance.__hooks?.onApplicationPrepareShutdown
|
||||
?.bind(instance)()
|
||||
.catch(() => {
|
||||
// The module should handle this and log it
|
||||
return void 0
|
||||
})
|
||||
})
|
||||
})
|
||||
.flat()
|
||||
)
|
||||
}
|
||||
|
||||
public static clearInstances(): void {
|
||||
MedusaModule.instances_.clear()
|
||||
MedusaModule.modules_.clear()
|
||||
MedusaModule.joinerConfig_.clear()
|
||||
MedusaModule.moduleResolutions_.clear()
|
||||
}
|
||||
|
||||
public static isInstalled(moduleKey: string, alias?: string): boolean {
|
||||
if (alias) {
|
||||
return (
|
||||
MedusaModule.modules_.has(moduleKey) &&
|
||||
MedusaModule.modules_.get(moduleKey)!.some((m) => m.alias === alias)
|
||||
)
|
||||
}
|
||||
|
||||
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 getModuleResolutions(moduleKey: string): ModuleResolution {
|
||||
return MedusaModule.moduleResolutions_.get(moduleKey)!
|
||||
}
|
||||
|
||||
public static getAllModuleResolutions(): ModuleResolution[] {
|
||||
return [...MedusaModule.moduleResolutions_.values()]
|
||||
}
|
||||
|
||||
public static setModuleResolution(
|
||||
moduleKey: string,
|
||||
resolution: ModuleResolution
|
||||
): ModuleResolution {
|
||||
MedusaModule.moduleResolutions_.set(moduleKey, resolution)
|
||||
|
||||
return resolution
|
||||
}
|
||||
|
||||
public static setJoinerConfig(
|
||||
moduleKey: string,
|
||||
config: ModuleJoinerConfig
|
||||
): ModuleJoinerConfig {
|
||||
MedusaModule.joinerConfig_.set(moduleKey, config)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
public static getModuleInstance(
|
||||
moduleKey: string,
|
||||
alias?: string
|
||||
): any | undefined {
|
||||
if (!MedusaModule.modules_.has(moduleKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
let mod
|
||||
const modules = MedusaModule.modules_.get(moduleKey)!
|
||||
if (alias) {
|
||||
mod = modules.find((m) => m.alias === alias)
|
||||
|
||||
return MedusaModule.instances_.get(mod?.hash)
|
||||
}
|
||||
|
||||
mod = modules.find((m) => m.main) ?? modules[0]
|
||||
|
||||
return MedusaModule.instances_.get(mod?.hash)
|
||||
}
|
||||
|
||||
private static registerModule(
|
||||
moduleKey: string,
|
||||
loadedModule: ModuleAlias
|
||||
): void {
|
||||
if (!MedusaModule.modules_.has(moduleKey)) {
|
||||
MedusaModule.modules_.set(moduleKey, [])
|
||||
}
|
||||
|
||||
const modules = MedusaModule.modules_.get(moduleKey)!
|
||||
|
||||
if (modules.some((m) => m.alias === loadedModule.alias)) {
|
||||
throw new Error(
|
||||
`Module ${moduleKey} already registed as '${loadedModule.alias}'. Please choose a different alias.`
|
||||
)
|
||||
}
|
||||
|
||||
if (loadedModule.main) {
|
||||
if (modules.some((m) => m.main)) {
|
||||
throw new Error(`Module ${moduleKey} already have a 'main' registered.`)
|
||||
}
|
||||
}
|
||||
|
||||
modules.push(loadedModule)
|
||||
MedusaModule.modules_.set(moduleKey, modules!)
|
||||
}
|
||||
|
||||
public static async bootstrap<T>({
|
||||
moduleKey,
|
||||
defaultPath,
|
||||
declaration,
|
||||
moduleExports,
|
||||
sharedContainer,
|
||||
moduleDefinition,
|
||||
injectedDependencies,
|
||||
migrationOnly,
|
||||
loaderOnly,
|
||||
workerMode,
|
||||
}: ModuleBootstrapOptions): Promise<{
|
||||
[key: string]: T
|
||||
}> {
|
||||
const hashKey = simpleHash(
|
||||
stringifyCircular({ moduleKey, defaultPath, declaration })
|
||||
)
|
||||
|
||||
if (!loaderOnly && MedusaModule.instances_.has(hashKey)) {
|
||||
return MedusaModule.instances_.get(hashKey)! as {
|
||||
[key: string]: T
|
||||
}
|
||||
}
|
||||
|
||||
if (!loaderOnly && MedusaModule.loading_.has(hashKey)) {
|
||||
return MedusaModule.loading_.get(hashKey)
|
||||
}
|
||||
|
||||
let finishLoading: any
|
||||
let errorLoading: any
|
||||
|
||||
const loadingPromise = new Promise((resolve, reject) => {
|
||||
finishLoading = resolve
|
||||
errorLoading = reject
|
||||
})
|
||||
|
||||
if (!loaderOnly) {
|
||||
MedusaModule.loading_.set(hashKey, loadingPromise)
|
||||
}
|
||||
|
||||
let modDeclaration =
|
||||
declaration ??
|
||||
({} as InternalModuleDeclaration | ExternalModuleDeclaration)
|
||||
|
||||
if (declaration?.scope !== MODULE_SCOPE.EXTERNAL) {
|
||||
modDeclaration = {
|
||||
scope: declaration?.scope || MODULE_SCOPE.INTERNAL,
|
||||
resources: declaration?.resources || MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: defaultPath,
|
||||
options: declaration?.options ?? declaration,
|
||||
alias: declaration?.alias,
|
||||
main: declaration?.main,
|
||||
worker_mode: workerMode,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Only do that while legacy modules sharing the manager exists then remove the ternary in favor of createMedusaContainer({}, globalContainer)
|
||||
const container =
|
||||
modDeclaration.scope === MODULE_SCOPE.INTERNAL &&
|
||||
modDeclaration.resources === MODULE_RESOURCE_TYPE.SHARED
|
||||
? sharedContainer ?? createMedusaContainer()
|
||||
: createMedusaContainer({}, sharedContainer)
|
||||
|
||||
if (injectedDependencies) {
|
||||
for (const service in injectedDependencies) {
|
||||
container.register(service, asValue(injectedDependencies[service]))
|
||||
if (!container.hasRegistration(service)) {
|
||||
container.register(service, asValue(injectedDependencies[service]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const moduleResolutions = registerMedusaModule(
|
||||
moduleKey,
|
||||
modDeclaration!,
|
||||
moduleExports,
|
||||
moduleDefinition
|
||||
)
|
||||
|
||||
const logger_ =
|
||||
container.resolve("logger", { allowUnregistered: true }) ?? logger
|
||||
|
||||
try {
|
||||
await moduleLoader({
|
||||
container,
|
||||
moduleResolutions,
|
||||
logger: logger_,
|
||||
migrationOnly,
|
||||
loaderOnly,
|
||||
})
|
||||
} catch (err) {
|
||||
errorLoading(err)
|
||||
throw err
|
||||
}
|
||||
|
||||
const services = {}
|
||||
|
||||
if (loaderOnly) {
|
||||
finishLoading(services)
|
||||
return services
|
||||
}
|
||||
|
||||
for (const resolution of Object.values(
|
||||
moduleResolutions
|
||||
) as ModuleResolution[]) {
|
||||
const keyName = resolution.definition.key
|
||||
const registrationName = resolution.definition.registrationName
|
||||
|
||||
services[keyName] = container.resolve(registrationName)
|
||||
services[keyName].__definition = resolution.definition
|
||||
|
||||
if (resolution.definition.isQueryable) {
|
||||
const joinerConfig: ModuleJoinerConfig = await services[
|
||||
keyName
|
||||
].__joinerConfig()
|
||||
|
||||
if (!joinerConfig.primaryKeys) {
|
||||
logger_.warn(
|
||||
`Primary keys are not defined by the module ${keyName}. Setting default primary key to 'id'${EOL}`
|
||||
)
|
||||
|
||||
joinerConfig.primaryKeys = ["id"]
|
||||
}
|
||||
|
||||
services[keyName].__joinerConfig = joinerConfig
|
||||
MedusaModule.setJoinerConfig(keyName, joinerConfig)
|
||||
}
|
||||
|
||||
MedusaModule.setModuleResolution(keyName, resolution)
|
||||
|
||||
MedusaModule.registerModule(keyName, {
|
||||
key: keyName,
|
||||
hash: hashKey,
|
||||
alias: modDeclaration.alias ?? hashKey,
|
||||
main: !!modDeclaration.main,
|
||||
isLink: false,
|
||||
})
|
||||
}
|
||||
|
||||
MedusaModule.instances_.set(hashKey, services)
|
||||
finishLoading(services)
|
||||
MedusaModule.loading_.delete(hashKey)
|
||||
|
||||
return services
|
||||
}
|
||||
|
||||
public static async bootstrapLink({
|
||||
definition,
|
||||
declaration,
|
||||
moduleExports,
|
||||
injectedDependencies,
|
||||
}: LinkModuleBootstrapOptions): Promise<{
|
||||
[key: string]: unknown
|
||||
}> {
|
||||
const moduleKey = definition.key
|
||||
const hashKey = simpleHash(stringifyCircular({ moduleKey, declaration }))
|
||||
|
||||
if (MedusaModule.instances_.has(hashKey)) {
|
||||
return { [moduleKey]: MedusaModule.instances_.get(hashKey) }
|
||||
}
|
||||
|
||||
if (MedusaModule.loading_.has(hashKey)) {
|
||||
return MedusaModule.loading_.get(hashKey)
|
||||
}
|
||||
|
||||
let finishLoading: any
|
||||
let errorLoading: any
|
||||
MedusaModule.loading_.set(
|
||||
hashKey,
|
||||
new Promise((resolve, reject) => {
|
||||
finishLoading = resolve
|
||||
errorLoading = reject
|
||||
})
|
||||
)
|
||||
|
||||
let modDeclaration =
|
||||
declaration ?? ({} as Partial<InternalModuleDeclaration>)
|
||||
|
||||
const moduleDefinition: ModuleDefinition = {
|
||||
key: definition.key,
|
||||
registrationName: definition.key,
|
||||
dependencies: definition.dependencies,
|
||||
defaultPackage: "",
|
||||
label: definition.label,
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
defaultModuleDeclaration: definition.defaultModuleDeclaration,
|
||||
}
|
||||
|
||||
modDeclaration = {
|
||||
resolve: "",
|
||||
options: declaration,
|
||||
alias: declaration?.alias,
|
||||
main: declaration?.main,
|
||||
}
|
||||
|
||||
const container = createMedusaContainer()
|
||||
|
||||
if (injectedDependencies) {
|
||||
for (const service in injectedDependencies) {
|
||||
container.register(service, asValue(injectedDependencies[service]))
|
||||
}
|
||||
}
|
||||
|
||||
const moduleResolutions = registerMedusaLinkModule(
|
||||
moduleDefinition,
|
||||
modDeclaration as InternalModuleDeclaration,
|
||||
moduleExports
|
||||
)
|
||||
|
||||
const logger_ =
|
||||
container.resolve("logger", { allowUnregistered: true }) ?? logger
|
||||
|
||||
try {
|
||||
await moduleLoader({
|
||||
container,
|
||||
moduleResolutions,
|
||||
logger: logger_,
|
||||
})
|
||||
} catch (err) {
|
||||
errorLoading(err)
|
||||
throw err
|
||||
}
|
||||
|
||||
const services = {}
|
||||
|
||||
for (const resolution of Object.values(
|
||||
moduleResolutions
|
||||
) as ModuleResolution[]) {
|
||||
const keyName = resolution.definition.key
|
||||
const registrationName = resolution.definition.registrationName
|
||||
|
||||
services[keyName] = container.resolve(registrationName)
|
||||
services[keyName].__definition = resolution.definition
|
||||
|
||||
if (resolution.definition.isQueryable) {
|
||||
const joinerConfig: ModuleJoinerConfig = await services[
|
||||
keyName
|
||||
].__joinerConfig()
|
||||
|
||||
services[keyName].__joinerConfig = joinerConfig
|
||||
MedusaModule.setJoinerConfig(keyName, joinerConfig)
|
||||
|
||||
if (!joinerConfig.isLink) {
|
||||
throw new Error(
|
||||
"MedusaModule.bootstrapLink must be used only for Link Modules"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MedusaModule.setModuleResolution(keyName, resolution)
|
||||
MedusaModule.registerModule(keyName, {
|
||||
key: keyName,
|
||||
hash: hashKey,
|
||||
alias: modDeclaration.alias ?? hashKey,
|
||||
main: !!modDeclaration.main,
|
||||
isLink: true,
|
||||
})
|
||||
}
|
||||
|
||||
MedusaModule.instances_.set(hashKey, services)
|
||||
finishLoading(services)
|
||||
MedusaModule.loading_.delete(hashKey)
|
||||
|
||||
return services
|
||||
}
|
||||
|
||||
public static async migrateUp(
|
||||
moduleKey: string,
|
||||
modulePath: string,
|
||||
options?: Record<string, any>,
|
||||
moduleExports?: ModuleExports
|
||||
): Promise<void> {
|
||||
const moduleResolutions = registerMedusaModule(moduleKey, {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: modulePath,
|
||||
options,
|
||||
})
|
||||
|
||||
for (const mod in moduleResolutions) {
|
||||
const [migrateUp] = await loadModuleMigrations(
|
||||
moduleResolutions[mod],
|
||||
moduleExports
|
||||
)
|
||||
|
||||
if (typeof migrateUp === "function") {
|
||||
await migrateUp({
|
||||
options,
|
||||
logger,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async migrateDown(
|
||||
moduleKey: string,
|
||||
modulePath: string,
|
||||
options?: Record<string, any>,
|
||||
moduleExports?: ModuleExports
|
||||
): Promise<void> {
|
||||
const moduleResolutions = registerMedusaModule(moduleKey, {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: modulePath,
|
||||
options,
|
||||
})
|
||||
|
||||
for (const mod in moduleResolutions) {
|
||||
const [, migrateDown] = await loadModuleMigrations(
|
||||
moduleResolutions[mod],
|
||||
moduleExports
|
||||
)
|
||||
|
||||
if (typeof migrateDown === "function") {
|
||||
await migrateDown({
|
||||
options,
|
||||
logger,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
global.MedusaModule ??= MedusaModule
|
||||
exports.MedusaModule = global.MedusaModule
|
||||
@@ -0,0 +1,456 @@
|
||||
import {
|
||||
ILinkModule,
|
||||
LoadedModule,
|
||||
ModuleJoinerRelationship,
|
||||
} from "@medusajs/types"
|
||||
|
||||
import { isObject, promiseAll, toPascalCase } from "@medusajs/utils"
|
||||
import { Modules } from "./definitions"
|
||||
import { MedusaModule } from "./medusa-module"
|
||||
import { linkingErrorMessage } from "./utils/linking-error"
|
||||
|
||||
export type DeleteEntityInput = {
|
||||
[moduleName: string | Modules]: Record<string, string | string[]>
|
||||
}
|
||||
export type RestoreEntityInput = DeleteEntityInput
|
||||
|
||||
type LinkDefinition = {
|
||||
[moduleName: string]: {
|
||||
[fieldName: string]: string
|
||||
}
|
||||
} & {
|
||||
data?: Record<string, unknown>
|
||||
}
|
||||
|
||||
type RemoteRelationship = ModuleJoinerRelationship & {
|
||||
isPrimary: boolean
|
||||
isForeign: boolean
|
||||
}
|
||||
|
||||
type LoadedLinkModule = LoadedModule & ILinkModule
|
||||
type DeleteEntities = { [key: string]: string[] }
|
||||
type RemovedIds = {
|
||||
[serviceName: string]: DeleteEntities
|
||||
}
|
||||
type RestoredIds = RemovedIds
|
||||
|
||||
type CascadeError = {
|
||||
serviceName: string
|
||||
method: String
|
||||
args: any
|
||||
error: Error
|
||||
}
|
||||
|
||||
export class RemoteLink {
|
||||
private modulesMap: Map<string, LoadedLinkModule> = new Map()
|
||||
private relationsPairs: Map<string, LoadedLinkModule> = new Map()
|
||||
private relations: Map<string, Map<string, RemoteRelationship[]>> = new Map()
|
||||
|
||||
constructor(modulesLoaded?: LoadedModule[]) {
|
||||
if (!modulesLoaded?.length) {
|
||||
modulesLoaded = MedusaModule.getLoadedModules().map(
|
||||
(mod) => Object.values(mod)[0]
|
||||
)
|
||||
}
|
||||
|
||||
for (const mod of modulesLoaded) {
|
||||
this.addModule(mod)
|
||||
}
|
||||
}
|
||||
|
||||
public addModule(mod: LoadedModule): void {
|
||||
if (!mod.__definition.isQueryable || mod.__joinerConfig.isReadOnlyLink) {
|
||||
return
|
||||
}
|
||||
|
||||
const joinerConfig = mod.__joinerConfig
|
||||
|
||||
const serviceName = joinerConfig.isLink
|
||||
? joinerConfig.serviceName!
|
||||
: mod.__definition.key
|
||||
|
||||
if (this.modulesMap.has(serviceName)) {
|
||||
throw new Error(
|
||||
`Duplicated instance of module ${serviceName} is not allowed.`
|
||||
)
|
||||
}
|
||||
|
||||
if (joinerConfig.relationships?.length) {
|
||||
if (joinerConfig.isLink) {
|
||||
const [primary, foreign] = joinerConfig.relationships
|
||||
const key = [
|
||||
primary.serviceName,
|
||||
primary.foreignKey,
|
||||
foreign.serviceName,
|
||||
foreign.foreignKey,
|
||||
].join("-")
|
||||
this.relationsPairs.set(key, mod as unknown as LoadedLinkModule)
|
||||
}
|
||||
for (const relationship of joinerConfig.relationships) {
|
||||
if (joinerConfig.isLink && !relationship.deleteCascade) {
|
||||
continue
|
||||
}
|
||||
|
||||
this.addRelationship(serviceName, {
|
||||
...relationship,
|
||||
isPrimary: false,
|
||||
isForeign: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (joinerConfig.extends?.length) {
|
||||
for (const service of joinerConfig.extends) {
|
||||
const relationship = service.relationship
|
||||
this.addRelationship(service.serviceName, {
|
||||
...relationship,
|
||||
serviceName: serviceName,
|
||||
isPrimary: true,
|
||||
isForeign: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.modulesMap.set(serviceName, mod as unknown as LoadedLinkModule)
|
||||
}
|
||||
|
||||
private addRelationship(
|
||||
serviceName: string,
|
||||
relationship: RemoteRelationship
|
||||
): void {
|
||||
const { primaryKey, foreignKey } = relationship
|
||||
|
||||
if (!this.relations.has(serviceName)) {
|
||||
this.relations.set(serviceName, new Map())
|
||||
}
|
||||
|
||||
const key = relationship.isPrimary ? primaryKey : foreignKey
|
||||
const serviceMap = this.relations.get(serviceName)!
|
||||
if (!serviceMap.has(key)) {
|
||||
serviceMap.set(key, [])
|
||||
}
|
||||
|
||||
serviceMap.get(key)!.push(relationship)
|
||||
}
|
||||
|
||||
getLinkModule(
|
||||
moduleA: string,
|
||||
moduleAKey: string,
|
||||
moduleB: string,
|
||||
moduleBKey: string
|
||||
) {
|
||||
const key = [moduleA, moduleAKey, moduleB, moduleBKey].join("-")
|
||||
return this.relationsPairs.get(key)
|
||||
}
|
||||
|
||||
getRelationships(): Map<string, Map<string, RemoteRelationship[]>> {
|
||||
return this.relations
|
||||
}
|
||||
|
||||
private getLinkableKeys(mod: LoadedLinkModule) {
|
||||
return (
|
||||
(mod.__joinerConfig.linkableKeys &&
|
||||
Object.keys(mod.__joinerConfig.linkableKeys)) ||
|
||||
mod.__joinerConfig.primaryKeys ||
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
private async executeCascade(
|
||||
removedServices: DeleteEntityInput,
|
||||
method: "softDelete" | "restore"
|
||||
): Promise<[CascadeError[] | null, RemovedIds]> {
|
||||
const removedIds: RemovedIds = {}
|
||||
const returnIdsList: RemovedIds = {}
|
||||
const processedIds: Record<string, Set<string>> = {}
|
||||
|
||||
const services = Object.keys(removedServices).map((serviceName) => {
|
||||
const deleteKeys = {}
|
||||
|
||||
for (const field in removedServices[serviceName]) {
|
||||
deleteKeys[field] = Array.isArray(removedServices[serviceName][field])
|
||||
? removedServices[serviceName][field]
|
||||
: [removedServices[serviceName][field]]
|
||||
}
|
||||
|
||||
return { serviceName, deleteKeys }
|
||||
})
|
||||
|
||||
const errors: CascadeError[] = []
|
||||
const cascade = async (
|
||||
services: { serviceName: string; deleteKeys: DeleteEntities }[],
|
||||
isCascading: boolean = false
|
||||
): Promise<RemovedIds> => {
|
||||
if (errors.length) {
|
||||
return returnIdsList
|
||||
}
|
||||
|
||||
const servicePromises = services.map(async (serviceInfo) => {
|
||||
const serviceRelations = this.relations.get(serviceInfo.serviceName)!
|
||||
|
||||
if (!serviceRelations) {
|
||||
return
|
||||
}
|
||||
|
||||
const values = serviceInfo.deleteKeys
|
||||
|
||||
const deletePromises: Promise<void>[] = []
|
||||
|
||||
for (const field in values) {
|
||||
const relatedServices = serviceRelations.get(field)
|
||||
|
||||
if (!relatedServices || !values[field]?.length) {
|
||||
continue
|
||||
}
|
||||
|
||||
const relatedServicesPromises = relatedServices.map(
|
||||
async (relatedService) => {
|
||||
const { serviceName, primaryKey, args } = relatedService
|
||||
const processedHash = `${serviceName}-${primaryKey}`
|
||||
|
||||
if (!processedIds[processedHash]) {
|
||||
processedIds[processedHash] = new Set()
|
||||
}
|
||||
|
||||
const unprocessedIds = values[field].filter(
|
||||
(id) => !processedIds[processedHash].has(id)
|
||||
)
|
||||
|
||||
if (!unprocessedIds.length) {
|
||||
return
|
||||
}
|
||||
|
||||
unprocessedIds.forEach((id) => {
|
||||
processedIds[processedHash].add(id)
|
||||
})
|
||||
|
||||
let cascadeDelKeys: DeleteEntities = {}
|
||||
cascadeDelKeys[primaryKey] = unprocessedIds
|
||||
const service: ILinkModule = this.modulesMap.get(serviceName)!
|
||||
|
||||
const returnFields = this.getLinkableKeys(
|
||||
service as LoadedLinkModule
|
||||
)
|
||||
|
||||
let deletedEntities: Record<string, string[]> = {}
|
||||
|
||||
try {
|
||||
if (args?.methodSuffix) {
|
||||
method += toPascalCase(args.methodSuffix)
|
||||
}
|
||||
|
||||
const removed = await service[method](cascadeDelKeys, {
|
||||
returnLinkableKeys: returnFields,
|
||||
})
|
||||
|
||||
deletedEntities = removed as Record<string, string[]>
|
||||
} catch (error) {
|
||||
errors.push({
|
||||
serviceName,
|
||||
method,
|
||||
args: cascadeDelKeys,
|
||||
error: JSON.parse(
|
||||
JSON.stringify(error, Object.getOwnPropertyNames(error))
|
||||
),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (Object.keys(deletedEntities).length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
removedIds[serviceName] = {
|
||||
...deletedEntities,
|
||||
}
|
||||
|
||||
if (!isCascading) {
|
||||
returnIdsList[serviceName] = {
|
||||
...deletedEntities,
|
||||
}
|
||||
} else {
|
||||
const [mainKey] = returnFields
|
||||
|
||||
if (!returnIdsList[serviceName]) {
|
||||
returnIdsList[serviceName] = {}
|
||||
}
|
||||
if (!returnIdsList[serviceName][mainKey]) {
|
||||
returnIdsList[serviceName][mainKey] = []
|
||||
}
|
||||
|
||||
returnIdsList[serviceName][mainKey] = [
|
||||
...new Set(
|
||||
returnIdsList[serviceName][mainKey].concat(
|
||||
deletedEntities[mainKey]
|
||||
)
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
Object.keys(deletedEntities).forEach((key) => {
|
||||
deletedEntities[key].forEach((id) => {
|
||||
const hash = `${serviceName}-${key}`
|
||||
if (!processedIds[hash]) {
|
||||
processedIds[hash] = new Set()
|
||||
}
|
||||
|
||||
processedIds[hash].add(id)
|
||||
})
|
||||
})
|
||||
|
||||
await cascade(
|
||||
[
|
||||
{
|
||||
serviceName: serviceName,
|
||||
deleteKeys: deletedEntities as DeleteEntities,
|
||||
},
|
||||
],
|
||||
true
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
deletePromises.push(...relatedServicesPromises)
|
||||
}
|
||||
|
||||
await promiseAll(deletePromises)
|
||||
})
|
||||
|
||||
await promiseAll(servicePromises)
|
||||
return returnIdsList
|
||||
}
|
||||
|
||||
const result = await cascade(services)
|
||||
|
||||
return [errors.length ? errors : null, result]
|
||||
}
|
||||
|
||||
async create(link: LinkDefinition | LinkDefinition[]): Promise<unknown[]> {
|
||||
const allLinks = Array.isArray(link) ? link : [link]
|
||||
const serviceLinks = new Map<
|
||||
string,
|
||||
[string | string[], string, Record<string, unknown>?][]
|
||||
>()
|
||||
|
||||
for (const rel of allLinks) {
|
||||
const extraFields = rel.data
|
||||
delete rel.data
|
||||
|
||||
const mods = Object.keys(rel)
|
||||
if (mods.length > 2) {
|
||||
throw new Error(`Only two modules can be linked.`)
|
||||
}
|
||||
|
||||
const [moduleA, moduleB] = mods
|
||||
const pk = Object.keys(rel[moduleA])
|
||||
const moduleAKey = pk.join(",")
|
||||
const moduleBKey = Object.keys(rel[moduleB]).join(",")
|
||||
|
||||
const service = this.getLinkModule(
|
||||
moduleA,
|
||||
moduleAKey,
|
||||
moduleB,
|
||||
moduleBKey
|
||||
)
|
||||
|
||||
if (!service) {
|
||||
throw new Error(
|
||||
linkingErrorMessage({
|
||||
moduleA,
|
||||
moduleAKey,
|
||||
moduleB,
|
||||
moduleBKey,
|
||||
type: "link",
|
||||
})
|
||||
)
|
||||
} else if (!serviceLinks.has(service.__definition.key)) {
|
||||
serviceLinks.set(service.__definition.key, [])
|
||||
}
|
||||
|
||||
const pkValue =
|
||||
pk.length === 1 ? rel[moduleA][pk[0]] : pk.map((k) => rel[moduleA][k])
|
||||
|
||||
const fields: unknown[] = [pkValue, rel[moduleB][moduleBKey]]
|
||||
if (isObject(extraFields)) {
|
||||
fields.push(extraFields)
|
||||
}
|
||||
|
||||
serviceLinks.get(service.__definition.key)?.push(fields as any)
|
||||
}
|
||||
|
||||
const promises: Promise<unknown[]>[] = []
|
||||
for (const [serviceName, links] of serviceLinks) {
|
||||
const service = this.modulesMap.get(serviceName)!
|
||||
promises.push(service.create(links))
|
||||
}
|
||||
|
||||
const created = await promiseAll(promises)
|
||||
return created.flat()
|
||||
}
|
||||
|
||||
async dismiss(link: LinkDefinition | LinkDefinition[]): Promise<unknown[]> {
|
||||
const allLinks = Array.isArray(link) ? link : [link]
|
||||
const serviceLinks = new Map<string, [string | string[], string][]>()
|
||||
|
||||
for (const rel of allLinks) {
|
||||
const mods = Object.keys(rel)
|
||||
if (mods.length > 2) {
|
||||
throw new Error(`Only two modules can be linked.`)
|
||||
}
|
||||
|
||||
const [moduleA, moduleB] = mods
|
||||
const pk = Object.keys(rel[moduleA])
|
||||
const moduleAKey = pk.join(",")
|
||||
const moduleBKey = Object.keys(rel[moduleB]).join(",")
|
||||
|
||||
const service = this.getLinkModule(
|
||||
moduleA,
|
||||
moduleAKey,
|
||||
moduleB,
|
||||
moduleBKey
|
||||
)
|
||||
|
||||
if (!service) {
|
||||
throw new Error(
|
||||
linkingErrorMessage({
|
||||
moduleA,
|
||||
moduleAKey,
|
||||
moduleB,
|
||||
moduleBKey,
|
||||
type: "dismiss",
|
||||
})
|
||||
)
|
||||
} else if (!serviceLinks.has(service.__definition.key)) {
|
||||
serviceLinks.set(service.__definition.key, [])
|
||||
}
|
||||
|
||||
const pkValue =
|
||||
pk.length === 1 ? rel[moduleA][pk[0]] : pk.map((k) => rel[moduleA][k])
|
||||
|
||||
serviceLinks
|
||||
.get(service.__definition.key)
|
||||
?.push([pkValue, rel[moduleB][moduleBKey]])
|
||||
}
|
||||
|
||||
const promises: Promise<unknown[]>[] = []
|
||||
for (const [serviceName, links] of serviceLinks) {
|
||||
const service = this.modulesMap.get(serviceName)!
|
||||
promises.push(service.dismiss(links))
|
||||
}
|
||||
|
||||
const created = await promiseAll(promises)
|
||||
return created.flat()
|
||||
}
|
||||
|
||||
async delete(
|
||||
removedServices: DeleteEntityInput
|
||||
): Promise<[CascadeError[] | null, RemovedIds]> {
|
||||
return await this.executeCascade(removedServices, "softDelete")
|
||||
}
|
||||
|
||||
async restore(
|
||||
removedServices: DeleteEntityInput
|
||||
): Promise<[CascadeError[] | null, RestoredIds]> {
|
||||
return await this.executeCascade(removedServices, "restore")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
import {
|
||||
RemoteFetchDataCallback,
|
||||
RemoteJoiner,
|
||||
toRemoteJoinerQuery,
|
||||
} from "@medusajs/orchestration"
|
||||
import {
|
||||
JoinerArgument,
|
||||
JoinerRelationship,
|
||||
JoinerServiceConfig,
|
||||
LoadedModule,
|
||||
ModuleJoinerConfig,
|
||||
RemoteExpandProperty,
|
||||
RemoteJoinerOptions,
|
||||
RemoteJoinerQuery,
|
||||
RemoteNestedExpands,
|
||||
} from "@medusajs/types"
|
||||
import { isString, toPascalCase } from "@medusajs/utils"
|
||||
import { MedusaModule } from "./medusa-module"
|
||||
|
||||
export class RemoteQuery {
|
||||
private remoteJoiner: RemoteJoiner
|
||||
private modulesMap: Map<string, LoadedModule> = new Map()
|
||||
private customRemoteFetchData?: RemoteFetchDataCallback
|
||||
|
||||
constructor({
|
||||
modulesLoaded,
|
||||
customRemoteFetchData,
|
||||
servicesConfig = [],
|
||||
}: {
|
||||
modulesLoaded?: LoadedModule[]
|
||||
customRemoteFetchData?: RemoteFetchDataCallback
|
||||
servicesConfig?: ModuleJoinerConfig[]
|
||||
}) {
|
||||
const servicesConfig_ = [...servicesConfig]
|
||||
|
||||
if (!modulesLoaded?.length) {
|
||||
modulesLoaded = MedusaModule.getLoadedModules().map(
|
||||
(mod) => Object.values(mod)[0]
|
||||
)
|
||||
}
|
||||
|
||||
for (const mod of modulesLoaded) {
|
||||
if (!mod.__definition.isQueryable) {
|
||||
continue
|
||||
}
|
||||
|
||||
const serviceName = mod.__definition.key
|
||||
|
||||
if (this.modulesMap.has(serviceName)) {
|
||||
throw new Error(
|
||||
`Duplicated instance of module ${serviceName} is not allowed.`
|
||||
)
|
||||
}
|
||||
|
||||
this.modulesMap.set(serviceName, mod)
|
||||
servicesConfig_!.push(mod.__joinerConfig)
|
||||
}
|
||||
|
||||
this.customRemoteFetchData = customRemoteFetchData
|
||||
|
||||
this.remoteJoiner = new RemoteJoiner(
|
||||
servicesConfig_ as JoinerServiceConfig[],
|
||||
this.remoteFetchData.bind(this),
|
||||
{ autoCreateServiceNameAlias: false }
|
||||
)
|
||||
}
|
||||
|
||||
public setFetchDataCallback(
|
||||
remoteFetchData: (
|
||||
expand: RemoteExpandProperty,
|
||||
keyField: string,
|
||||
ids?: (unknown | unknown[])[],
|
||||
relationship?: any
|
||||
) => Promise<{
|
||||
data: unknown[] | { [path: string]: unknown[] }
|
||||
path?: string
|
||||
}>
|
||||
): void {
|
||||
this.remoteJoiner.setFetchDataCallback(remoteFetchData)
|
||||
}
|
||||
|
||||
public static getAllFieldsAndRelations(
|
||||
expand: RemoteExpandProperty | RemoteNestedExpands[number],
|
||||
prefix = "",
|
||||
args: JoinerArgument = {} as JoinerArgument
|
||||
): {
|
||||
select?: string[]
|
||||
relations: string[]
|
||||
args: JoinerArgument
|
||||
} {
|
||||
expand = JSON.parse(JSON.stringify(expand))
|
||||
|
||||
let fields: Set<string> = new Set()
|
||||
let relations: string[] = []
|
||||
|
||||
let shouldSelectAll = false
|
||||
|
||||
for (const field of expand.fields ?? []) {
|
||||
if (field === "*") {
|
||||
shouldSelectAll = true
|
||||
break
|
||||
}
|
||||
fields.add(prefix ? `${prefix}.${field}` : field)
|
||||
}
|
||||
|
||||
args[prefix] = expand.args
|
||||
|
||||
for (const property in expand.expands ?? {}) {
|
||||
const newPrefix = prefix ? `${prefix}.${property}` : property
|
||||
|
||||
relations.push(newPrefix)
|
||||
fields.delete(newPrefix)
|
||||
|
||||
const result = RemoteQuery.getAllFieldsAndRelations(
|
||||
expand.expands![property],
|
||||
newPrefix,
|
||||
args
|
||||
)
|
||||
|
||||
result.select?.forEach(fields.add, fields)
|
||||
relations = relations.concat(result.relations)
|
||||
}
|
||||
|
||||
const allFields = Array.from(fields)
|
||||
const select =
|
||||
allFields.length && !shouldSelectAll
|
||||
? allFields
|
||||
: shouldSelectAll
|
||||
? undefined
|
||||
: []
|
||||
return { select, relations, args }
|
||||
}
|
||||
|
||||
private hasPagination(options: { [attr: string]: unknown }): boolean {
|
||||
if (!options) {
|
||||
return false
|
||||
}
|
||||
|
||||
const attrs = ["skip", "cursor"]
|
||||
return Object.keys(options).some((key) => attrs.includes(key))
|
||||
}
|
||||
|
||||
private buildPagination(options, count) {
|
||||
return {
|
||||
skip: options.skip,
|
||||
take: options.take,
|
||||
cursor: options.cursor,
|
||||
// TODO: next cursor
|
||||
count,
|
||||
}
|
||||
}
|
||||
|
||||
public async remoteFetchData(
|
||||
expand: RemoteExpandProperty,
|
||||
keyField: string,
|
||||
ids?: (unknown | unknown[])[],
|
||||
relationship?: JoinerRelationship
|
||||
): Promise<{
|
||||
data: unknown[] | { [path: string]: unknown }
|
||||
path?: string
|
||||
}> {
|
||||
if (this.customRemoteFetchData) {
|
||||
const resp = await this.customRemoteFetchData(expand, keyField, ids)
|
||||
if (resp !== undefined) {
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
const serviceConfig = expand.serviceConfig
|
||||
const service = this.modulesMap.get(serviceConfig.serviceName)!
|
||||
|
||||
let filters = {}
|
||||
const options = {
|
||||
...RemoteQuery.getAllFieldsAndRelations(expand),
|
||||
}
|
||||
|
||||
const availableOptions = [
|
||||
"skip",
|
||||
"take",
|
||||
"limit",
|
||||
"offset",
|
||||
"cursor",
|
||||
"sort",
|
||||
"order",
|
||||
"withDeleted",
|
||||
]
|
||||
const availableOptionsAlias = new Map([
|
||||
["limit", "take"],
|
||||
["offset", "skip"],
|
||||
])
|
||||
|
||||
for (const arg of expand.args || []) {
|
||||
if (arg.name === "filters" && arg.value) {
|
||||
filters = { ...filters, ...arg.value }
|
||||
} else if (arg.name === "context" && arg.value) {
|
||||
filters["context"] = arg.value
|
||||
} else if (availableOptions.includes(arg.name)) {
|
||||
const argName = availableOptionsAlias.has(arg.name)
|
||||
? availableOptionsAlias.get(arg.name)!
|
||||
: arg.name
|
||||
options[argName] = arg.value
|
||||
}
|
||||
}
|
||||
|
||||
if (ids) {
|
||||
filters[keyField] = ids
|
||||
}
|
||||
|
||||
const hasPagination = this.hasPagination(options)
|
||||
|
||||
let methodName = hasPagination ? "listAndCount" : "list"
|
||||
|
||||
if (relationship?.args?.methodSuffix) {
|
||||
methodName += toPascalCase(relationship.args.methodSuffix)
|
||||
} else if (serviceConfig?.args?.methodSuffix) {
|
||||
methodName += toPascalCase(serviceConfig.args.methodSuffix)
|
||||
}
|
||||
|
||||
if (typeof service[methodName] !== "function") {
|
||||
throw new Error(
|
||||
`Method "${methodName}" does not exist on "${serviceConfig.serviceName}"`
|
||||
)
|
||||
}
|
||||
|
||||
const result = await service[methodName](filters, options)
|
||||
|
||||
if (hasPagination) {
|
||||
const [data, count] = result
|
||||
return {
|
||||
data: {
|
||||
rows: data,
|
||||
metadata: this.buildPagination(options, count),
|
||||
},
|
||||
path: "rows",
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: result,
|
||||
}
|
||||
}
|
||||
|
||||
public async query(
|
||||
query: string | RemoteJoinerQuery | object,
|
||||
variables?: Record<string, unknown>,
|
||||
options?: RemoteJoinerOptions
|
||||
): Promise<any> {
|
||||
let finalQuery: RemoteJoinerQuery = query as RemoteJoinerQuery
|
||||
|
||||
if (isString(query)) {
|
||||
finalQuery = RemoteJoiner.parseQuery(query, variables)
|
||||
} else if (!isString(finalQuery?.service) && !isString(finalQuery?.alias)) {
|
||||
finalQuery = toRemoteJoinerQuery(query, variables)
|
||||
}
|
||||
|
||||
return await this.remoteJoiner.query(finalQuery, options)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { GraphQLNamedType, GraphQLObjectType, isObjectType } from "graphql"
|
||||
|
||||
/**
|
||||
* From graphql schema get all the fields for the requested type and relations
|
||||
*
|
||||
* @param schemaTypeMap
|
||||
* @param typeName
|
||||
* @param relations
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* const userModule = `
|
||||
* type User {
|
||||
* id: ID!
|
||||
* name: String!
|
||||
* blabla: WHATEVER
|
||||
* }
|
||||
*
|
||||
* type Post {
|
||||
* author: User!
|
||||
* }
|
||||
* `
|
||||
*
|
||||
* const postModule = `
|
||||
* type Post {
|
||||
* id: ID!
|
||||
* title: String!
|
||||
* date: String
|
||||
* }
|
||||
*
|
||||
* type User {
|
||||
* posts: [Post!]!
|
||||
* }
|
||||
*
|
||||
* type WHATEVER {
|
||||
* random_field: String
|
||||
* post: Post
|
||||
* }
|
||||
* `
|
||||
*
|
||||
* const mergedSchema = mergeTypeDefs([userModule, postModule])
|
||||
* const schema = makeExecutableSchema({
|
||||
* typeDefs: mergedSchema,
|
||||
* })
|
||||
*
|
||||
* const fields = graphqlSchemaToFields(types, "User", ["posts"])
|
||||
*
|
||||
* console.log(fields)
|
||||
*
|
||||
* // [
|
||||
* // "id",
|
||||
* // "name",
|
||||
* // "posts.id",
|
||||
* // "posts.title",
|
||||
* // "posts.date",
|
||||
* // ]
|
||||
*/
|
||||
export function graphqlSchemaToFields(
|
||||
schemaTypeMap: { [key: string]: GraphQLNamedType },
|
||||
typeName: string,
|
||||
relations: string[] = []
|
||||
) {
|
||||
const result: string[] = []
|
||||
|
||||
function traverseFields(typeName, parent = "") {
|
||||
const type = schemaTypeMap[typeName]
|
||||
|
||||
if (!(type instanceof GraphQLObjectType)) {
|
||||
return
|
||||
}
|
||||
|
||||
const fields = type.getFields()
|
||||
|
||||
for (const fieldName in fields) {
|
||||
const field = fields[fieldName]
|
||||
let fieldType = field.type as any
|
||||
|
||||
while (fieldType.ofType) {
|
||||
fieldType = fieldType.ofType
|
||||
}
|
||||
|
||||
const composedField = parent ? `${parent}.${fieldName}` : fieldName
|
||||
if (!isObjectType(fieldType)) {
|
||||
result.push(composedField)
|
||||
} else if (relations.includes(composedField)) {
|
||||
traverseFields(fieldType.name, composedField)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverseFields(typeName)
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./clean-graphql-schema"
|
||||
export * from "./graphql-schema-to-fields"
|
||||
export * from "./initialize-factory"
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
ExternalModuleDeclaration,
|
||||
InternalModuleDeclaration,
|
||||
ModuleExports,
|
||||
ModuleServiceInitializeCustomDataLayerOptions,
|
||||
ModuleServiceInitializeOptions,
|
||||
} from "@medusajs/types"
|
||||
import { MODULE_PACKAGE_NAMES } from "../definitions"
|
||||
import { MedusaModule } from "../medusa-module"
|
||||
|
||||
/**
|
||||
* Generate a initialize module factory that is exported by the module to be initialized manually
|
||||
*
|
||||
* @param moduleName
|
||||
* @param moduleDefinition
|
||||
*/
|
||||
export function initializeFactory<T>({
|
||||
moduleName,
|
||||
moduleDefinition,
|
||||
}: {
|
||||
moduleName: string
|
||||
moduleDefinition: ModuleExports
|
||||
}) {
|
||||
return async (
|
||||
options?:
|
||||
| ModuleServiceInitializeOptions
|
||||
| ModuleServiceInitializeCustomDataLayerOptions
|
||||
| ExternalModuleDeclaration
|
||||
| InternalModuleDeclaration,
|
||||
injectedDependencies?: any
|
||||
) => {
|
||||
const loaded = await MedusaModule.bootstrap<T>({
|
||||
moduleKey: moduleName,
|
||||
defaultPath: MODULE_PACKAGE_NAMES[moduleName],
|
||||
declaration: options as
|
||||
| InternalModuleDeclaration
|
||||
| ExternalModuleDeclaration,
|
||||
injectedDependencies,
|
||||
moduleExports: moduleDefinition,
|
||||
})
|
||||
|
||||
return loaded[moduleName] as T
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
const typeToMethod = new Map([
|
||||
[`dismiss`, `dismiss`],
|
||||
[`link`, `create`],
|
||||
])
|
||||
|
||||
type LinkingErrorMessageInput = {
|
||||
moduleA: string
|
||||
moduleAKey: string
|
||||
moduleB: string
|
||||
moduleBKey: string
|
||||
type: "dismiss" | "link"
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Example: Module to dismiss salesChannel and apiKey by keys sales_channel_id and api_key_id was not found. Ensure the link exists, keys are correct, and the link is passed in the correct order to method 'remoteLink.dismiss'
|
||||
*/
|
||||
export const linkingErrorMessage = (input: LinkingErrorMessageInput) => {
|
||||
const { moduleA, moduleB, moduleAKey, moduleBKey, type } = input
|
||||
return `Module to type ${moduleA} and ${moduleB} by keys ${moduleAKey} and ${moduleBKey} was not found. Ensure the link exists, keys are correct, and link is passed in the correct order to method 'remoteLink.${typeToMethod.get(
|
||||
type
|
||||
)}'.
|
||||
`
|
||||
}
|
||||
Reference in New Issue
Block a user