feat(link-modules,modules-sdk, utils, types, products) - Remote Link and Link modules (#4695)

What:
- Definition of all Modules links
- `link-modules` package to manage the creation of all pre-defined link or custom ones

```typescript
import { initialize as iniInventory } from "@medusajs/inventory";
import { initialize as iniProduct } from "@medusajs/product";

import {
  initialize as iniLinks,
  runMigrations as migrateLinks
} from "@medusajs/link-modules";

await Promise.all([iniInventory(), iniProduct()]);


await migrateLinks(); // create tables based on previous loaded modules

await iniLinks(); // load link based on previous loaded modules

await iniLinks(undefined, [
  {
    serviceName: "product_custom_translation_service_link",
    isLink: true,
    databaseConfig: {
      tableName: "product_transalations",
    },
    alias: [
      {
        name: "translations",
      },
    ],
    primaryKeys: ["id", "product_id", "translation_id"],
    relationships: [
      {
        serviceName: Modules.PRODUCT,
        primaryKey: "id",
        foreignKey: "product_id",
        alias: "product",
      },
      {
        serviceName: "custom_translation_service",
        primaryKey: "id",
        foreignKey: "translation_id",
        alias: "transalation",
        deleteCascade: true,
      },
    ],
    extends: [
      {
        serviceName: Modules.PRODUCT,
        relationship: {
          serviceName: "product_custom_translation_service_link",
          primaryKey: "product_id",
          foreignKey: "id",
          alias: "translations",
          isList: true,
        },
      },
      {
        serviceName: "custom_translation_service",
        relationship: {
          serviceName: "product_custom_translation_service_link",
          primaryKey: "product_id",
          foreignKey: "id",
          alias: "product_link",
        },
      },
    ],
  },
]); // custom links
```

Remote Link

```typescript
import { RemoteLink, Modules } from "@medusajs/modules-sdk";

// [...] initialize modules and links

const remoteLink = new RemoteLink();

// upsert the relationship
await remoteLink.create({ // one (object) or many (array)
  [Modules.PRODUCT]: {
    variant_id: "var_abc",
  },
  [Modules.INVENTORY]: {
    inventory_item_id: "iitem_abc",
  },
  data: { // optional additional fields
    required_quantity: 5
  }
});

// dismiss (doesn't cascade)
await remoteLink.dismiss({ // one (object) or many (array)
  [Modules.PRODUCT]: {
    variant_id: "var_abc",
  },
  [Modules.INVENTORY]: {
    inventory_item_id: "iitem_abc",
  },
});

// delete
await remoteLink.delete({
  // every key is a module
  [Modules.PRODUCT]: {
    // every key is a linkable field
    variant_id: "var_abc", // single or multiple values
  },
});

// restore
await remoteLink.restore({
  // every key is a module
  [Modules.PRODUCT]: {
    // every key is a linkable field
    variant_id: "var_abc", // single or multiple values
  },
});

```

Co-authored-by: Riqwan Thamir <5105988+riqwan@users.noreply.github.com>
This commit is contained in:
Carlos R. L. Rodrigues
2023-08-30 11:31:32 -03:00
committed by GitHub
parent bc4c9e0d32
commit 4d16acf5f0
97 changed files with 3540 additions and 424 deletions
@@ -1,9 +1,10 @@
import {
Logger,
MedusaContainer,
MODULE_SCOPE,
MedusaContainer,
ModuleResolution,
} from "@medusajs/types"
import { asValue } from "awilix"
import { EOL } from "os"
import { ModulesHelper } from "../module-helper"
@@ -11,53 +12,6 @@ import { loadInternalModule } from "./utils"
export const moduleHelper = new ModulesHelper()
async function loadModule(
container: MedusaContainer,
resolution: ModuleResolution,
logger: Logger
): 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)
}
export const moduleLoader = async ({
container,
moduleResolutions,
@@ -94,7 +48,48 @@ export const moduleLoader = async ({
}, {})
)
container.register({
modulesHelper: asValue(moduleHelper),
})
container.register("modulesHelper", asValue(moduleHelper))
}
async function loadModule(
container: MedusaContainer,
resolution: ModuleResolution,
logger: Logger
): 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)
}
@@ -6,9 +6,10 @@ import {
ModuleExports,
ModuleResolution,
} from "@medusajs/types"
import { isObject } from "@medusajs/utils"
import resolveCwd from "resolve-cwd"
import MODULE_DEFINITIONS from "../definitions"
import { MODULE_DEFINITIONS, ModulesDefinition } from "../definitions"
export const registerModules = (
modules?: Record<
@@ -46,34 +47,57 @@ export const registerModules = (
export const registerMedusaModule = (
moduleKey: string,
moduleDeclaration: InternalModuleDeclaration | ExternalModuleDeclaration,
moduleDeclaration:
| Partial<InternalModuleDeclaration | ExternalModuleDeclaration>
| string
| false,
moduleExports?: ModuleExports,
definition?: ModuleDefinition
): Record<string, ModuleResolution> => {
const moduleResolutions = {} as Record<string, ModuleResolution>
const modDefinition = definition ?? ModulesDefinition[moduleKey]
if (modDefinition === undefined) {
throw new Error(`Module: ${moduleKey} is not defined.`)
}
if (
isObject(moduleDeclaration) &&
moduleDeclaration?.scope === MODULE_SCOPE.EXTERNAL
) {
// TODO: getExternalModuleResolution(...)
throw new Error("External Modules are not supported yet.")
}
moduleResolutions[moduleKey] = getInternalModuleResolution(
modDefinition,
moduleDeclaration as InternalModuleDeclaration,
moduleExports
)
return moduleResolutions
}
export const registerMedusaLinkModule = (
definition: ModuleDefinition,
moduleDeclaration: Partial<InternalModuleDeclaration>,
moduleExports?: ModuleExports
): Record<string, ModuleResolution> => {
const moduleResolutions = {} as Record<string, ModuleResolution>
for (const definition of MODULE_DEFINITIONS) {
if (definition.key !== moduleKey) {
continue
}
if (moduleDeclaration.scope === MODULE_SCOPE.EXTERNAL) {
// TODO: getExternalModuleResolution(...)
throw new Error("External Modules are not supported yet.")
}
moduleResolutions[definition.key] = getInternalModuleResolution(
definition,
moduleDeclaration as InternalModuleDeclaration,
moduleExports
)
}
moduleResolutions[definition.key] = getInternalModuleResolution(
definition,
moduleDeclaration as InternalModuleDeclaration,
moduleExports
)
return moduleResolutions
}
function getInternalModuleResolution(
definition: ModuleDefinition,
moduleConfig: InternalModuleDeclaration | false | string,
moduleConfig: InternalModuleDeclaration | string | false,
moduleExports?: ModuleExports
): ModuleResolution {
if (typeof moduleConfig === "boolean") {
@@ -116,7 +140,7 @@ function getInternalModuleResolution(
),
],
moduleDeclaration: {
...definition.defaultModuleDeclaration,
...(definition.defaultModuleDeclaration ?? {}),
...moduleDeclaration,
},
moduleExports,
@@ -2,9 +2,9 @@ import {
Constructor,
InternalModuleDeclaration,
Logger,
MedusaContainer,
MODULE_RESOURCE_TYPE,
MODULE_SCOPE,
MedusaContainer,
ModuleExports,
ModuleResolution,
} from "@medusajs/types"
@@ -30,9 +30,14 @@ export async function loadInternalModule(
// 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.
loadedModule =
resolution.moduleExports ??
(await import(resolution.resolutionPath as string)).default
const path = resolution.resolutionPath as string
if (resolution.moduleExports) {
loadedModule = resolution.moduleExports
} else {
loadedModule = await import(path)
loadedModule = (loadedModule as any).default
}
} catch (error) {
if (
resolution.definition.isRequired &&