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
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export { default as NotificationModuleService } from "./notification-module-service"
|
||||
export { default as NotificationProviderService } from "./notification-provider"
|
||||
@@ -0,0 +1,153 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Constructor, DAL, NotificationTypes } from "@medusajs/types"
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { NotificationProvider } from "@models"
|
||||
import { NotificationProviderRegistrationPrefix } from "@types"
|
||||
|
||||
type InjectedDependencies = {
|
||||
notificationProviderRepository: DAL.RepositoryService
|
||||
[
|
||||
key: `${typeof NotificationProviderRegistrationPrefix}${string}`
|
||||
]: NotificationTypes.INotificationProvider
|
||||
}
|
||||
|
||||
export default class NotificationProviderService extends ModulesSdkUtils.internalModuleServiceFactory<InjectedDependencies>(
|
||||
NotificationProvider
|
||||
) {
|
||||
protected readonly notificationProviderRepository_: DAL.RepositoryService<NotificationProvider>
|
||||
// We can store the providers in a memory since they can only be registered on startup and not changed during runtime
|
||||
protected providersCache: Map<string, NotificationProvider>
|
||||
|
||||
constructor(container: InjectedDependencies) {
|
||||
super(container)
|
||||
this.notificationProviderRepository_ =
|
||||
container.notificationProviderRepository
|
||||
}
|
||||
|
||||
protected retrieveProviderRegistration(
|
||||
providerId: string
|
||||
): NotificationTypes.INotificationProvider {
|
||||
try {
|
||||
return this.__container__[
|
||||
`${NotificationProviderRegistrationPrefix}${providerId}`
|
||||
]
|
||||
} catch (err) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Could not find a notification provider with id: ${providerId}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async getProviderForChannel(
|
||||
channel: string
|
||||
): Promise<NotificationProvider | undefined> {
|
||||
if (!this.providersCache) {
|
||||
const providers = await this.notificationProviderRepository_.find()
|
||||
this.providersCache = new Map(
|
||||
providers.flatMap((provider) =>
|
||||
provider.channels.map((c) => [c, provider])
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return this.providersCache.get(channel)
|
||||
}
|
||||
|
||||
async send(
|
||||
provider: NotificationProvider,
|
||||
notification: NotificationTypes.ProviderSendNotificationDTO
|
||||
): Promise<NotificationTypes.ProviderSendNotificationResultsDTO> {
|
||||
const providerHandler = this.retrieveProviderRegistration(provider.id)
|
||||
return await providerHandler.send(notification)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user