chore(): Improve cascade soft deletetion/restoration and update (#11618)
**What** - Fix soft deletion and restoration emitted events - Improve soft deleted/restore algorithm - Fix big number field handling null value during partial hydration from mikro orm
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/utils": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
chore(): Improve cascade soft deletetion and update
|
||||||
@@ -29,34 +29,42 @@ export function MikroOrmBigNumberProperty(
|
|||||||
this.__helper.__data[rawColumnName] = null
|
this.__helper.__data[rawColumnName] = null
|
||||||
this[rawColumnName] = null
|
this[rawColumnName] = null
|
||||||
} else {
|
} else {
|
||||||
let bigNumber: BigNumber
|
// When mikro orm create and hydrate the entity with partial selection it can happen
|
||||||
|
// that null value is being passed.
|
||||||
|
if (!options?.nullable && value === null) {
|
||||||
|
this.__helper.__data[columnName] = undefined
|
||||||
|
this.__helper.__data[rawColumnName] = undefined
|
||||||
|
this[rawColumnName] = undefined
|
||||||
|
} else {
|
||||||
|
let bigNumber: BigNumber
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (value instanceof BigNumber) {
|
if (value instanceof BigNumber) {
|
||||||
bigNumber = value
|
bigNumber = value
|
||||||
} else if (this[rawColumnName]) {
|
} else if (this[rawColumnName]) {
|
||||||
const precision = this[rawColumnName].precision
|
const precision = this[rawColumnName].precision
|
||||||
bigNumber = new BigNumber(value, {
|
bigNumber = new BigNumber(value, {
|
||||||
precision,
|
precision,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
bigNumber = new BigNumber(value)
|
bigNumber = new BigNumber(value)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Cannot set value ${value} for ${columnName}.`)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Cannot set value ${value} for ${columnName}.`)
|
const raw = bigNumber.raw!
|
||||||
|
raw.value = trimZeros(raw.value as string)
|
||||||
|
|
||||||
|
// Note: this.__helper isn't present when directly working with the entity
|
||||||
|
// Adding this in optionally for it not to break.
|
||||||
|
if (isDefined(this.__helper)) {
|
||||||
|
this.__helper.__data[columnName] = bigNumber.numeric
|
||||||
|
this.__helper.__data[rawColumnName] = raw
|
||||||
|
}
|
||||||
|
|
||||||
|
this[rawColumnName] = raw
|
||||||
}
|
}
|
||||||
|
|
||||||
const raw = bigNumber.raw!
|
|
||||||
raw.value = trimZeros(raw.value as string)
|
|
||||||
|
|
||||||
// Note: this.__helper isn't present when directly working with the entity
|
|
||||||
// Adding this in optionally for it not to break.
|
|
||||||
if (isDefined(this.__helper)) {
|
|
||||||
this.__helper.__data[columnName] = bigNumber.numeric
|
|
||||||
this.__helper.__data[rawColumnName] = raw
|
|
||||||
}
|
|
||||||
|
|
||||||
this[rawColumnName] = raw
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: this.__helper isn't present when directly working with the entity
|
// Note: this.__helper isn't present when directly working with the entity
|
||||||
|
|||||||
@@ -31,10 +31,7 @@ import {
|
|||||||
} from "../../common"
|
} from "../../common"
|
||||||
import { toMikroORMEntity } from "../../dml"
|
import { toMikroORMEntity } from "../../dml"
|
||||||
import { buildQuery } from "../../modules-sdk/build-query"
|
import { buildQuery } from "../../modules-sdk/build-query"
|
||||||
import {
|
import { transactionWrapper } from "../utils"
|
||||||
getSoftDeletedCascadedEntitiesIdsMappedBy,
|
|
||||||
transactionWrapper,
|
|
||||||
} from "../utils"
|
|
||||||
import { dbErrorMapper } from "./db-error-mapper"
|
import { dbErrorMapper } from "./db-error-mapper"
|
||||||
import { mikroOrmSerializer } from "./mikro-orm-serializer"
|
import { mikroOrmSerializer } from "./mikro-orm-serializer"
|
||||||
import { mikroOrmUpdateDeletedAtRecursively } from "./utils"
|
import { mikroOrmUpdateDeletedAtRecursively } from "./utils"
|
||||||
@@ -215,17 +212,13 @@ export class MikroOrmBaseRepository<const T extends object = object>
|
|||||||
const date = new Date()
|
const date = new Date()
|
||||||
|
|
||||||
const manager = this.getActiveManager<SqlEntityManager>(sharedContext)
|
const manager = this.getActiveManager<SqlEntityManager>(sharedContext)
|
||||||
await mikroOrmUpdateDeletedAtRecursively<T>(
|
const softDeletedEntitiesMap = await mikroOrmUpdateDeletedAtRecursively<T>(
|
||||||
manager,
|
manager,
|
||||||
entities as any[],
|
entities as any[],
|
||||||
date
|
date
|
||||||
)
|
)
|
||||||
|
|
||||||
const softDeletedEntitiesMap = getSoftDeletedCascadedEntitiesIdsMappedBy({
|
return [entities, Object.fromEntries(softDeletedEntitiesMap)]
|
||||||
entities,
|
|
||||||
})
|
|
||||||
|
|
||||||
return [entities, softDeletedEntitiesMap]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async restore(
|
async restore(
|
||||||
@@ -239,14 +232,13 @@ export class MikroOrmBaseRepository<const T extends object = object>
|
|||||||
const entities = await this.find(query, sharedContext)
|
const entities = await this.find(query, sharedContext)
|
||||||
|
|
||||||
const manager = this.getActiveManager<SqlEntityManager>(sharedContext)
|
const manager = this.getActiveManager<SqlEntityManager>(sharedContext)
|
||||||
await mikroOrmUpdateDeletedAtRecursively(manager, entities as any[], null)
|
const softDeletedEntitiesMap = await mikroOrmUpdateDeletedAtRecursively(
|
||||||
|
manager,
|
||||||
|
entities as any[],
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
const softDeletedEntitiesMap = getSoftDeletedCascadedEntitiesIdsMappedBy({
|
return [entities, Object.fromEntries(softDeletedEntitiesMap)]
|
||||||
entities,
|
|
||||||
restored: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
return [entities, softDeletedEntitiesMap]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,38 +390,41 @@ export function mikroOrmBaseRepositoryFactory<const T extends object>(
|
|||||||
descriptor,
|
descriptor,
|
||||||
] of collectionsToRemoveAllFrom) {
|
] of collectionsToRemoveAllFrom) {
|
||||||
await promiseAll(
|
await promiseAll(
|
||||||
data.map(async ({ entity }) => {
|
data.flatMap(async ({ entity }) => {
|
||||||
if (!descriptor.mappedBy) {
|
if (!descriptor.mappedBy) {
|
||||||
return await entity[collectionToRemoveAllFrom].init()
|
return await entity[collectionToRemoveAllFrom].init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const promises: Promise<any>[] = []
|
||||||
await entity[collectionToRemoveAllFrom].init()
|
await entity[collectionToRemoveAllFrom].init()
|
||||||
const items = entity[collectionToRemoveAllFrom]
|
const items = entity[collectionToRemoveAllFrom]
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
await item[descriptor.mappedBy!].init()
|
promises.push(item[descriptor.mappedBy!].init())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return promises
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async update(
|
||||||
data: { entity; update }[],
|
data: { entity: any; update: any }[],
|
||||||
context?: Context
|
context?: Context
|
||||||
): Promise<InferRepositoryReturnType<T>[]> {
|
): Promise<InferRepositoryReturnType<T>[]> {
|
||||||
const manager = this.getActiveManager<EntityManager>(context)
|
const manager = this.getActiveManager<EntityManager>(context)
|
||||||
|
|
||||||
await this.initManyToManyToDetachAllItemsIfNeeded(data, context)
|
await this.initManyToManyToDetachAllItemsIfNeeded(data, context)
|
||||||
|
|
||||||
data.map((_, index) => {
|
data.forEach(({ entity, update }) => {
|
||||||
manager.assign(data[index].entity, data[index].update, {
|
manager.assign(entity, update, {
|
||||||
mergeObjectProperties: true,
|
mergeObjectProperties: true,
|
||||||
})
|
})
|
||||||
manager.persist(data[index].entity)
|
manager.persist(entity)
|
||||||
})
|
})
|
||||||
|
|
||||||
return data.map((d) => d.entity)
|
return data.map((d) => d.entity) as InferRepositoryReturnType<T>[]
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(
|
async delete(
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Collection, EntityMetadata, FindOptions, wrap } from "@mikro-orm/core"
|
import { EntityMetadata, FindOptions } from "@mikro-orm/core"
|
||||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||||
import { buildQuery } from "../../modules-sdk/build-query"
|
import { promiseAll } from "../../common"
|
||||||
import { isString } from "../../common/is-string"
|
import { isString } from "../../common/is-string"
|
||||||
|
import { buildQuery } from "../../modules-sdk/build-query"
|
||||||
|
|
||||||
function detectCircularDependency(
|
function detectCircularDependency(
|
||||||
manager: SqlEntityManager,
|
manager: SqlEntityManager,
|
||||||
@@ -60,47 +61,80 @@ function detectCircularDependency(
|
|||||||
async function performCascadingSoftDeletion<T>(
|
async function performCascadingSoftDeletion<T>(
|
||||||
manager: SqlEntityManager,
|
manager: SqlEntityManager,
|
||||||
entity: T & { id: string; deleted_at?: string | Date | null },
|
entity: T & { id: string; deleted_at?: string | Date | null },
|
||||||
value: Date | null
|
value: Date | null,
|
||||||
|
softDeletedEntitiesMap: Map<
|
||||||
|
string,
|
||||||
|
(T & { id: string; deleted_at?: string | Date | null })[]
|
||||||
|
> = new Map()
|
||||||
) {
|
) {
|
||||||
if (!("deleted_at" in entity)) return
|
if (!("deleted_at" in entity)) return
|
||||||
|
|
||||||
entity.deleted_at = value
|
entity.deleted_at = value
|
||||||
|
|
||||||
const entityName = entity.constructor.name
|
const softDeletedEntityMapItem = softDeletedEntitiesMap.get(
|
||||||
|
entity.constructor.name
|
||||||
|
)
|
||||||
|
if (!softDeletedEntityMapItem) {
|
||||||
|
softDeletedEntitiesMap.set(entity.constructor.name, [entity])
|
||||||
|
} else {
|
||||||
|
softDeletedEntityMapItem.push(entity)
|
||||||
|
}
|
||||||
|
|
||||||
const relations = manager.getDriver().getMetadata().get(entityName).relations
|
const entityName = entity.constructor.name
|
||||||
|
const entityMetadata = manager.getDriver().getMetadata().get(entityName)
|
||||||
|
const relations = entityMetadata.relations
|
||||||
|
|
||||||
const relationsToCascade = relations.filter((relation) =>
|
const relationsToCascade = relations.filter((relation) =>
|
||||||
relation.cascade?.includes("soft-remove" as any)
|
relation.cascade?.includes("soft-remove" as any)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// If there are no relations to cascade, just persist the entity and return
|
||||||
|
if (!relationsToCascade.length) {
|
||||||
|
manager.persist(entity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the entity with all cascading relations in a single query
|
||||||
|
const relationNames = relationsToCascade.map((r) => r.name)
|
||||||
|
|
||||||
|
const query = buildQuery(
|
||||||
|
{
|
||||||
|
id: entity.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
select: [
|
||||||
|
"id",
|
||||||
|
"deleted_at",
|
||||||
|
...relationNames.flatMap((r) => [`${r}.id`, `${r}.deleted_at`]),
|
||||||
|
],
|
||||||
|
relations: relationNames,
|
||||||
|
withDeleted: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const entityWithRelations = await manager.findOne(entityName, query.where, {
|
||||||
|
...query.options,
|
||||||
|
populateFilter: {
|
||||||
|
withDeleted: true,
|
||||||
|
},
|
||||||
|
} as FindOptions<any>)
|
||||||
|
|
||||||
|
if (!entityWithRelations) {
|
||||||
|
manager.persist(entity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a map to group related entities by their type
|
||||||
|
const relatedEntitiesByType = new Map<
|
||||||
|
string,
|
||||||
|
T & { id: string; deleted_at?: string | Date | null }[]
|
||||||
|
>()
|
||||||
|
|
||||||
|
// Collect all related entities by type
|
||||||
for (const relation of relationsToCascade) {
|
for (const relation of relationsToCascade) {
|
||||||
let entityRelation = entity[relation.name]
|
const entityRelation = entityWithRelations[relation.name]
|
||||||
|
|
||||||
// Handle optional relationships
|
// Skip if relation is null or undefined
|
||||||
if (relation.nullable && !entityRelation) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const retrieveEntity = async () => {
|
|
||||||
const query = buildQuery(
|
|
||||||
{
|
|
||||||
id: entity.id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
relations: [relation.name],
|
|
||||||
withDeleted: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return await manager.findOne(
|
|
||||||
entity.constructor.name,
|
|
||||||
query.where,
|
|
||||||
query.options as FindOptions<any>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
entityRelation = await retrieveEntity()
|
|
||||||
entityRelation = entityRelation[relation.name]
|
|
||||||
if (!entityRelation) {
|
if (!entityRelation) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -109,45 +143,86 @@ async function performCascadingSoftDeletion<T>(
|
|||||||
let relationEntities: any[] = []
|
let relationEntities: any[] = []
|
||||||
|
|
||||||
if (isCollection) {
|
if (isCollection) {
|
||||||
if (!(entityRelation as Collection<any, any>).isInitialized()) {
|
|
||||||
entityRelation = await retrieveEntity()
|
|
||||||
entityRelation = entityRelation[relation.name]
|
|
||||||
}
|
|
||||||
relationEntities = entityRelation.getItems()
|
relationEntities = entityRelation.getItems()
|
||||||
} else {
|
} else {
|
||||||
const wrappedEntity = wrap(entityRelation)
|
relationEntities = [entityRelation]
|
||||||
|
|
||||||
let initializedEntityRelation = entityRelation
|
|
||||||
if (!wrappedEntity.isInitialized()) {
|
|
||||||
initializedEntityRelation = await wrap(entityRelation).init()
|
|
||||||
}
|
|
||||||
|
|
||||||
relationEntities = [initializedEntityRelation]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!relationEntities.length) {
|
if (!relationEntities.length) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
await mikroOrmUpdateDeletedAtRecursively(manager, relationEntities, value)
|
// Add to the map of entities by type
|
||||||
|
if (!relatedEntitiesByType.has(relation.type)) {
|
||||||
|
relatedEntitiesByType.set(relation.type, [] as any)
|
||||||
|
}
|
||||||
|
relatedEntitiesByType.get(relation.type)!.push(...relationEntities)
|
||||||
}
|
}
|
||||||
|
|
||||||
await manager.persist(entity)
|
// Process each type of related entity in batch
|
||||||
|
const promises: Promise<void>[] = []
|
||||||
|
for (const [, entities] of relatedEntitiesByType.entries()) {
|
||||||
|
if (entities.length === 0) continue
|
||||||
|
|
||||||
|
// Process cascading relations for these entities
|
||||||
|
promises.push(
|
||||||
|
...entities.map((entity) =>
|
||||||
|
performCascadingSoftDeletion(
|
||||||
|
manager,
|
||||||
|
entity as any,
|
||||||
|
value,
|
||||||
|
softDeletedEntitiesMap
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await promiseAll(promises)
|
||||||
|
|
||||||
|
manager.persist(entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the deleted_at field for all entities in the given array and their
|
||||||
|
* cascaded relations and returns a map of entity IDs to their corresponding
|
||||||
|
* entity types.
|
||||||
|
*
|
||||||
|
* @param manager - The Mikro ORM manager instance.
|
||||||
|
* @param entities - An array of entities to update.
|
||||||
|
* @param value - The value to set for the deleted_at field.
|
||||||
|
* @returns A map of entity IDs to their corresponding entity types.
|
||||||
|
*/
|
||||||
export const mikroOrmUpdateDeletedAtRecursively = async <
|
export const mikroOrmUpdateDeletedAtRecursively = async <
|
||||||
T extends object = any
|
T extends object = any
|
||||||
>(
|
>(
|
||||||
manager: SqlEntityManager,
|
manager: SqlEntityManager,
|
||||||
entities: (T & { id: string; deleted_at?: string | Date | null })[],
|
entities: (T & { id: string; deleted_at?: string | Date | null })[],
|
||||||
value: Date | null
|
value: Date | null
|
||||||
) => {
|
): Promise<
|
||||||
|
Map<string, (T & { id: string; deleted_at?: string | Date | null })[]>
|
||||||
|
> => {
|
||||||
|
const softDeletedEntitiesMap = new Map<
|
||||||
|
string,
|
||||||
|
(T & { id: string; deleted_at?: string | Date | null })[]
|
||||||
|
>()
|
||||||
|
|
||||||
|
if (!entities.length) return softDeletedEntitiesMap
|
||||||
|
|
||||||
|
const entityMetadata = manager
|
||||||
|
.getDriver()
|
||||||
|
.getMetadata()
|
||||||
|
.get(entities[0].constructor.name)
|
||||||
|
detectCircularDependency(manager, entityMetadata)
|
||||||
|
|
||||||
|
// Process each entity type
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
const entityMetadata = manager
|
await performCascadingSoftDeletion(
|
||||||
.getDriver()
|
manager,
|
||||||
.getMetadata()
|
entity,
|
||||||
.get(entity.constructor.name)
|
value,
|
||||||
detectCircularDependency(manager, entityMetadata)
|
softDeletedEntitiesMap
|
||||||
await performCascadingSoftDeletion(manager, entity, value)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return softDeletedEntitiesMap
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { isObject } from "../common"
|
|
||||||
|
|
||||||
export async function transactionWrapper<TManager = unknown>(
|
export async function transactionWrapper<TManager = unknown>(
|
||||||
manager: any,
|
manager: any,
|
||||||
task: (transactionManager: any) => Promise<any>,
|
task: (transactionManager: any) => Promise<any>,
|
||||||
@@ -32,58 +30,6 @@ export async function transactionWrapper<TManager = unknown>(
|
|||||||
return await transactionMethod.bind(manager)(task, options)
|
return await transactionMethod.bind(manager)(task, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Can be used to create a new Object that collect the entities
|
|
||||||
* based on the columnLookup. This is useful when you want to soft delete entities and return
|
|
||||||
* an object where the keys are the entities name and the values are the entities
|
|
||||||
* that were soft deleted.
|
|
||||||
*
|
|
||||||
* @param entities
|
|
||||||
* @param deletedEntitiesMap
|
|
||||||
* @param getEntityName
|
|
||||||
*/
|
|
||||||
export function getSoftDeletedCascadedEntitiesIdsMappedBy({
|
|
||||||
entities,
|
|
||||||
deletedEntitiesMap,
|
|
||||||
getEntityName,
|
|
||||||
restored,
|
|
||||||
}: {
|
|
||||||
entities: any[]
|
|
||||||
deletedEntitiesMap?: Map<string, any[]>
|
|
||||||
getEntityName?: (entity: any) => string
|
|
||||||
restored?: boolean
|
|
||||||
}): Record<string, any[]> {
|
|
||||||
deletedEntitiesMap ??= new Map<string, any[]>()
|
|
||||||
getEntityName ??= (entity) => entity.constructor.name
|
|
||||||
|
|
||||||
for (const entity of entities) {
|
|
||||||
const entityName = getEntityName(entity)
|
|
||||||
const shouldSkip = !!deletedEntitiesMap
|
|
||||||
.get(entityName)
|
|
||||||
?.some((e) => e.id === entity.id)
|
|
||||||
|
|
||||||
if ((restored ? !!entity.deleted_at : !entity.deleted_at) || shouldSkip) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const values = deletedEntitiesMap.get(entityName) ?? []
|
|
||||||
values.push(entity)
|
|
||||||
deletedEntitiesMap.set(entityName, values)
|
|
||||||
|
|
||||||
Object.values(entity).forEach((propValue: any) => {
|
|
||||||
if (propValue != null && isObject(propValue[0])) {
|
|
||||||
getSoftDeletedCascadedEntitiesIdsMappedBy({
|
|
||||||
entities: propValue,
|
|
||||||
deletedEntitiesMap,
|
|
||||||
getEntityName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.fromEntries(deletedEntitiesMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeMigrationSQL(sql: string) {
|
export function normalizeMigrationSQL(sql: string) {
|
||||||
sql = sql.replace(
|
sql = sql.replace(
|
||||||
/create table (?!if not exists)/g,
|
/create table (?!if not exists)/g,
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ export function MedusaInternalService<
|
|||||||
const primaryKeys = AbstractService_.retrievePrimaryKeys(model)
|
const primaryKeys = AbstractService_.retrievePrimaryKeys(model)
|
||||||
const inputArray = Array.isArray(input) ? input : [input]
|
const inputArray = Array.isArray(input) ? input : [input]
|
||||||
|
|
||||||
const toUpdateData: { entity; update }[] = []
|
const toUpdateData: { entity: TEntity; update: Partial<TEntity> }[] = []
|
||||||
|
|
||||||
// Only used when we receive data and no selector
|
// Only used when we receive data and no selector
|
||||||
const keySelectorForDataOnly: any = {
|
const keySelectorForDataOnly: any = {
|
||||||
@@ -353,10 +353,17 @@ export function MedusaInternalService<
|
|||||||
|
|
||||||
// Manage metadata if needed
|
// Manage metadata if needed
|
||||||
toUpdateData.forEach(({ entity, update }) => {
|
toUpdateData.forEach(({ entity, update }) => {
|
||||||
if (isPresent(update.metadata)) {
|
const update_ = update as (typeof toUpdateData)[number]["update"] & {
|
||||||
entity.metadata = update.metadata = mergeMetadata(
|
metadata: Record<string, unknown>
|
||||||
entity.metadata ?? {},
|
}
|
||||||
update.metadata
|
const entity_ = entity as InferEntityType<TEntity> & {
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPresent(update_.metadata)) {
|
||||||
|
entity_.metadata = update_.metadata = mergeMetadata(
|
||||||
|
entity_.metadata ?? {},
|
||||||
|
update_.metadata
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -137,6 +137,37 @@ export function MedusaService<
|
|||||||
? ModelConfigurationsToConfigTemplate<TModels>
|
? ModelConfigurationsToConfigTemplate<TModels>
|
||||||
: ModelsConfig
|
: ModelsConfig
|
||||||
> {
|
> {
|
||||||
|
function emitSoftDeleteRestoreEvents(
|
||||||
|
this: AbstractModuleService_,
|
||||||
|
klassPrototype: any,
|
||||||
|
cascadedModelsMap: Record<string, string[]>,
|
||||||
|
action: string,
|
||||||
|
sharedContext: Context
|
||||||
|
) {
|
||||||
|
const joinerConfig = (
|
||||||
|
typeof this.__joinerConfig === "function"
|
||||||
|
? this.__joinerConfig()
|
||||||
|
: this.__joinerConfig
|
||||||
|
) as ModuleJoinerConfig
|
||||||
|
|
||||||
|
const emittedEntities = new Set<string>()
|
||||||
|
|
||||||
|
Object.entries(cascadedModelsMap).forEach(([linkableKey, ids]) => {
|
||||||
|
const entity = joinerConfig.linkableKeys?.[linkableKey]!
|
||||||
|
if (entity && !emittedEntities.has(entity)) {
|
||||||
|
emittedEntities.add(entity)
|
||||||
|
const linkableKeyEntity = camelToSnakeCase(entity).toLowerCase()
|
||||||
|
|
||||||
|
klassPrototype.aggregatedEvents.bind(this)({
|
||||||
|
action: action,
|
||||||
|
object: linkableKeyEntity,
|
||||||
|
data: { id: ids },
|
||||||
|
context: sharedContext,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const buildAndAssignMethodImpl = function (
|
const buildAndAssignMethodImpl = function (
|
||||||
klassPrototype: any,
|
klassPrototype: any,
|
||||||
method: string,
|
method: string,
|
||||||
@@ -321,27 +352,11 @@ export function MedusaService<
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (mappedCascadedModelsMap) {
|
if (mappedCascadedModelsMap) {
|
||||||
const joinerConfig = (
|
emitSoftDeleteRestoreEvents.bind(this)(
|
||||||
typeof this.__joinerConfig === "function"
|
klassPrototype,
|
||||||
? this.__joinerConfig()
|
mappedCascadedModelsMap,
|
||||||
: this.__joinerConfig
|
CommonEvents.DELETED,
|
||||||
) as ModuleJoinerConfig
|
sharedContext
|
||||||
|
|
||||||
Object.entries(mappedCascadedModelsMap).forEach(
|
|
||||||
([linkableKey, ids]) => {
|
|
||||||
const entity = joinerConfig.linkableKeys?.[linkableKey]!
|
|
||||||
if (entity) {
|
|
||||||
const linkableKeyEntity =
|
|
||||||
camelToSnakeCase(entity).toLowerCase()
|
|
||||||
|
|
||||||
klassPrototype.aggregatedEvents.bind(this)({
|
|
||||||
action: CommonEvents.DELETED,
|
|
||||||
object: linkableKeyEntity,
|
|
||||||
data: { id: ids },
|
|
||||||
context: sharedContext,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,26 +393,11 @@ export function MedusaService<
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (mappedCascadedModelsMap) {
|
if (mappedCascadedModelsMap) {
|
||||||
const joinerConfig = (
|
emitSoftDeleteRestoreEvents.bind(this)(
|
||||||
typeof this.__joinerConfig === "function"
|
klassPrototype,
|
||||||
? this.__joinerConfig()
|
mappedCascadedModelsMap,
|
||||||
: this.__joinerConfig
|
CommonEvents.CREATED,
|
||||||
) as ModuleJoinerConfig
|
sharedContext
|
||||||
|
|
||||||
Object.entries(mappedCascadedModelsMap).forEach(
|
|
||||||
([linkableKey, ids]) => {
|
|
||||||
const entity = joinerConfig.linkableKeys?.[linkableKey]!
|
|
||||||
if (entity) {
|
|
||||||
const linkableKeyEntity =
|
|
||||||
camelToSnakeCase(entity).toLowerCase()
|
|
||||||
klassPrototype.aggregatedEvents.bind(this)({
|
|
||||||
action: CommonEvents.CREATED,
|
|
||||||
object: linkableKeyEntity,
|
|
||||||
data: { id: ids },
|
|
||||||
context: sharedContext,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -52,7 +52,7 @@ export const buildProductAndRelationsData = ({
|
|||||||
options,
|
options,
|
||||||
variants,
|
variants,
|
||||||
collection_id,
|
collection_id,
|
||||||
}: Partial<ProductTypes.CreateProductDTO>) => {
|
}: Partial<ProductTypes.CreateProductDTO> & { tags?: { value: string }[] }) => {
|
||||||
const defaultOptionTitle = "test-option"
|
const defaultOptionTitle = "test-option"
|
||||||
const defaultOptionValue = "test-value"
|
const defaultOptionValue = "test-value"
|
||||||
|
|
||||||
|
|||||||
+100
-1
@@ -30,7 +30,7 @@ import {
|
|||||||
createTypes,
|
createTypes,
|
||||||
} from "../../__fixtures__/product"
|
} from "../../__fixtures__/product"
|
||||||
|
|
||||||
jest.setTimeout(3000000)
|
jest.setTimeout(300000)
|
||||||
|
|
||||||
moduleIntegrationTestRunner<IProductModuleService>({
|
moduleIntegrationTestRunner<IProductModuleService>({
|
||||||
moduleName: Modules.PRODUCT,
|
moduleName: Modules.PRODUCT,
|
||||||
@@ -975,6 +975,85 @@ moduleIntegrationTestRunner<IProductModuleService>({
|
|||||||
const data = buildProductAndRelationsData({
|
const data = buildProductAndRelationsData({
|
||||||
images,
|
images,
|
||||||
thumbnail: images[0].url,
|
thumbnail: images[0].url,
|
||||||
|
options: [
|
||||||
|
{ title: "size", values: ["large", "small"] },
|
||||||
|
{ title: "color", values: ["red", "blue"] },
|
||||||
|
{ title: "material", values: ["cotton", "polyester"] },
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "Large Red Cotton",
|
||||||
|
sku: "LRG-RED-CTN",
|
||||||
|
options: {
|
||||||
|
size: "large",
|
||||||
|
color: "red",
|
||||||
|
material: "cotton",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Large Red Polyester",
|
||||||
|
sku: "LRG-RED-PLY",
|
||||||
|
options: {
|
||||||
|
size: "large",
|
||||||
|
color: "red",
|
||||||
|
material: "polyester",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Large Blue Cotton",
|
||||||
|
sku: "LRG-BLU-CTN",
|
||||||
|
options: {
|
||||||
|
size: "large",
|
||||||
|
color: "blue",
|
||||||
|
material: "cotton",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Large Blue Polyester",
|
||||||
|
sku: "LRG-BLU-PLY",
|
||||||
|
options: {
|
||||||
|
size: "large",
|
||||||
|
color: "blue",
|
||||||
|
material: "polyester",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Small Red Cotton",
|
||||||
|
sku: "SML-RED-CTN",
|
||||||
|
options: {
|
||||||
|
size: "small",
|
||||||
|
color: "red",
|
||||||
|
material: "cotton",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Small Red Polyester",
|
||||||
|
sku: "SML-RED-PLY",
|
||||||
|
options: {
|
||||||
|
size: "small",
|
||||||
|
color: "red",
|
||||||
|
material: "polyester",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Small Blue Cotton",
|
||||||
|
sku: "SML-BLU-CTN",
|
||||||
|
options: {
|
||||||
|
size: "small",
|
||||||
|
color: "blue",
|
||||||
|
material: "cotton",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Small Blue Polyester",
|
||||||
|
sku: "SML-BLU-PLY",
|
||||||
|
options: {
|
||||||
|
size: "small",
|
||||||
|
color: "blue",
|
||||||
|
material: "polyester",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const products = await service.createProducts([data])
|
const products = await service.createProducts([data])
|
||||||
@@ -1074,6 +1153,26 @@ moduleIntegrationTestRunner<IProductModuleService>({
|
|||||||
source: Modules.PRODUCT,
|
source: Modules.PRODUCT,
|
||||||
action: CommonEvents.DELETED,
|
action: CommonEvents.DELETED,
|
||||||
}),
|
}),
|
||||||
|
composeMessage(ProductEvents.PRODUCT_VARIANT_DELETED, {
|
||||||
|
data: { id: [products[0].variants[0].id] },
|
||||||
|
object: "product_variant",
|
||||||
|
source: Modules.PRODUCT,
|
||||||
|
action: CommonEvents.DELETED,
|
||||||
|
}),
|
||||||
|
composeMessage(ProductEvents.PRODUCT_OPTION_DELETED, {
|
||||||
|
data: { id: [products[0].options[0].id] },
|
||||||
|
object: "product_option",
|
||||||
|
source: Modules.PRODUCT,
|
||||||
|
action: CommonEvents.DELETED,
|
||||||
|
}),
|
||||||
|
composeMessage(ProductEvents.PRODUCT_OPTION_VALUE_DELETED, {
|
||||||
|
data: {
|
||||||
|
id: [products[0].options[0].values[0].id],
|
||||||
|
},
|
||||||
|
object: "product_option_value",
|
||||||
|
source: Modules.PRODUCT,
|
||||||
|
action: CommonEvents.DELETED,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
internal: true,
|
internal: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user