Files
medusa-store/packages/modules/link-modules/src/services/link-module-service.ts
2025-08-07 07:34:50 -03:00

416 lines
11 KiB
TypeScript

import {
Context,
DAL,
FindConfig,
IEventBusModuleService,
ILinkModule,
InternalModuleDeclaration,
ModuleJoinerConfig,
RestoreReturn,
SoftDeleteReturn,
} from "@medusajs/framework/types"
import {
CommonEvents,
EmitEvents,
InjectManager,
InjectTransactionManager,
isDefined,
mapObjectTo,
MapToConfig,
MedusaContext,
MedusaError,
moduleEventBuilderFactory,
Modules,
ModulesSdkUtils,
} from "@medusajs/framework/utils"
import { LinkService } from "@services"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
linkService: LinkService<any>
primaryKey: string | string[]
foreignKey: string
extraFields: string[]
entityName: string
serviceName: string
[Modules.EVENT_BUS]?: IEventBusModuleService
}
export default class LinkModuleService implements ILinkModule {
protected baseRepository_: DAL.RepositoryService
protected readonly linkService_: LinkService<any>
protected readonly eventBusModuleService_?: IEventBusModuleService
protected readonly entityName_: string
protected readonly serviceName_: string
protected primaryKey_: string[]
protected foreignKey_: string
protected extraFields_: string[]
constructor(
{
baseRepository,
linkService,
primaryKey,
foreignKey,
extraFields,
entityName,
serviceName,
[Modules.EVENT_BUS]: eventBusModuleService,
}: InjectedDependencies,
readonly moduleDeclaration: InternalModuleDeclaration
) {
this.baseRepository_ = baseRepository
this.linkService_ = linkService
this.eventBusModuleService_ = eventBusModuleService
this.primaryKey_ = !Array.isArray(primaryKey) ? [primaryKey] : primaryKey
this.foreignKey_ = foreignKey
this.extraFields_ = extraFields
this.entityName_ = entityName
this.serviceName_ = serviceName
}
__joinerConfig(): ModuleJoinerConfig {
return {} as ModuleJoinerConfig
}
private buildData(
primaryKeyData: string | string[],
foreignKeyData: string,
extra: Record<string, unknown> = {}
) {
if (this.primaryKey_.length > 1) {
if (
!Array.isArray(primaryKeyData) ||
primaryKeyData.length !== this.primaryKey_.length
) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Primary key data must be an array ${this.primaryKey_.length} values`
)
}
}
const pk = this.primaryKey_.join(",")
return {
[pk]: primaryKeyData,
[this.foreignKey_]: foreignKeyData,
...extra,
}
}
private isValidKeyName(name: string) {
return this.primaryKey_.concat(this.foreignKey_).includes(name)
}
private validateFields(data: any | any[]) {
const dataToValidate = Array.isArray(data) ? data : [data]
dataToValidate.forEach((d) => {
const keys = Object.keys(d)
if (keys.some((k) => !this.isValidKeyName(k))) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Invalid field name provided. Valid field names are ${this.primaryKey_.concat(
this.foreignKey_
)}`
)
}
})
}
@InjectManager()
async retrieve(
primaryKeyData: string | string[],
foreignKeyData: string,
@MedusaContext() sharedContext: Context = {}
): Promise<unknown> {
const filter = this.buildData(primaryKeyData, foreignKeyData)
const queryOptions = ModulesSdkUtils.buildQuery<unknown>(filter)
const entry = await this.linkService_.list(queryOptions, {}, sharedContext)
if (!entry?.length) {
const pk = this.primaryKey_.join(",")
const errMessage = `${pk}[${primaryKeyData}] and ${this.foreignKey_}[${foreignKeyData}]`
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Entry ${errMessage} was not found`
)
}
return (await this.baseRepository_.serialize(entry[0])) as unknown
}
@InjectManager()
async list(
filters: Record<string, unknown> = {},
config: FindConfig<unknown> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<unknown[]> {
if (!isDefined(config.take)) {
config.take = null
}
const rows = await this.linkService_.list(filters, config, sharedContext)
return (await this.baseRepository_.serialize(rows)) as unknown[]
}
@InjectManager()
async listAndCount(
filters: Record<string, unknown> = {},
config: FindConfig<unknown> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[unknown[], number]> {
if (!isDefined(config.take)) {
config.take = null
}
let [rows, count] = await this.linkService_.listAndCount(
filters,
config,
sharedContext
)
rows = (await this.baseRepository_.serialize(rows)) as unknown[]
return [rows, count]
}
@InjectTransactionManager()
@EmitEvents()
async create(
primaryKeyOrBulkData:
| string
| string[]
| [string | string[], string, Record<string, unknown>][],
foreignKeyData?: string,
extraFields?: Record<string, unknown>,
@MedusaContext() sharedContext: Context = {}
) {
const data: unknown[] = []
if (foreignKeyData === undefined && Array.isArray(primaryKeyOrBulkData)) {
for (const [primaryKey, foreignKey, extra] of primaryKeyOrBulkData) {
data.push(
this.buildData(
primaryKey as string | string[],
foreignKey as string,
extra as Record<string, unknown>
)
)
}
} else {
data.push(
this.buildData(
primaryKeyOrBulkData as string | string[],
foreignKeyData!,
extraFields
)
)
}
const links = await this.linkService_.create(data, sharedContext)
moduleEventBuilderFactory({
action: CommonEvents.ATTACHED,
object: this.entityName_,
source: this.serviceName_,
eventName: this.entityName_ + "." + CommonEvents.ATTACHED,
})({
data: data as { id: string }[],
sharedContext,
})
return (await this.baseRepository_.serialize(links)) as unknown[]
}
@InjectTransactionManager()
@EmitEvents()
async dismiss(
primaryKeyOrBulkData: string | string[] | [string | string[], string][],
foreignKeyData?: string,
@MedusaContext() sharedContext: Context = {}
) {
const data: unknown[] = []
if (foreignKeyData === undefined && Array.isArray(primaryKeyOrBulkData)) {
for (const [primaryKey, foreignKey] of primaryKeyOrBulkData) {
data.push(this.buildData(primaryKey, foreignKey as string))
}
} else {
data.push(
this.buildData(
primaryKeyOrBulkData as string | string[],
foreignKeyData!
)
)
}
const links = await this.linkService_.dismiss(data, sharedContext)
moduleEventBuilderFactory({
action: CommonEvents.DETACHED,
object: this.entityName_,
source: this.serviceName_,
eventName: this.entityName_ + "." + CommonEvents.DETACHED,
})({
data: links.map((link) => link.id),
sharedContext,
})
return (await this.baseRepository_.serialize(links)) as unknown[]
}
@InjectTransactionManager()
@EmitEvents()
async delete(
data: any,
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
this.validateFields(data)
await this.linkService_.delete(data, sharedContext)
const allData = Array.isArray(data) ? data : [data]
moduleEventBuilderFactory({
action: CommonEvents.DETACHED,
object: this.entityName_,
source: this.serviceName_,
eventName: this.entityName_ + "." + CommonEvents.DETACHED,
})({
data: allData as { id: string }[],
sharedContext,
})
}
@InjectTransactionManager()
@EmitEvents()
async softDelete(
data: any,
{ returnLinkableKeys }: SoftDeleteReturn = {},
@MedusaContext() sharedContext: Context = {}
): Promise<Record<string, unknown[]> | void> {
const inputArray = Array.isArray(data) ? data : [data]
this.validateFields(inputArray)
let [deletedEntities, cascadedEntitiesMap] = await this.softDelete_(
inputArray,
sharedContext
)
const pk = this.primaryKey_.join(",")
const entityNameToLinkableKeysMap: MapToConfig = {
LinkModel: [
{ mapTo: pk, valueFrom: pk },
{ mapTo: this.foreignKey_, valueFrom: this.foreignKey_ },
],
}
let mappedCascadedEntitiesMap
if (returnLinkableKeys) {
// Map internal table/column names to their respective external linkable keys
// eg: product.id = product_id, variant.id = variant_id
mappedCascadedEntitiesMap = mapObjectTo<Record<string, string[]>>(
cascadedEntitiesMap,
entityNameToLinkableKeysMap,
{
pick: returnLinkableKeys,
}
)
}
moduleEventBuilderFactory({
action: CommonEvents.DETACHED,
object: this.entityName_,
source: this.serviceName_,
eventName: this.entityName_ + "." + CommonEvents.DETACHED,
})({
data: deletedEntities as { id: string }[],
sharedContext,
})
return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
}
@InjectTransactionManager()
protected async softDelete_(
data: any[],
@MedusaContext() sharedContext: Context = {}
): Promise<[object[], Record<string, string[]>]> {
return await this.linkService_.softDelete(data, sharedContext)
}
@InjectTransactionManager()
@EmitEvents()
async restore(
data: any,
{ returnLinkableKeys }: RestoreReturn = {},
@MedusaContext() sharedContext: Context = {}
): Promise<Record<string, unknown[]> | void> {
const inputArray = Array.isArray(data) ? data : [data]
this.validateFields(inputArray)
let [restoredEntities, cascadedEntitiesMap] = await this.restore_(
inputArray,
sharedContext
)
const pk = this.primaryKey_.join(",")
const entityNameToLinkableKeysMap: MapToConfig = {
LinkModel: [
{ mapTo: pk, valueFrom: pk },
{ mapTo: this.foreignKey_, valueFrom: this.foreignKey_ },
],
}
let mappedCascadedEntitiesMap
if (returnLinkableKeys) {
// Map internal table/column names to their respective external linkable keys
// eg: product.id = product_id, variant.id = variant_id
mappedCascadedEntitiesMap = mapObjectTo<Record<string, string[]>>(
cascadedEntitiesMap,
entityNameToLinkableKeysMap,
{
pick: returnLinkableKeys,
}
)
}
moduleEventBuilderFactory({
action: CommonEvents.ATTACHED,
object: this.entityName_,
source: this.serviceName_,
eventName: this.entityName_ + "." + CommonEvents.ATTACHED,
})({
data: restoredEntities as { id: string }[],
sharedContext,
})
return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
}
@InjectTransactionManager()
async restore_(
data: any,
@MedusaContext() sharedContext: Context = {}
): Promise<[object[], Record<string, string[]>]> {
return await this.linkService_.restore(data, sharedContext)
}
protected async emitEvents_(groupedEvents) {
if (!this.eventBusModuleService_ || !groupedEvents) {
return
}
const promises: Promise<void>[] = []
for (const group of Object.keys(groupedEvents)) {
promises.push(
this.eventBusModuleService_.emit(groupedEvents[group], {
internal: true,
})
)
}
await Promise.all(promises)
}
}