Files
medusa-store/packages/core/modules-sdk/src/remote-link.ts
Adrien de Peretti 48963f55ef Chore/rm main entity concept (#7709)
**What**
Update the `MedusaService` class, factory and types to remove the concept of main modules. The idea being that all method will be explicitly named and suffixes to represent the object you are trying to manipulate.
This pr also includes various fixes in different modules

Co-authored-by: Stevche Radevski <4820812+sradevski@users.noreply.github.com>
Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
2024-06-19 13:02:16 +00:00

510 lines
14 KiB
TypeScript

import {
ILinkModule,
LoadedModule,
ModuleJoinerRelationship,
} from "@medusajs/types"
import { isObject, promiseAll, toPascalCase } from "@medusajs/utils"
import { Modules } from "./definitions"
import { MedusaModule } from "./medusa-module"
import { convertRecordsToLinkDefinition } from "./utils/convert-data-to-link-definition"
import { linkingErrorMessage } from "./utils/linking-error"
export type DeleteEntityInput = {
[moduleName: string | Modules]: Record<string, string | string[]>
}
export type RestoreEntityInput = DeleteEntityInput
export type LinkDefinition = {
[moduleName: string]: {
// TODO: changing this to any temporarily as the "data" attribute is not being picked up correctly
[fieldName: string]: any
}
} & {
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
}
type LinkDataConfig = {
moduleA: string
moduleB: string
primaryKeys: string[]
moduleAKey: string
moduleBKey: string
}
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,
executionMethod: "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> => {
let method = executionMethod
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]
}
private getLinkModuleOrThrow(link: LinkDefinition): LoadedLinkModule {
const mods = Object.keys(link).filter((attr) => attr !== "data")
if (mods.length > 2) {
throw new Error(`Only two modules can be linked.`)
}
const { moduleA, moduleB, moduleAKey, moduleBKey } =
this.getLinkDataConfig(link)
const service = this.getLinkModule(moduleA, moduleAKey, moduleB, moduleBKey)
if (!service) {
throw new Error(
linkingErrorMessage({
moduleA,
moduleAKey,
moduleB,
moduleBKey,
type: "link",
})
)
}
return service
}
private getLinkDataConfig(link: LinkDefinition): LinkDataConfig {
const moduleNames = Object.keys(link).filter((attr) => attr !== "data")
const [moduleA, moduleB] = moduleNames
const primaryKeys = Object.keys(link[moduleA])
const moduleAKey = primaryKeys.join(",")
const moduleBKey = Object.keys(link[moduleB]).join(",")
return {
moduleA,
moduleB,
primaryKeys,
moduleAKey,
moduleBKey,
}
}
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 link of allLinks) {
const service = this.getLinkModuleOrThrow(link)
const { moduleA, moduleB, moduleBKey, primaryKeys } =
this.getLinkDataConfig(link)
if (!serviceLinks.has(service.__definition.key)) {
serviceLinks.set(service.__definition.key, [])
}
const pkValue =
primaryKeys.length === 1
? link[moduleA][primaryKeys[0]]
: primaryKeys.map((k) => link[moduleA][k])
const fields: unknown[] = [pkValue, link[moduleB][moduleBKey]]
if (isObject(link.data)) {
fields.push(link.data)
}
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))
}
return (await promiseAll(promises)).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 link of allLinks) {
const service = this.getLinkModuleOrThrow(link)
const { moduleA, moduleB, moduleBKey, primaryKeys } =
this.getLinkDataConfig(link)
if (!serviceLinks.has(service.__definition.key)) {
serviceLinks.set(service.__definition.key, [])
}
const pkValue =
primaryKeys.length === 1
? link[moduleA][primaryKeys[0]]
: primaryKeys.map((k) => link[moduleA][k])
serviceLinks
.get(service.__definition.key)
?.push([pkValue, link[moduleB][moduleBKey]] as any)
}
const promises: Promise<unknown[]>[] = []
for (const [serviceName, links] of serviceLinks) {
const service = this.modulesMap.get(serviceName)!
promises.push(service.dismiss(links))
}
return (await promiseAll(promises)).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")
}
async list(
link: LinkDefinition | LinkDefinition[],
options?: { asLinkDefinition?: boolean }
): Promise<(object | LinkDefinition)[]> {
const allLinks = Array.isArray(link) ? link : [link]
const serviceLinks = new Map<string, object[]>()
for (const link of allLinks) {
const service = this.getLinkModuleOrThrow(link)
const { moduleA, moduleB, moduleBKey, primaryKeys } =
this.getLinkDataConfig(link)
if (!serviceLinks.has(service.__definition.key)) {
serviceLinks.set(service.__definition.key, [])
}
serviceLinks.get(service.__definition.key)?.push({
...link[moduleA],
...link[moduleB],
})
}
const promises: Promise<object[]>[] = []
for (const [serviceName, filters] of serviceLinks) {
const service = this.modulesMap.get(serviceName)!
promises.push(
service
.list({ $or: filters })
.then((links: any[]) =>
options?.asLinkDefinition
? convertRecordsToLinkDefinition(links, service)
: links
)
)
}
return (await promiseAll(promises)).flat()
}
}