### What Add workflow for refreshing a payment collection. The idea is that on all cart updates, we want two things to happen (in the context of payments) 1. the currently active payment sessions should be destroyed, and 2. the payment collection should be updated with the new cart total. We do this to ensure that we always collect the correct payment amount. From a customer perspective, this would mean that every time something on the cart is updated, the customer would need to enter their payment details anew. To me, this is a good tradeoff to avoid inconsistencies with payment collection. Additionally, I updated the Payment Module interface with `upsert` and `updated` following our established convention. ### Note This PR depends on a fix to the `remoteJoiner` that @carlos-r-l-rodrigues is working on. Update: Fix merged in #6602 Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com>
178 lines
4.6 KiB
TypeScript
178 lines
4.6 KiB
TypeScript
import { buildQuery } from "../../modules-sdk"
|
|
import { EntityMetadata, FindOptions, wrap } from "@mikro-orm/core"
|
|
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
|
|
|
function detectCircularDependency(
|
|
manager: SqlEntityManager,
|
|
entityMetadata: EntityMetadata,
|
|
visited: Set<string> = new Set(),
|
|
shouldStop: boolean = false
|
|
) {
|
|
if (shouldStop) {
|
|
return
|
|
}
|
|
|
|
visited.add(entityMetadata.className)
|
|
|
|
const relations = entityMetadata.relations
|
|
const relationsToCascade = relations.filter((relation) =>
|
|
relation.cascade.includes("soft-remove" as any)
|
|
)
|
|
|
|
for (const relation of relationsToCascade) {
|
|
const branchVisited = new Set(Array.from(visited))
|
|
|
|
const isSelfCircularDependency = entityMetadata.class === relation.entity()
|
|
|
|
if (!isSelfCircularDependency && branchVisited.has(relation.name)) {
|
|
const dependencies = Array.from(visited)
|
|
dependencies.push(entityMetadata.className)
|
|
const circularDependencyStr = dependencies.join(" -> ")
|
|
|
|
throw new Error(
|
|
`Unable to soft delete the ${relation.name}. Circular dependency detected: ${circularDependencyStr}`
|
|
)
|
|
}
|
|
branchVisited.add(relation.name)
|
|
|
|
const relationEntityMetadata = manager
|
|
.getDriver()
|
|
.getMetadata()
|
|
.get(relation.type)
|
|
|
|
detectCircularDependency(
|
|
manager,
|
|
relationEntityMetadata,
|
|
branchVisited,
|
|
isSelfCircularDependency
|
|
)
|
|
}
|
|
}
|
|
|
|
async function performCascadingSoftDeletion<T>(
|
|
manager: SqlEntityManager,
|
|
entity: T & { id: string; deleted_at?: string | Date | null },
|
|
value: Date | null
|
|
) {
|
|
if (!("deleted_at" in entity)) return
|
|
|
|
entity.deleted_at = value
|
|
|
|
const entityName = entity.constructor.name
|
|
|
|
const relations = manager.getDriver().getMetadata().get(entityName).relations
|
|
|
|
const relationsToCascade = relations.filter((relation) =>
|
|
relation.cascade.includes("soft-remove" as any)
|
|
)
|
|
|
|
for (const relation of relationsToCascade) {
|
|
let entityRelation = entity[relation.name]
|
|
|
|
// Handle optional relationships
|
|
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>
|
|
)
|
|
}
|
|
|
|
if (!entityRelation) {
|
|
// Fixes the case of many to many through pivot table
|
|
entityRelation = await retrieveEntity()
|
|
if (!entityRelation) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
const isCollection = "toArray" in entityRelation
|
|
let relationEntities: any[] = []
|
|
|
|
if (isCollection) {
|
|
if (!entityRelation.isInitialized()) {
|
|
entityRelation = await retrieveEntity()
|
|
entityRelation = entityRelation[relation.name]
|
|
}
|
|
relationEntities = entityRelation.getItems()
|
|
} else {
|
|
const wrappedEntity = wrap(entityRelation)
|
|
const initializedEntityRelation = wrappedEntity.isInitialized()
|
|
? entityRelation
|
|
: await wrap(entityRelation).init()
|
|
relationEntities = [initializedEntityRelation]
|
|
}
|
|
|
|
if (!relationEntities.length) {
|
|
continue
|
|
}
|
|
|
|
await mikroOrmUpdateDeletedAtRecursively(manager, relationEntities, value)
|
|
}
|
|
|
|
await manager.persist(entity)
|
|
}
|
|
|
|
export const mikroOrmUpdateDeletedAtRecursively = async <
|
|
T extends object = any
|
|
>(
|
|
manager: SqlEntityManager,
|
|
entities: (T & { id: string; deleted_at?: string | Date | null })[],
|
|
value: Date | null
|
|
) => {
|
|
for (const entity of entities) {
|
|
const entityMetadata = manager
|
|
.getDriver()
|
|
.getMetadata()
|
|
.get(entity.constructor.name)
|
|
detectCircularDependency(manager, entityMetadata)
|
|
await performCascadingSoftDeletion(manager, entity, value)
|
|
}
|
|
}
|
|
|
|
export const mikroOrmSerializer = async <TOutput extends object>(
|
|
data: any,
|
|
options?: any
|
|
): Promise<TOutput> => {
|
|
options ??= {}
|
|
|
|
const data_ = (Array.isArray(data) ? data : [data]).filter(Boolean)
|
|
|
|
const forSerialization: unknown[] = []
|
|
const notForSerialization: unknown[] = []
|
|
|
|
data_.forEach((object) => {
|
|
if (object.__meta) {
|
|
return forSerialization.push(object)
|
|
}
|
|
|
|
return notForSerialization.push(object)
|
|
})
|
|
|
|
const { serialize } = await import("@mikro-orm/core")
|
|
let result: any = serialize(forSerialization, {
|
|
forceObject: true,
|
|
populate: true,
|
|
...options,
|
|
}) as TOutput[]
|
|
|
|
if (notForSerialization.length) {
|
|
result = result.concat(notForSerialization)
|
|
}
|
|
|
|
return Array.isArray(data) ? result : result[0]
|
|
}
|