Files
medusa-store/packages/modules/settings/src/services/settings-module-service.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

384 lines
11 KiB
TypeScript

import {
Context,
DAL,
InferEntityType,
InternalModuleDeclaration,
ModulesSdkTypes,
SettingsTypes,
} from "@medusajs/framework/types"
import {
EmitEvents,
InjectManager,
InjectTransactionManager,
MedusaContext,
MedusaError,
MedusaService,
} from "@medusajs/framework/utils"
import { ViewConfiguration, UserPreference } from "@/models"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
viewConfigurationService: ModulesSdkTypes.IMedusaInternalService<any>
userPreferenceService: ModulesSdkTypes.IMedusaInternalService<any>
}
export default class SettingsModuleService
extends MedusaService<{
ViewConfiguration: { dto: SettingsTypes.ViewConfigurationDTO }
UserPreference: { dto: SettingsTypes.UserPreferenceDTO }
}>({ ViewConfiguration, UserPreference })
implements SettingsTypes.ISettingsModuleService
{
protected baseRepository_: DAL.RepositoryService
protected readonly viewConfigurationService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ViewConfiguration>
>
protected readonly userPreferenceService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof UserPreference>
>
constructor(
{
baseRepository,
viewConfigurationService,
userPreferenceService,
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
super(...arguments)
this.baseRepository_ = baseRepository
this.viewConfigurationService_ = viewConfigurationService
this.userPreferenceService_ = userPreferenceService
}
@InjectManager()
@EmitEvents()
// @ts-expect-error
async createViewConfigurations(
data:
| SettingsTypes.CreateViewConfigurationDTO
| SettingsTypes.CreateViewConfigurationDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<
SettingsTypes.ViewConfigurationDTO | SettingsTypes.ViewConfigurationDTO[]
> {
// Convert to array for validation only
const isArrayInput = Array.isArray(data)
const dataArray = isArrayInput ? data : [data]
// Validate system defaults
for (const config of dataArray) {
if (config.is_system_default && config.user_id) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"System default view configurations cannot have a user_id"
)
}
if (config.is_system_default) {
// Check if a system default already exists for this entity
const existingDefault = await this.viewConfigurationService_.list(
{
entity: config.entity,
is_system_default: true,
},
{ select: ["id"] },
sharedContext
)
if (existingDefault.length > 0) {
throw new MedusaError(
MedusaError.Types.DUPLICATE_ERROR,
`A system default view configuration already exists for entity: ${config.entity}`
)
}
}
}
const result = await super.createViewConfigurations(
dataArray,
sharedContext
)
return await this.baseRepository_.serialize<
SettingsTypes.ViewConfigurationDTO[] | SettingsTypes.ViewConfigurationDTO
>(isArrayInput ? result : result[0])
}
@InjectManager()
@EmitEvents()
// @ts-expect-error
async updateViewConfigurations(
idOrSelector: string | SettingsTypes.FilterableViewConfigurationProps,
data: SettingsTypes.UpdateViewConfigurationDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
SettingsTypes.ViewConfigurationDTO | SettingsTypes.ViewConfigurationDTO[]
> {
const updated = await this.updateViewConfigurations_(
idOrSelector,
data,
sharedContext
)
const serialized = await this.baseRepository_.serialize<
SettingsTypes.ViewConfigurationDTO[] | SettingsTypes.ViewConfigurationDTO
>(updated)
return typeof idOrSelector === "string" ? serialized[0] : serialized
}
@InjectTransactionManager()
protected async updateViewConfigurations_(
idOrSelector: string | SettingsTypes.FilterableViewConfigurationProps,
data: SettingsTypes.UpdateViewConfigurationDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<InferEntityType<typeof ViewConfiguration>[]> {
let selector: SettingsTypes.FilterableViewConfigurationProps = {}
if (typeof idOrSelector === "string") {
selector = { id: idOrSelector }
} else {
selector = idOrSelector
}
// Special handling for configuration updates to ensure replacement instead of merge
if (data.configuration) {
// First, get the entities to update
const entities = await this.viewConfigurationService_.list(
selector,
{},
sharedContext
)
if (entities.length === 0) {
return typeof idOrSelector === "string" ? [] : []
}
// Use upsertWithReplace to update the configuration field without merging
const updateDataArray = entities.map((entity) => ({
id: entity.id,
...data,
configuration: {
visible_columns: data.configuration?.visible_columns ?? [],
column_order: data.configuration?.column_order ?? [],
column_widths:
data.configuration?.column_widths !== undefined
? data.configuration.column_widths
: {},
filters:
data.configuration?.filters !== undefined
? data.configuration.filters
: {},
sorting:
data.configuration?.sorting !== undefined
? data.configuration.sorting
: null,
search:
data.configuration?.search !== undefined
? data.configuration.search
: "",
},
}))
// Use upsertWithReplace which uses nativeUpdateMany internally and doesn't merge JSON fields
const { entities: updatedEntities } =
await this.viewConfigurationService_.upsertWithReplace(
updateDataArray,
{ relations: [] },
sharedContext
)
return updatedEntities
}
// For non-configuration updates, use the standard update method
const updated = await this.viewConfigurationService_.update(
{ selector, data },
sharedContext
)
return updated as unknown as InferEntityType<typeof ViewConfiguration>[]
}
@InjectManager()
async getUserPreference(
userId: string,
key: string,
@MedusaContext() sharedContext: Context = {}
): Promise<SettingsTypes.UserPreferenceDTO | null> {
const prefs = await this.userPreferenceService_.list(
{ user_id: userId, key },
{},
sharedContext
)
if (prefs.length === 0) {
return null
}
return await this.baseRepository_.serialize<SettingsTypes.UserPreferenceDTO>(
prefs[0]
)
}
@InjectManager()
@EmitEvents()
async setUserPreference(
userId: string,
key: string,
value: any,
@MedusaContext() sharedContext: Context = {}
): Promise<SettingsTypes.UserPreferenceDTO> {
const existing = await this.userPreferenceService_.list(
{ user_id: userId, key },
{ select: ["id"] },
sharedContext
)
let result: InferEntityType<typeof UserPreference>
if (existing.length > 0) {
const updated = await this.userPreferenceService_.update(
[{ id: existing[0].id, value }],
sharedContext
)
result = updated[0]
} else {
const created = await this.userPreferenceService_.create(
{ user_id: userId, key, value },
sharedContext
)
result = created[0]
}
return await this.baseRepository_.serialize<SettingsTypes.UserPreferenceDTO>(
result
)
}
@InjectManager()
async getActiveViewConfiguration(
entity: string,
userId: string,
@MedusaContext() sharedContext: Context = {}
): Promise<SettingsTypes.ViewConfigurationDTO | null> {
// Check if user has an active view preference
const activeViewPref = await this.getUserPreference(
userId,
`active_view.${entity}`,
sharedContext
)
// Check if we have a preference with a view configuration ID (not explicitly null)
if (
activeViewPref &&
activeViewPref.value?.viewConfigurationId &&
activeViewPref.value.viewConfigurationId !== null
) {
try {
return await this.retrieveViewConfiguration(
activeViewPref.value.viewConfigurationId,
{},
sharedContext
)
} catch (error) {
// View configuration might have been deleted
}
}
// If we have an explicit null preference, or no preference, or a deleted view
// We should check for defaults in this order:
// Check if user has any personal views (only if no explicit null preference)
if (!activeViewPref || activeViewPref.value?.viewConfigurationId !== null) {
const [personalView] = await this.listViewConfigurations(
{ entity, user_id: userId },
{ take: 1, order: { created_at: "ASC" } },
sharedContext
)
if (personalView) {
return personalView
}
}
// Fall back to system default
const systemDefaults = await this.listViewConfigurations(
{ entity, is_system_default: true },
{},
sharedContext
)
return systemDefaults.length > 0 ? systemDefaults[0] : null
}
@InjectManager()
@EmitEvents()
async setActiveViewConfiguration(
entity: string,
userId: string,
viewConfigurationId: string,
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
// Verify the view configuration exists and user has access
const viewConfig = await this.retrieveViewConfiguration(
viewConfigurationId,
{},
sharedContext
)
if (viewConfig.entity !== entity) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`View configuration ${viewConfigurationId} is not for entity ${entity}`
)
}
if (viewConfig.user_id && viewConfig.user_id !== userId) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`User ${userId} does not have access to view configuration ${viewConfigurationId}`
)
}
await this.setUserPreference(
userId,
`active_view.${entity}`,
{ viewConfigurationId },
sharedContext
)
}
@InjectManager()
async getSystemDefaultViewConfiguration(
entity: string,
@MedusaContext() sharedContext: Context = {}
): Promise<SettingsTypes.ViewConfigurationDTO | null> {
const systemDefaults = await this.listViewConfigurations(
{ entity, is_system_default: true },
{},
sharedContext
)
return systemDefaults.length > 0 ? systemDefaults[0] : null
}
@InjectManager()
@EmitEvents()
async clearActiveViewConfiguration(
entity: string,
userId: string,
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
// Instead of deleting, set the preference to null
// This ensures we're using the same transaction pattern as setActiveViewConfiguration
await this.setUserPreference(
userId,
`active_view.${entity}`,
{ viewConfigurationId: null },
sharedContext
)
}
}