chore: Migrate notification module to DML (#7835)
This commit is contained in:
@@ -10,9 +10,7 @@ import { medusaIntegrationTestRunner, TestEventUtils } from "medusa-test-utils"
|
||||
|
||||
jest.setTimeout(50000)
|
||||
|
||||
const env = { MEDUSA_FF_MEDUSA_V2: true }
|
||||
medusaIntegrationTestRunner({
|
||||
env,
|
||||
testSuite: ({ getContainer }) => {
|
||||
describe("Notifications", () => {
|
||||
let service: INotificationModuleService
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
} from "./decorators"
|
||||
import { DmlEntity, toMikroORMEntity } from "../dml"
|
||||
|
||||
type SelectorAndData = {
|
||||
selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
|
||||
@@ -35,12 +36,16 @@ type SelectorAndData = {
|
||||
}
|
||||
|
||||
export function MedusaInternalService<TContainer extends object = object>(
|
||||
model: any
|
||||
rawModel: any
|
||||
): {
|
||||
new <TEntity extends object = any>(
|
||||
container: TContainer
|
||||
): ModulesSdkTypes.IMedusaInternalService<TEntity, TContainer>
|
||||
} {
|
||||
const model = DmlEntity.isDmlEntity(rawModel)
|
||||
? toMikroORMEntity(rawModel)
|
||||
: rawModel
|
||||
|
||||
const injectedRepositoryName = `${lowerCaseFirst(model.name)}Repository`
|
||||
const propertyRepositoryName = `__${injectedRepositoryName}__`
|
||||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import * as entities from "./src/models"
|
||||
import { TSMigrationGenerator } from "@medusajs/utils"
|
||||
import { defineMikroOrmCliConfig } from "@medusajs/utils"
|
||||
|
||||
module.exports = {
|
||||
module.exports = defineMikroOrmCliConfig({
|
||||
entities: Object.values(entities),
|
||||
schema: "public",
|
||||
clientUrl: "postgres://postgres@localhost/medusa-notification",
|
||||
type: "postgresql",
|
||||
migrations: {
|
||||
generator: TSMigrationGenerator,
|
||||
},
|
||||
}
|
||||
databaseName: "medusa-notification",
|
||||
})
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { moduleProviderLoader } from "@medusajs/modules-sdk"
|
||||
import { LoaderOptions, ModuleProvider, ModulesSdkTypes } from "@medusajs/types"
|
||||
import {
|
||||
DAL,
|
||||
InferEntityType,
|
||||
LoaderOptions,
|
||||
ModuleProvider,
|
||||
ModulesSdkTypes,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
lowerCaseFirst,
|
||||
@@ -62,8 +68,9 @@ async function syncDatabaseProviders({
|
||||
const providerServiceRegistrationKey = lowerCaseFirst(
|
||||
NotificationProviderService.name
|
||||
)
|
||||
const providerService: ModulesSdkTypes.IMedusaInternalService<NotificationProvider> =
|
||||
container.resolve(providerServiceRegistrationKey)
|
||||
const providerService: ModulesSdkTypes.IMedusaInternalService<
|
||||
typeof NotificationProvider
|
||||
> = container.resolve(providerServiceRegistrationKey)
|
||||
|
||||
const logger = container.resolve(ContainerRegistrationKeys.LOGGER) ?? console
|
||||
const normalizedProviders = providers.map((provider) => {
|
||||
@@ -106,7 +113,10 @@ async function syncDatabaseProviders({
|
||||
if (providersToDisable.length) {
|
||||
promises.push(
|
||||
providerService.update(
|
||||
providersToDisable.map((p) => ({ id: p.id, is_enabled: false }))
|
||||
providersToDisable.map((p) => ({
|
||||
entity: p,
|
||||
update: { is_enabled: false },
|
||||
}))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,40 @@
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "'{}'",
|
||||
"mappedType": "array"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "notification_provider",
|
||||
@@ -198,6 +231,27 @@
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"default": "now()",
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "notification",
|
||||
@@ -209,15 +263,15 @@
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_notification_receiver_id\" ON \"notification\" (receiver_id)"
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_notification_receiver_id\" ON \"notification\" (receiver_id) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_notification_idempotency_key",
|
||||
"keyName": "IDX_notification_idempotency_key_unique",
|
||||
"columnNames": [],
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_notification_idempotency_key\" ON \"notification\" (idempotency_key)"
|
||||
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_notification_idempotency_key_unique\" ON \"notification\" (idempotency_key) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_notification_provider_id",
|
||||
@@ -225,7 +279,7 @@
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_notification_provider_id\" ON \"notification\" (provider_id)"
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_notification_provider_id\" ON \"notification\" (provider_id) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "notification_pkey",
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Migration } from "@mikro-orm/migrations"
|
||||
|
||||
export class Migration20240628075401 extends Migration {
|
||||
async up(): Promise<void> {
|
||||
this.addSql(
|
||||
'alter table if exists "notification_provider" add column if not exists "created_at" timestamptz not null default now(), add column "updated_at" timestamptz not null default now(), add column "deleted_at" timestamptz null;'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table if exists "notification_provider" alter column "channels" type text[] using ("channels"::text[]);'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table if exists "notification_provider" alter column "channels" set default \'{}\';'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table if exists "notification" add column if not exists "updated_at" timestamptz not null default now(), add column "deleted_at" timestamptz null;'
|
||||
)
|
||||
this.addSql('drop index if exists "IDX_notification_idempotency_key";')
|
||||
this.addSql(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_notification_idempotency_key_unique" ON "notification" (idempotency_key) WHERE deleted_at IS NULL;'
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
export { default as Notification } from "./notification"
|
||||
export { default as NotificationProvider } from "./notification-provider"
|
||||
|
||||
export { Notification } from "./notification"
|
||||
export { NotificationProvider } from "./notification-provider"
|
||||
|
||||
@@ -1,46 +1,11 @@
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import {
|
||||
ArrayType,
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
OnInit,
|
||||
OneToMany,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import Notification from "./notification"
|
||||
import { model } from "@medusajs/utils"
|
||||
import { Notification } from "./notification"
|
||||
|
||||
@Entity()
|
||||
export default class NotificationProvider {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
handle: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
name: string
|
||||
|
||||
@Property({ columnType: "boolean", defaultRaw: "true" })
|
||||
is_enabled: boolean = true
|
||||
|
||||
@Property({ type: ArrayType })
|
||||
channels: string[]
|
||||
|
||||
@OneToMany({
|
||||
entity: () => Notification,
|
||||
mappedBy: (notification) => notification.provider_id,
|
||||
})
|
||||
notifications = new Collection<Notification>(this)
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "notpro")
|
||||
}
|
||||
|
||||
@OnInit()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "notpro")
|
||||
}
|
||||
}
|
||||
export const NotificationProvider = model.define("notificationProvider", {
|
||||
id: model.id({ prefix: "notpro" }),
|
||||
handle: model.text(),
|
||||
name: model.text(),
|
||||
is_enabled: model.boolean().default(true),
|
||||
channels: model.array().default([]),
|
||||
notifications: model.hasMany(() => Notification, { mappedBy: "provider" }),
|
||||
})
|
||||
|
||||
@@ -1,109 +1,30 @@
|
||||
import {
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import NotificationProvider from "./notification-provider"
|
||||
|
||||
const NotificationProviderIdIndex = createPsqlIndexStatementHelper({
|
||||
tableName: "notification",
|
||||
columns: "provider_id",
|
||||
})
|
||||
|
||||
const NotificationIdempotencyKeyIndex = createPsqlIndexStatementHelper({
|
||||
tableName: "notification",
|
||||
columns: "idempotency_key",
|
||||
})
|
||||
|
||||
const NotificationReceiverIdIndex = createPsqlIndexStatementHelper({
|
||||
tableName: "notification",
|
||||
columns: "receiver_id",
|
||||
})
|
||||
|
||||
// We don't need to support soft deletes here as this information is mainly used for auditing purposes.
|
||||
// Instead, we probably want to have a TTL for each entry, so we don't bloat the DB (and also for GDPR reasons if TTL < 30 days).
|
||||
@NotificationProviderIdIndex.MikroORMIndex()
|
||||
@NotificationIdempotencyKeyIndex.MikroORMIndex()
|
||||
@NotificationReceiverIdIndex.MikroORMIndex()
|
||||
@Entity({ tableName: "notification" })
|
||||
// Since there is a native `Notification` type, we have to call this something else here and in a couple of other places.
|
||||
export default class Notification {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id: string
|
||||
import { model } from "@medusajs/utils"
|
||||
import { NotificationProvider } from "./notification-provider"
|
||||
|
||||
// We probably want to have a TTL for each entry, so we don't bloat the DB (and also for GDPR reasons if TTL < 30 days).
|
||||
export const Notification = model.define("notification", {
|
||||
id: model.id({ prefix: "noti" }),
|
||||
// This can be an email, phone number, or username, depending on the channel.
|
||||
@Property({ columnType: "text" })
|
||||
to: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
channel: string
|
||||
|
||||
to: model.text(),
|
||||
channel: model.text(),
|
||||
// The template name in the provider's system.
|
||||
@Property({ columnType: "text" })
|
||||
template: string
|
||||
|
||||
template: model.text(),
|
||||
// The data that gets passed over to the provider for rendering the notification.
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
data: Record<string, unknown> | null
|
||||
|
||||
data: model.json().nullable(),
|
||||
// This can be the event name, the workflow, or anything else that can help to identify what triggered the notification.
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
trigger_type?: string | null
|
||||
|
||||
trigger_type: model.text().nullable(),
|
||||
// The ID of the resource this notification is for, if applicable. Useful for displaying relevant information in the UI
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
resource_id?: string | null
|
||||
|
||||
resource_id: model.text().nullable(),
|
||||
// The typeame of the resource this notification is for, if applicable, eg. "order"
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
resource_type?: string | null
|
||||
|
||||
resource_type: model.text().nullable(),
|
||||
// The ID of the receiver of the notification, if applicable. This can be a customer, user, a company, or anything else.
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
receiver_id?: string | null
|
||||
|
||||
receiver_id: model.text().index().nullable(),
|
||||
// The original notification, in case this is a retried notification.
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
original_notification_id?: string | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
idempotency_key?: string | null
|
||||
|
||||
original_notification_id: model.text().nullable(),
|
||||
idempotency_key: model.text().unique().nullable(),
|
||||
// The ID of the notification in the external system, if applicable
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
external_id?: string | null
|
||||
|
||||
@ManyToOne(() => NotificationProvider, {
|
||||
columnType: "text",
|
||||
fieldName: "provider_id",
|
||||
mapToPk: true,
|
||||
nullable: true,
|
||||
onDelete: "set null",
|
||||
})
|
||||
provider_id: string
|
||||
|
||||
@ManyToOne(() => NotificationProvider, { persist: false })
|
||||
provider: NotificationProvider
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@BeforeCreate()
|
||||
@OnInit()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "noti")
|
||||
this.provider_id ??= this.provider_id ?? this.provider?.id
|
||||
}
|
||||
}
|
||||
external_id: model.text().nullable(),
|
||||
provider: model
|
||||
.belongsTo(() => NotificationProvider, { mappedBy: "notifications" })
|
||||
.nullable(),
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ModuleJoinerConfig,
|
||||
ModulesSdkTypes,
|
||||
NotificationTypes,
|
||||
InferEntityType,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
InjectManager,
|
||||
@@ -21,7 +22,9 @@ import NotificationProviderService from "./notification-provider"
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
notificationService: ModulesSdkTypes.IMedusaInternalService<any>
|
||||
notificationService: ModulesSdkTypes.IMedusaInternalService<
|
||||
typeof Notification
|
||||
>
|
||||
notificationProviderService: NotificationProviderService
|
||||
}
|
||||
|
||||
@@ -32,7 +35,9 @@ export default class NotificationModuleService
|
||||
implements INotificationModuleService
|
||||
{
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
protected readonly notificationService_: ModulesSdkTypes.IMedusaInternalService<Notification>
|
||||
protected readonly notificationService_: ModulesSdkTypes.IMedusaInternalService<
|
||||
typeof Notification
|
||||
>
|
||||
protected readonly notificationProviderService_: NotificationProviderService
|
||||
|
||||
constructor(
|
||||
@@ -91,7 +96,7 @@ export default class NotificationModuleService
|
||||
protected async createNotifications_(
|
||||
data: NotificationTypes.CreateNotificationDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<Notification[]> {
|
||||
): Promise<InferEntityType<typeof Notification>[]> {
|
||||
if (!data.length) {
|
||||
return []
|
||||
}
|
||||
@@ -108,12 +113,13 @@ export default class NotificationModuleService
|
||||
{ take: null },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const existsMap = new Map(
|
||||
alreadySentNotifications.map((n) => [n.idempotency_key, true])
|
||||
alreadySentNotifications.map((n) => [n.idempotency_key as string, true])
|
||||
)
|
||||
|
||||
const notificationsToProcess = data.filter(
|
||||
(entry) => !existsMap.has(entry.idempotency_key)
|
||||
(entry) => !entry.idempotency_key || !existsMap.has(entry.idempotency_key)
|
||||
)
|
||||
|
||||
const notificationsToCreate = await promiseAll(
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { DAL, NotificationTypes } from "@medusajs/types"
|
||||
import { DAL, InferEntityType, NotificationTypes } from "@medusajs/types"
|
||||
import { MedusaError, ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { NotificationProvider } from "@models"
|
||||
import { NotificationProviderRegistrationPrefix } from "@types"
|
||||
|
||||
type InjectedDependencies = {
|
||||
notificationProviderRepository: DAL.RepositoryService
|
||||
notificationProviderRepository: DAL.RepositoryService<
|
||||
InferEntityType<typeof NotificationProvider>
|
||||
>
|
||||
[
|
||||
key: `${typeof NotificationProviderRegistrationPrefix}${string}`
|
||||
]: NotificationTypes.INotificationProvider
|
||||
@@ -13,9 +15,14 @@ type InjectedDependencies = {
|
||||
export default class NotificationProviderService extends ModulesSdkUtils.MedusaInternalService<InjectedDependencies>(
|
||||
NotificationProvider
|
||||
) {
|
||||
protected readonly notificationProviderRepository_: DAL.RepositoryService<NotificationProvider>
|
||||
protected readonly notificationProviderRepository_: DAL.RepositoryService<
|
||||
InferEntityType<typeof 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>
|
||||
protected providersCache: Map<
|
||||
string,
|
||||
InferEntityType<typeof NotificationProvider>
|
||||
>
|
||||
|
||||
constructor(container: InjectedDependencies) {
|
||||
super(container)
|
||||
@@ -40,7 +47,7 @@ export default class NotificationProviderService extends ModulesSdkUtils.MedusaI
|
||||
|
||||
async getProviderForChannel(
|
||||
channel: string
|
||||
): Promise<NotificationProvider | undefined> {
|
||||
): Promise<InferEntityType<typeof NotificationProvider> | undefined> {
|
||||
if (!this.providersCache) {
|
||||
const providers = await this.notificationProviderRepository_.find()
|
||||
this.providersCache = new Map(
|
||||
@@ -54,7 +61,7 @@ export default class NotificationProviderService extends ModulesSdkUtils.MedusaI
|
||||
}
|
||||
|
||||
async send(
|
||||
provider: NotificationProvider,
|
||||
provider: InferEntityType<typeof NotificationProvider>,
|
||||
notification: NotificationTypes.ProviderSendNotificationDTO
|
||||
): Promise<NotificationTypes.ProviderSendNotificationResultsDTO> {
|
||||
const providerHandler = this.retrieveProviderRegistration(provider.id)
|
||||
|
||||
Reference in New Issue
Block a user