Files
medusa-store/packages/modules/user/src/services/user-module.ts
Adrien de Peretti e8822f3e69 chore(): Module Internal Events (#13296)
* chore(): Ensure the product module emits all necessary events

* chore(): Ensure the product module emits all necessary events

* Update events tests

* more events and fixes

* more tests and category fixes

* more tests and category fixes

* Add todo

* update updateProduct_ event emitting and adjust test

* Adjust update products implementation to rely on already computed events

* rm unnecessary update variants events

* Fix formatting in changeset for product events

* refactor: Manage event emitting automatically (WIP)

* refactor: Manage event emitting automatically (WIP)

* chore(api-key): Add missing emit events and refactoring

* chore(cart): Add missing emit events and refactoring

* chore(customer): Add missing emit events and refactoring

* chore(fufillment, utils): Add missing emit events and refactoring

* chore(fufillment, utils): Add missing emit events and refactoring

* chore(inventory): Add missing emit events and refactoring

* chore(notification): Add missing emit events and refactoring

* chore(utils): Remove medusa service event handling legacy

* chore(product): Add missing emit events and refactoring

* chore(order): Add missing emit events and refactoring

* chore(payment): Add missing emit events and refactoring

* chore(pricing, util): Add missing emit events and refactoring, fix internal service upsertWithReplace event dispatching

* chore(promotions): Add missing emit events and refactoring

* chore(region): Add missing emit events and refactoring

* chore(sales-channel): Add missing emit events and refactoring

* chore(settings): Add missing emit events and refactoring

* chore(stock-location): Add missing emit events and refactoring

* chore(store): Add missing emit events and refactoring

* chore(taxes): Add missing emit events and refactoring

* chore(user): Add missing emit events and refactoring

* fix unit tests

* rm changeset for regeneration

* Create changeset for Medusa.js patch updates

Add a changeset for patch updates to multiple Medusa.js modules.

* rm unused product event builders

* address feedback

* remove old changeset

* fix event action for token generated

* fix user module events

* fix import

* fix promotion events

* add new module integration tests shard

* fix medusa service

* revert shard

* fix event action

* fix pipeline

* fix pipeline

---------

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
2025-09-10 14:37:38 +02:00

382 lines
10 KiB
TypeScript

import {
Context,
DAL,
InferEntityType,
InternalModuleDeclaration,
ModulesSdkTypes,
ProjectConfigOptions,
UserTypes,
} from "@medusajs/framework/types"
import {
arrayDifference,
CommonEvents,
EmitEvents,
generateEntityId,
generateJwtToken,
InjectManager,
InjectTransactionManager,
MedusaContext,
MedusaError,
MedusaService,
moduleEventBuilderFactory,
Modules,
UserEvents,
} from "@medusajs/framework/utils"
import jwt, { JwtPayload, SignOptions, VerifyOptions } from "jsonwebtoken"
import crypto from "node:crypto"
import { Invite, User } from "@models"
import { getExpiresAt } from "../utils/utils"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
userService: ModulesSdkTypes.IMedusaInternalService<any>
inviteService: ModulesSdkTypes.IMedusaInternalService<any>
}
const DEFAULT_VALID_INVITE_DURATION_SECONDS = 60 * 60 * 24
export default class UserModuleService
extends MedusaService<{
User: {
dto: UserTypes.UserDTO
}
Invite: {
dto: UserTypes.InviteDTO
}
}>({ User, Invite })
implements UserTypes.IUserModuleService
{
protected baseRepository_: DAL.RepositoryService
protected readonly userService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof User>
>
protected readonly inviteService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof Invite>
>
protected readonly config: {
jwtSecret: string
jwtPublicKey?: string
jwt_verify_options: ProjectConfigOptions["http"]["jwtVerifyOptions"]
jwtOptions: ProjectConfigOptions["http"]["jwtOptions"] & {
expiresIn: number
}
}
constructor(
{ userService, inviteService, baseRepository }: InjectedDependencies,
moduleDeclaration: InternalModuleDeclaration
) {
// @ts-ignore
super(...arguments)
this.baseRepository_ = baseRepository
this.userService_ = userService
this.inviteService_ = inviteService
this.config = {
jwtSecret: moduleDeclaration["jwt_secret"],
jwtPublicKey: moduleDeclaration["jwt_public_key"],
jwt_verify_options: moduleDeclaration["jwt_verify_options"],
jwtOptions: {
...moduleDeclaration["jwt_options"],
expiresIn:
moduleDeclaration["valid_duration"] ??
moduleDeclaration["jwt_options"]?.expiresIn ??
DEFAULT_VALID_INVITE_DURATION_SECONDS,
},
}
if (!this.config.jwtSecret) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"No jwt_secret was provided in the UserModule's options. Please add one."
)
}
}
@InjectManager()
async validateInviteToken(
token: string,
@MedusaContext() sharedContext: Context = {}
): Promise<UserTypes.InviteDTO> {
const options = {
...(this.config.jwt_verify_options ?? this.config.jwtOptions),
complete: true,
} as VerifyOptions & SignOptions
if (!options.algorithms && options.algorithm) {
options.algorithms = [options.algorithm]
delete options.algorithm
}
const decoded = jwt.verify(
token,
this.config.jwtPublicKey ?? this.config.jwtSecret,
options
) as JwtPayload
const invite = await this.inviteService_.retrieve(
decoded.payload.id,
{},
sharedContext
)
if (invite.expires_at < new Date()) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"The invite has expired"
)
}
return await this.baseRepository_.serialize<UserTypes.InviteDTO>(invite)
}
@InjectManager()
@EmitEvents()
async refreshInviteTokens(
inviteIds: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<UserTypes.InviteDTO[]> {
const invites = await this.refreshInviteTokens_(inviteIds, sharedContext)
const serializedInvites = await this.baseRepository_.serialize<
UserTypes.InviteDTO[]
>(invites)
moduleEventBuilderFactory({
eventName: UserEvents.INVITE_TOKEN_GENERATED,
source: Modules.USER,
action: CommonEvents.CREATED,
object: "invite",
})({
data: serializedInvites,
sharedContext,
})
return serializedInvites
}
@InjectTransactionManager()
async refreshInviteTokens_(
inviteIds: string[],
@MedusaContext() sharedContext: Context = {}
) {
const [invites, count] = await this.inviteService_.listAndCount(
{ id: inviteIds },
{},
sharedContext
)
if (count !== inviteIds.length) {
const missing = arrayDifference(
inviteIds,
invites.map((invite) => invite.id)
)
if (missing.length > 0) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`The following invites do not exist: ${missing.join(", ")}`
)
}
}
const expiresAt = getExpiresAt(this.config.jwtOptions.expiresIn)
const updates = invites.map((invite) => {
return {
id: invite.id,
expires_at: expiresAt,
token: this.generateToken({ id: invite.id, email: invite.email }),
}
})
return await this.inviteService_.update(updates, sharedContext)
}
// @ts-expect-error
createUsers(
data: UserTypes.CreateUserDTO[],
sharedContext?: Context
): Promise<UserTypes.UserDTO[]>
// @ts-expect-error
createUsers(
data: UserTypes.CreateUserDTO,
sharedContext?: Context
): Promise<UserTypes.UserDTO>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async createUsers(
data: UserTypes.CreateUserDTO[] | UserTypes.CreateUserDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<UserTypes.UserDTO | UserTypes.UserDTO[]> {
const input = Array.isArray(data) ? data : [data]
const users = await this.userService_.create(input, sharedContext)
const serializedUsers = await this.baseRepository_.serialize<
UserTypes.UserDTO[] | UserTypes.UserDTO
>(users)
return Array.isArray(data) ? serializedUsers : serializedUsers[0]
}
// @ts-expect-error
updateUsers(
data: UserTypes.UpdateUserDTO[],
sharedContext?: Context
): Promise<UserTypes.UserDTO[]>
// @ts-expect-error
updateUsers(
data: UserTypes.UpdateUserDTO,
sharedContext?: Context
): Promise<UserTypes.UserDTO>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async updateUsers(
data: UserTypes.UpdateUserDTO | UserTypes.UpdateUserDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<UserTypes.UserDTO | UserTypes.UserDTO[]> {
const input = Array.isArray(data) ? data : [data]
const updatedUsers = await this.userService_.update(input, sharedContext)
const serializedUsers = await this.baseRepository_.serialize<
UserTypes.UserDTO[]
>(updatedUsers)
return Array.isArray(data) ? serializedUsers : serializedUsers[0]
}
// @ts-expect-error
createInvites(
data: UserTypes.CreateInviteDTO[],
sharedContext?: Context
): Promise<UserTypes.InviteDTO[]>
// @ts-expect-error
createInvites(
data: UserTypes.CreateInviteDTO,
sharedContext?: Context
): Promise<UserTypes.InviteDTO>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async createInvites(
data: UserTypes.CreateInviteDTO[] | UserTypes.CreateInviteDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<UserTypes.InviteDTO | UserTypes.InviteDTO[]> {
const input = Array.isArray(data) ? data : [data]
const invites = await this.createInvites_(input, sharedContext)
const serializedInvites = await this.baseRepository_.serialize<
UserTypes.InviteDTO[] | UserTypes.InviteDTO
>(invites)
moduleEventBuilderFactory({
eventName: UserEvents.INVITE_TOKEN_GENERATED,
source: Modules.USER,
action: "token_generated",
object: "invite",
})({
data: serializedInvites,
sharedContext,
})
return Array.isArray(data) ? serializedInvites : serializedInvites[0]
}
@InjectTransactionManager()
private async createInvites_(
data: UserTypes.CreateInviteDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<InferEntityType<typeof Invite>[]> {
const alreadyExistingUsers = await this.listUsers({
email: data.map((d) => d.email),
})
if (alreadyExistingUsers.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`User account for following email(s) already exist: ${alreadyExistingUsers
.map((u) => u.email)
.join(", ")}`
)
}
const expiresAt = getExpiresAt(this.config.jwtOptions.expiresIn)
const toCreate = data.map((invite) => {
const id = generateEntityId((invite as { id?: string }).id, "invite")
return {
...invite,
id,
expires_at: expiresAt,
token: this.generateToken({ id, email: invite.email }),
}
})
return await this.inviteService_.create(toCreate, sharedContext)
}
// @ts-ignore
updateInvites(
data: UserTypes.UpdateInviteDTO[],
sharedContext?: Context
): Promise<UserTypes.InviteDTO[]>
// @ts-expect-error
updateInvites(
data: UserTypes.UpdateInviteDTO,
sharedContext?: Context
): Promise<UserTypes.InviteDTO>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async updateInvites(
data: UserTypes.UpdateInviteDTO | UserTypes.UpdateInviteDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<UserTypes.InviteDTO | UserTypes.InviteDTO[]> {
const input = Array.isArray(data) ? data : [data]
const updatedInvites = await this.inviteService_.update(
input,
sharedContext
)
const serializedInvites = await this.baseRepository_.serialize<
UserTypes.InviteDTO[]
>(updatedInvites)
moduleEventBuilderFactory({
eventName: UserEvents.INVITE_TOKEN_GENERATED,
source: Modules.USER,
action: "token_generated",
object: "invite",
})({
data: serializedInvites,
sharedContext,
})
return Array.isArray(data) ? serializedInvites : serializedInvites[0]
}
private generateToken(data: any): string {
const jwtId = this.config.jwtOptions.jwtid ?? crypto.randomUUID()
const token = generateJwtToken(data, {
secret: this.config.jwtSecret,
jwtOptions: {
...this.config.jwtOptions,
jwtid: jwtId,
},
})
return token
}
}