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 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 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 = {} ) { 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 { const filter = this.buildData(primaryKeyData, foreignKeyData) const queryOptions = ModulesSdkUtils.buildQuery(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 = {}, config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise { 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 = {}, config: FindConfig = {}, @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][], foreignKeyData?: string, extraFields?: Record, @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 ) ) } } 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 { 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 | 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>( 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]> { return await this.linkService_.softDelete(data, sharedContext) } @InjectTransactionManager() @EmitEvents() async restore( data: any, { returnLinkableKeys }: RestoreReturn = {}, @MedusaContext() sharedContext: Context = {} ): Promise | 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>( 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]> { return await this.linkService_.restore(data, sharedContext) } protected async emitEvents_(groupedEvents) { if (!this.eventBusModuleService_ || !groupedEvents) { return } const promises: Promise[] = [] for (const group of Object.keys(groupedEvents)) { promises.push( this.eventBusModuleService_.emit(groupedEvents[group], { internal: true, }) ) } await Promise.all(promises) } }