feat(medusa): Migrate NotificationService to TS + add NotificationInterface (#1922)

This commit is contained in:
Adrien de Peretti
2022-07-27 20:07:57 +02:00
committed by GitHub
parent 902af55723
commit 6014872a72
8 changed files with 355 additions and 271 deletions
@@ -7,6 +7,8 @@ import {
defaultAdminNotificationsFields,
defaultAdminNotificationsRelations,
} from "./"
import { Notification } from "../../../../models"
import { FindConfig } from "../../../../types/common"
/**
* @oas [get] /notifications
@@ -91,20 +93,23 @@ export default async (req, res) => {
}
const listConfig = {
select: includeFields.length
select: (includeFields.length
? includeFields
: defaultAdminNotificationsFields,
: defaultAdminNotificationsFields) as (keyof Notification)[],
relations: expandFields.length
? expandFields
: defaultAdminNotificationsRelations,
skip: offset,
take: limit,
order: { created_at: "DESC" },
}
} as FindConfig<Notification>
const notifications = await notificationService.list(selector, listConfig)
const resultFields = [...listConfig.select, ...listConfig.relations]
const resultFields = [
...(listConfig.select ?? []),
...(listConfig.relations ?? []),
]
const data = notifications.map((o) => pick(o, resultFields))
res.json({ notifications: data })
@@ -6,6 +6,7 @@ import {
} from "."
import { validator } from "../../../../utils/validator"
import { NotificationService } from "../../../../services"
import { Notification } from "../../../../models"
/**
* @oas [post] /notifications/{id}/resend
@@ -56,7 +57,7 @@ export default async (req, res) => {
await notificationService.resend(id, config)
const notification = await notificationService.retrieve(id, {
select: defaultAdminNotificationsFields,
select: defaultAdminNotificationsFields as (keyof Notification)[],
relations: defaultAdminNotificationsRelations,
})
+2
View File
@@ -4,5 +4,7 @@ export * from "./tax-service"
export * from "./transaction-base-service"
export * from "./batch-job-strategy"
export * from "./file-service"
export * from "./notification-service"
export * from "./price-selection-strategy"
export * from "./models/base-entity"
export * from "./models/soft-deletable-entity"
@@ -0,0 +1,55 @@
import { TransactionBaseService } from "./transaction-base-service"
import BaseNotificationService from "medusa-interfaces/dist/notification-service"
type ReturnedData = {
to: string
status: string
data: Record<string, unknown>
}
export interface INotificationService<T extends TransactionBaseService<never>>
extends TransactionBaseService<T> {
sendNotification(
event: string,
data: unknown,
attachmentGenerator: unknown
): Promise<ReturnedData>
resendNotification(
notification: unknown,
config: unknown,
attachmentGenerator: unknown
): Promise<ReturnedData>
}
export abstract class AbstractNotificationService<
T extends TransactionBaseService<never>
>
extends TransactionBaseService<T>
implements INotificationService<T>
{
static identifier: string
getIdentifier(): string {
return (this.constructor as any).identifier
}
abstract sendNotification(
event: string,
data: unknown,
attachmentGenerator: unknown
): Promise<ReturnedData>
abstract resendNotification(
notification: unknown,
config: unknown,
attachmentGenerator: unknown
): Promise<ReturnedData>
}
export const isNotificationService = (obj: unknown): boolean => {
return (
obj instanceof AbstractNotificationService ||
obj instanceof BaseNotificationService
)
}
+4 -4
View File
@@ -5,7 +5,6 @@ import {
BaseService as LegacyBaseService,
PaymentService,
FulfillmentService,
NotificationService,
FileService,
OauthService,
SearchService,
@@ -21,6 +20,9 @@ import {
isFileService,
isTaxCalculationStrategy,
TransactionBaseService as BaseService,
isNotificationService,
isBatchJobStrategy,
isPriceSelectionStrategy,
} from "../interfaces"
import formatRegistrationName from "../utils/format-registration-name"
import {
@@ -30,8 +32,6 @@ import {
MedusaContainer,
} from "../types/global"
import { MiddlewareService } from "../services"
import { isBatchJobStrategy } from "../interfaces/batch-job-strategy"
import { isPriceSelectionStrategy } from "../interfaces/price-selection-strategy"
import logger from "./logger"
type Options = {
@@ -392,7 +392,7 @@ export async function registerServices(
).singleton(),
[`fp_${loaded.identifier}`]: aliasTo(name),
})
} else if (loaded.prototype instanceof NotificationService) {
} else if (isNotificationService(loaded.prototype)) {
container.registerAdd(
"notificationProviders",
asFunction((cradle) => new loaded(cradle, pluginDetails.options))
+1 -1
View File
@@ -29,7 +29,7 @@ export class Notification extends BaseEntity {
@Index()
@Column({ nullable: true })
customer_id: string
customer_id: string | null
@ManyToOne(() => Customer)
@JoinColumn({ name: "customer_id" })
@@ -1,261 +0,0 @@
import { MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
/**
* Provides layer to manipulate orchestrate notifications.
* @extends BaseService
*/
class NotificationService extends BaseService {
constructor(container) {
super()
const {
manager,
notificationProviderRepository,
notificationRepository,
logger,
} = container
this.container_ = container
/** @private @const {EntityManager} */
this.manager_ = manager
this.logger_ = logger
/** @private @const {NotificationRepository} */
this.notificationRepository_ = notificationRepository
this.notificationProviderRepository_ = notificationProviderRepository
this.subscribers_ = {}
this.attachmentGenerator_ = null
}
/**
* Registers an attachment generator to the service. The generator can be
* used to generate on demand invoices or other documents.
* @param {object} service
*/
registerAttachmentGenerator(service) {
this.attachmentGenerator_ = service
}
/**
* Sets the service's manager to a given transaction manager.
* @param {EntityManager} transactionManager - the manager to use
* @return {NotificationService} a cloned notification service
*/
withTransaction(transactionManager) {
if (!transactionManager) {
return this
}
const cloned = new NotificationService({
manager: transactionManager,
notificationProviderRepository: this.notificationProviderRepository_,
notificationRepository: this.notificationRepository_,
logger: this.logger_,
})
cloned.transactionManager_ = transactionManager
return cloned
}
/**
* Takes a list of notification provider ids and persists them in the database.
* @param {Array<string>} providers - a list of provider ids
*/
async registerInstalledProviders(providers) {
const { manager, notificationProviderRepository } = this.container_
const model = manager.getCustomRepository(notificationProviderRepository)
model.update({}, { is_installed: false })
for (const p of providers) {
const n = model.create({ id: p, is_installed: true })
await model.save(n)
}
}
/**
* Retrieves a list of notifications.
* @param {object} selector - the params to select the notifications by.
* @param {object} config - the configuration to apply to the query
* @return {Array<Notification>} the notifications that satisfy the query.
*/
async list(
selector,
config = { skip: 0, take: 50, order: { created_at: "DESC" } }
) {
const notiRepo = this.manager_.getCustomRepository(
this.notificationRepository_
)
const query = this.buildQuery_(selector, config)
return notiRepo.find(query)
}
/**
* Retrieves a notification with a given id
* @param {string} id - the id of the notification
* @param {object} config - the configuration to apply to the query
* @return {Notification} the notification
*/
async retrieve(id, config = {}) {
const notiRepository = this.manager_.getCustomRepository(
this.notificationRepository_
)
const validatedId = this.validateId_(id)
const query = this.buildQuery_({ id: validatedId }, config)
const notification = await notiRepository.findOne(query)
if (!notification) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Notification with id: ${id} was not found.`
)
}
return notification
}
/**
* Subscribes a given provider to an event.
* @param {string} eventName - the event to subscribe to
* @param {string} providerId - the provider that the event will be sent to
*/
subscribe(eventName, providerId) {
if (typeof providerId !== "string") {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"providerId must be a string"
)
}
if (this.subscribers_[eventName]) {
this.subscribers_[eventName].push(providerId)
} else {
this.subscribers_[eventName] = [providerId]
}
}
/**
* Finds a provider with a given id. Will throw a NOT_FOUND error if the
* resolution fails.
* @param {string} id - the id of the provider
* @return {NotificationProvider} the notification provider
*/
retrieveProvider_(id) {
try {
return this.container_[`noti_${id}`]
} catch (err) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Could not find a notification provider with id: ${id}.`
)
}
}
/**
* Handles an event by relaying the event data to the subscribing providers.
* The result of the notification send will be persisted in the database in
* order to allow for resends. Will log any errors that are encountered.
* @param {string} eventName - the event to handle
* @param {object} data - the data the event was sent with
* @return {Promise} - the result of notification subscribed
*/
handleEvent(eventName, data) {
const subs = this.subscribers_[eventName]
if (!subs) {
return Promise.resolve()
}
if (data["no_notification"] === true) {
return
}
return Promise.all(
subs.map(async (providerId) => {
return this.send(eventName, data, providerId).catch((err) => {
console.log(err)
this.logger_.warn(
`An error occured while ${providerId} was processing a notification for ${eventName}: ${err.message}`
)
})
})
)
}
/**
* Sends a notification, by calling the given provider's sendNotification
* method. Persists the Notification in the database.
* @param {string} event - the name of the event
* @param {object} eventData - the data the event was sent with
* @param {string} providerId - the provider that should hande the event.
* @return {Notification} the created notification
*/
async send(event, eventData, providerId) {
const provider = this.retrieveProvider_(providerId)
const result = await provider.sendNotification(
event,
eventData,
this.attachmentGenerator_
)
if (!result) {
return
}
const { to, data } = result
const notiRepo = this.manager_.getCustomRepository(
this.notificationRepository_
)
const [resource_type] = event.split(".")
const resource_id = eventData.id
const customer_id = eventData.customer_id || null
const created = notiRepo.create({
resource_type,
resource_id,
customer_id,
to,
data,
event_name: event,
provider_id: providerId,
})
return notiRepo.save(created)
}
/**
* Resends a notification by retrieving a prior notification and calling the
* underlying provider's resendNotification method.
* @param {string} id - the id of the notification
* @param {object} config - any configuration that might override the previous
* send
* @return {Notification} the newly created notification
*/
async resend(id, config = {}) {
const notification = await this.retrieve(id)
const provider = this.retrieveProvider_(notification.provider_id)
const { to, data } = await provider.resendNotification(
notification,
config,
this.attachmentGenerator_
)
const notiRepo = this.manager_.getCustomRepository(
this.notificationRepository_
)
const created = notiRepo.create({
...notification,
to,
data,
parent_id: id,
})
return notiRepo.save(created)
}
}
export default NotificationService
@@ -0,0 +1,282 @@
import { MedusaError } from "medusa-core-utils"
import {
AbstractNotificationService,
TransactionBaseService,
} from "../interfaces"
import { EntityManager } from "typeorm"
import { Logger } from "../types/global"
import { NotificationRepository } from "../repositories/notification"
import { NotificationProviderRepository } from "../repositories/notification-provider"
import { FindConfig, Selector } from "../types/common"
import { buildQuery } from "../utils"
import { Notification } from "../models"
type InjectedDependencies = {
manager: EntityManager
logger: Logger
notificationRepository: typeof NotificationRepository
notificationProviderRepository: typeof NotificationProviderRepository
}
type NotificationProviderKey = `noti_${string}`
class NotificationService extends TransactionBaseService<NotificationService> {
protected manager_: EntityManager
protected transactionManager_: EntityManager | undefined
protected subscribers_ = {}
protected attachmentGenerator_: unknown = null
protected readonly container_: InjectedDependencies & {
[key in `${NotificationProviderKey}`]: AbstractNotificationService<never>
}
protected readonly logger_: Logger
protected readonly notificationRepository_: typeof NotificationRepository
protected readonly notificationProviderRepository_: typeof NotificationProviderRepository
constructor(container: InjectedDependencies) {
super(container)
const {
manager,
notificationProviderRepository,
notificationRepository,
logger,
} = container
this.container_ = container
/** @private @const {EntityManager} */
this.manager_ = manager
this.logger_ = logger
/** @private @const {NotificationRepository} */
this.notificationRepository_ = notificationRepository
this.notificationProviderRepository_ = notificationProviderRepository
}
/**
* Registers an attachment generator to the service. The generator can be
* used to generate on demand invoices or other documents.
* @param service the service to assign to the attachmentGenerator
*/
registerAttachmentGenerator(service: unknown): void {
this.attachmentGenerator_ = service
}
/**
* Takes a list of notification provider ids and persists them in the database.
* @param providerIds - a list of provider ids
*/
async registerInstalledProviders(providerIds: string[]): Promise<void> {
const { manager, notificationProviderRepository } = this.container_
const model = manager.getCustomRepository(notificationProviderRepository)
await model.update({}, { is_installed: false })
for (const id of providerIds) {
const n = model.create({ id, is_installed: true })
await model.save(n)
}
}
/**
* Retrieves a list of notifications.
* @param selector - the params to select the notifications by.
* @param config - the configuration to apply to the query
* @return the notifications that satisfy the query.
*/
async list(
selector: Selector<Notification>,
config: FindConfig<Notification> = {
skip: 0,
take: 50,
order: { created_at: "DESC" },
}
): Promise<Notification[]> {
const notiRepo = this.manager_.getCustomRepository(
this.notificationRepository_
)
const query = buildQuery(selector, config)
return await notiRepo.find(query)
}
/**
* Retrieves a notification with a given id
* @param id - the id of the notification
* @param config - the configuration to apply to the query
* @return the notification
*/
async retrieve(
id: string,
config: FindConfig<Notification> = {}
): Promise<Notification | never> {
const notiRepository = this.manager_.getCustomRepository(
this.notificationRepository_
)
const query = buildQuery({ id }, config)
const notification = await notiRepository.findOne(query)
if (!notification) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Notification with id: ${id} was not found.`
)
}
return notification
}
/**
* Subscribes a given provider to an event.
* @param eventName - the event to subscribe to
* @param providerId - the provider that the event will be sent to
*/
subscribe(eventName: string, providerId: string): void {
if (typeof providerId !== "string") {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"providerId must be a string"
)
}
if (this.subscribers_[eventName]) {
this.subscribers_[eventName].push(providerId)
} else {
this.subscribers_[eventName] = [providerId]
}
}
/**
* Finds a provider with a given id. Will throw a NOT_FOUND error if the
* resolution fails.
* @param id - the id of the provider
* @return the notification provider
*/
protected retrieveProvider_(id: string): AbstractNotificationService<never> {
try {
return this.container_[`noti_${id}`]
} catch (err) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Could not find a notification provider with id: ${id}.`
)
}
}
/**
* Handles an event by relaying the event data to the subscribing providers.
* The result of the notification send will be persisted in the database in
* order to allow for resends. Will log any errors that are encountered.
* @param eventName - the event to handle
* @param data - the data the event was sent with
* @return the result of notification subscribed
*/
handleEvent(
eventName: string,
data: Record<string, unknown>
): Promise<void | undefined | Notification[]> {
const subs = this.subscribers_[eventName]
if (!subs) {
return Promise.resolve()
}
if (data["no_notification"] === true) {
return Promise.resolve()
}
return Promise.all(
subs.map(async (providerId) => {
return this.send(eventName, data, providerId).catch((err) => {
console.log(err)
this.logger_.warn(
`An error occured while ${providerId} was processing a notification for ${eventName}: ${err.message}`
)
})
})
)
}
/**
* Sends a notification, by calling the given provider's sendNotification
* method. Persists the Notification in the database.
* @param event - the name of the event
* @param eventData - the data the event was sent with
* @param providerId - the provider that should hande the event.
* @return the created notification
*/
async send(
event: string,
eventData: Record<string, unknown>,
providerId: string
): Promise<Notification | undefined> {
return await this.atomicPhase_(async (transactionManager) => {
const provider = this.retrieveProvider_(providerId)
const result = await provider.sendNotification(
event,
eventData,
this.attachmentGenerator_
)
if (!result) {
return
}
const { to, data } = result
const notiRepo = transactionManager.getCustomRepository(
this.notificationRepository_
)
const [resource_type] = event.split(".") as string[]
const resource_id = eventData.id as string
const customer_id = (eventData.customer_id as string) || null
const created = notiRepo.create({
resource_type,
resource_id,
customer_id,
to,
data,
event_name: event,
provider_id: providerId,
})
return await notiRepo.save(created)
})
}
/**
* Resends a notification by retrieving a prior notification and calling the
* underlying provider's resendNotification method.
* @param {string} id - the id of the notification
* @param {object} config - any configuration that might override the previous
* send
* @return {Notification} the newly created notification
*/
async resend(
id: string,
config: FindConfig<Notification> = {}
): Promise<Notification> {
return await this.atomicPhase_(async (transactionManager) => {
const notification = await this.retrieve(id)
const provider = this.retrieveProvider_(notification.provider_id)
const { to, data } = await provider.resendNotification(
notification,
config,
this.attachmentGenerator_
)
const notiRepo = transactionManager.getCustomRepository(
this.notificationRepository_
)
const created = notiRepo.create({
...notification,
to,
data,
parent_id: id,
})
return notiRepo.save(created)
})
}
}
export default NotificationService