Files
medusa-store/packages/modules/notification/src/services/notification-module-service.ts
Stevche Radevski 144e09e852 feat: Add the basic implementation of notification module (#7282)
* feat: Add the basic implementation of notification module

* fix: Minor fixes and introduction of idempotency key

* fix: Changes based on PR review
2024-05-10 11:22:03 +02:00

154 lines
4.8 KiB
TypeScript

import {
Context,
DAL,
NotificationTypes,
INotificationModuleService,
InternalModuleDeclaration,
ModuleJoinerConfig,
ModulesSdkTypes,
} from "@medusajs/types"
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
ModulesSdkUtils,
promiseAll,
MedusaError,
} from "@medusajs/utils"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import NotificationProviderService from "./notification-provider"
import { NotificationModel, NotificationProvider } from "@models"
const generateMethodForModels = [NotificationProvider]
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
notificationModelService: ModulesSdkTypes.InternalModuleService<any>
notificationProviderService: NotificationProviderService
}
export default class NotificationModuleService<
TEntity extends NotificationModel = NotificationModel
>
extends ModulesSdkUtils.abstractModuleServiceFactory<
InjectedDependencies,
NotificationTypes.NotificationDTO,
{
NotificationProvider: { dto: NotificationTypes.NotificationProviderDTO }
}
>(NotificationModel, generateMethodForModels, entityNameToLinkableKeysMap)
implements INotificationModuleService
{
protected baseRepository_: DAL.RepositoryService
protected readonly notificationService_: ModulesSdkTypes.InternalModuleService<TEntity>
protected readonly notificationProviderService_: NotificationProviderService
constructor(
{
baseRepository,
notificationModelService,
notificationProviderService,
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
// @ts-ignore
super(...arguments)
this.baseRepository_ = baseRepository
this.notificationService_ = notificationModelService
this.notificationProviderService_ = notificationProviderService
}
__joinerConfig(): ModuleJoinerConfig {
return joinerConfig
}
create(
data: NotificationTypes.CreateNotificationDTO[],
sharedContext?: Context
): Promise<NotificationTypes.NotificationDTO[]>
create(
data: NotificationTypes.CreateNotificationDTO,
sharedContext?: Context
): Promise<NotificationTypes.NotificationDTO>
@InjectManager("baseRepository_")
async create(
data:
| NotificationTypes.CreateNotificationDTO
| NotificationTypes.CreateNotificationDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<
NotificationTypes.NotificationDTO | NotificationTypes.NotificationDTO[]
> {
const normalized = Array.isArray(data) ? data : [data]
const createdNotifications = await this.create_(normalized, sharedContext)
const serialized = await this.baseRepository_.serialize<
NotificationTypes.NotificationDTO[]
>(createdNotifications)
return Array.isArray(data) ? serialized : serialized[0]
}
@InjectTransactionManager("baseRepository_")
protected async create_(
data: NotificationTypes.CreateNotificationDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
if (!data.length) {
return []
}
// TODO: At this point we should probably take a lock with the idempotency keys so we don't have race conditions.
// Also, we should probably rely on Redis for this instead of the database.
const idempotencyKeys = data
.map((entry) => entry.idempotency_key)
.filter(Boolean)
const alreadySentNotifications = await this.notificationService_.list(
{
idempotency_key: idempotencyKeys,
},
{ take: null },
sharedContext
)
const existsMap = new Map(
alreadySentNotifications.map((n) => [n.idempotency_key, true])
)
const notificationsToProcess = data.filter(
(entry) => !existsMap.has(entry.idempotency_key)
)
const notificationsToCreate = await promiseAll(
notificationsToProcess.map(async (entry) => {
const provider =
await this.notificationProviderService_.getProviderForChannel(
entry.channel
)
if (!provider) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Could not find a notification provider for channel: ${entry.channel}`
)
}
const res = await this.notificationProviderService_.send(
provider,
entry
)
return { ...entry, provider_id: provider.id, external_id: res.id }
})
)
// Currently we store notifications after they are sent, which might result in a notification being sent that is not registered in the database.
// If necessary, we can switch to a two-step process where we first create the notification, send it, and update it after it being sent.
const createdNotifications = await this.notificationService_.create(
notificationsToCreate,
sharedContext
)
return createdNotifications
}
}