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:
committed by
GitHub
parent
bc4c9e0d32
commit
4d16acf5f0
@@ -0,0 +1,190 @@
|
||||
import { InternalModuleDeclaration, MedusaModule } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
ExternalModuleDeclaration,
|
||||
ILinkModule,
|
||||
LinkModuleDefinition,
|
||||
LoaderOptions,
|
||||
MODULE_RESOURCE_TYPE,
|
||||
MODULE_SCOPE,
|
||||
ModuleExports,
|
||||
ModuleJoinerConfig,
|
||||
ModuleServiceInitializeCustomDataLayerOptions,
|
||||
ModuleServiceInitializeOptions,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
lowerCaseFirst,
|
||||
simpleHash,
|
||||
} from "@medusajs/utils"
|
||||
import * as linkDefinitions from "../definitions"
|
||||
import { getMigration } from "../migration"
|
||||
import { InitializeModuleInjectableDependencies } from "../types"
|
||||
import { composeLinkName } from "../utils"
|
||||
import { getLinkModuleDefinition } from "./module-definition"
|
||||
|
||||
export const initialize = async (
|
||||
options?:
|
||||
| ModuleServiceInitializeOptions
|
||||
| ModuleServiceInitializeCustomDataLayerOptions
|
||||
| ExternalModuleDeclaration
|
||||
| InternalModuleDeclaration,
|
||||
modulesDefinition?: ModuleJoinerConfig[],
|
||||
injectedDependencies?: InitializeModuleInjectableDependencies
|
||||
): Promise<{ [link: string]: ILinkModule }> => {
|
||||
const allLinks = {}
|
||||
const modulesLoadedKeys = MedusaModule.getLoadedModules().map(
|
||||
(mod) => Object.keys(mod)[0]
|
||||
)
|
||||
|
||||
const allLinksToLoad = Object.values(linkDefinitions).concat(
|
||||
modulesDefinition ?? []
|
||||
)
|
||||
|
||||
for (const linkDefinition of allLinksToLoad) {
|
||||
const definition = JSON.parse(JSON.stringify(linkDefinition))
|
||||
|
||||
const [primary, foreign] = definition.relationships ?? []
|
||||
|
||||
if (definition.relationships?.length !== 2 && !definition.isReadOnlyLink) {
|
||||
throw new Error(
|
||||
`Link module ${definition.serviceName} can only link 2 modules.`
|
||||
)
|
||||
} else if (
|
||||
foreign?.foreignKey?.split(",").length > 1 &&
|
||||
!definition.isReadOnlyLink
|
||||
) {
|
||||
throw new Error(`Foreign key cannot be a composed key.`)
|
||||
}
|
||||
|
||||
const serviceKey = !definition.isReadOnlyLink
|
||||
? lowerCaseFirst(
|
||||
definition.serviceName ??
|
||||
composeLinkName(
|
||||
primary.serviceName,
|
||||
primary.foreignKey,
|
||||
foreign.serviceName,
|
||||
foreign.foreignKey
|
||||
)
|
||||
)
|
||||
: simpleHash(JSON.stringify(definition.extends))
|
||||
|
||||
if (modulesLoadedKeys.includes(serviceKey)) {
|
||||
continue
|
||||
} else if (serviceKey in allLinks) {
|
||||
throw new Error(`Link module ${serviceKey} already defined.`)
|
||||
}
|
||||
|
||||
if (definition.isReadOnlyLink) {
|
||||
const extended: any[] = []
|
||||
for (const extension of definition.extends ?? []) {
|
||||
if (
|
||||
modulesLoadedKeys.includes(extension.serviceName) &&
|
||||
modulesLoadedKeys.includes(extension.relationship.serviceName)
|
||||
) {
|
||||
extended.push(extension)
|
||||
}
|
||||
}
|
||||
|
||||
definition.extends = extended
|
||||
if (extended.length === 0) {
|
||||
continue
|
||||
}
|
||||
} else if (
|
||||
!modulesLoadedKeys.includes(primary.serviceName) ||
|
||||
!modulesLoadedKeys.includes(foreign.serviceName)
|
||||
) {
|
||||
// TODO: This should be uncommented when all modules are done
|
||||
// continue
|
||||
}
|
||||
|
||||
const moduleDefinition = getLinkModuleDefinition(
|
||||
definition,
|
||||
primary,
|
||||
foreign
|
||||
) as ModuleExports
|
||||
|
||||
const linkModuleDefinition: LinkModuleDefinition = {
|
||||
key: serviceKey,
|
||||
registrationName: serviceKey,
|
||||
label: serviceKey,
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: injectedDependencies?.[
|
||||
ContainerRegistrationKeys.PG_CONNECTION
|
||||
]
|
||||
? MODULE_RESOURCE_TYPE.SHARED
|
||||
: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
},
|
||||
}
|
||||
|
||||
const loaded = await MedusaModule.bootstrapLink(
|
||||
linkModuleDefinition,
|
||||
options as InternalModuleDeclaration,
|
||||
moduleDefinition,
|
||||
injectedDependencies
|
||||
)
|
||||
|
||||
allLinks[serviceKey as string] = Object.values(loaded)[0]
|
||||
}
|
||||
|
||||
return allLinks
|
||||
}
|
||||
|
||||
export async function runMigrations(
|
||||
{
|
||||
options,
|
||||
logger,
|
||||
}: Omit<LoaderOptions<ModuleServiceInitializeOptions>, "container">,
|
||||
modulesDefinition?: ModuleJoinerConfig[]
|
||||
) {
|
||||
const modulesLoadedKeys = MedusaModule.getLoadedModules().map(
|
||||
(mod) => Object.keys(mod)[0]
|
||||
)
|
||||
|
||||
const allLinksToLoad = Object.values(linkDefinitions).concat(
|
||||
modulesDefinition ?? []
|
||||
)
|
||||
|
||||
const allLinks = new Set<string>()
|
||||
for (const definition of allLinksToLoad) {
|
||||
if (definition.isReadOnlyLink) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (definition.relationships?.length !== 2) {
|
||||
throw new Error(
|
||||
`Link module ${definition.serviceName} must have 2 relationships.`
|
||||
)
|
||||
}
|
||||
|
||||
const [primary, foreign] = definition.relationships ?? []
|
||||
const serviceKey = lowerCaseFirst(
|
||||
definition.serviceName ??
|
||||
composeLinkName(
|
||||
primary.serviceName,
|
||||
primary.foreignKey,
|
||||
foreign.serviceName,
|
||||
foreign.foreignKey
|
||||
)
|
||||
)
|
||||
|
||||
if (modulesLoadedKeys.includes(serviceKey)) {
|
||||
continue
|
||||
} else if (allLinks.has(serviceKey)) {
|
||||
throw new Error(`Link module ${serviceKey} already exists.`)
|
||||
}
|
||||
|
||||
allLinks.add(serviceKey)
|
||||
|
||||
if (
|
||||
!modulesLoadedKeys.includes(primary.serviceName) ||
|
||||
!modulesLoadedKeys.includes(foreign.serviceName)
|
||||
) {
|
||||
// TODO: This should be uncommented when all modules are done
|
||||
// continue
|
||||
}
|
||||
|
||||
const migrate = getMigration(definition, serviceKey, primary, foreign)
|
||||
await migrate({ options, logger })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
JoinerRelationship,
|
||||
ModuleExports,
|
||||
ModuleJoinerConfig,
|
||||
} from "@medusajs/types"
|
||||
import { getModuleService, getReadOnlyModuleService } from "@services"
|
||||
import { getLoaders } from "../loaders"
|
||||
|
||||
export function getLinkModuleDefinition(
|
||||
joinerConfig: ModuleJoinerConfig,
|
||||
primary: JoinerRelationship,
|
||||
foreign: JoinerRelationship
|
||||
): ModuleExports {
|
||||
return {
|
||||
service: joinerConfig.isReadOnlyLink
|
||||
? getReadOnlyModuleService(joinerConfig)
|
||||
: getModuleService(joinerConfig),
|
||||
loaders: getLoaders({
|
||||
joinerConfig,
|
||||
primary,
|
||||
foreign,
|
||||
}),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user