Merge pull request #1344 from adrien2p/feat-improveNewBaseService
feat(medusa): Move BaseService to the core and refactor it as part of the new TransactionBaseService
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
import BaseService from "../base-service"
|
||||
import { In, Not } from "typeorm"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { MockManager } from "medusa-test-utils"
|
||||
import { TransactionBaseService } from "../transaction-base-service"
|
||||
|
||||
describe("BaseService", () => {
|
||||
describe("TransactionBaseService", () => {
|
||||
it("should cloned the child class withTransaction", () => {
|
||||
class Child extends BaseService<Child> {
|
||||
class Child extends TransactionBaseService<Child> {
|
||||
protected manager_!: EntityManager
|
||||
protected transactionManager_!: EntityManager
|
||||
|
||||
constructor(protected readonly container) {
|
||||
super(container, {});
|
||||
super(container);
|
||||
this.container = container
|
||||
}
|
||||
|
||||
@@ -32,30 +35,4 @@ describe("BaseService", () => {
|
||||
expect(child2.getTransactionManager()).toBeTruthy()
|
||||
expect((child2.getTransactionManager() as any)?.testProp).toBe('testProp')
|
||||
})
|
||||
|
||||
describe("buildQuery_", () => {
|
||||
const baseService = new BaseService({}, {})
|
||||
|
||||
it("successfully creates query", () => {
|
||||
const q = baseService.buildQuery_(
|
||||
{
|
||||
id: "1234",
|
||||
test1: ["123", "12", "1"],
|
||||
test2: Not("this"),
|
||||
},
|
||||
{
|
||||
relations: ["1234"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(q).toEqual({
|
||||
where: {
|
||||
id: "1234",
|
||||
test1: In(["123", "12", "1"]),
|
||||
test2: Not("this"),
|
||||
},
|
||||
relations: ["1234"],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,342 +0,0 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { EntityManager, FindOperator, In, Raw } from "typeorm"
|
||||
import { IsolationLevel } from "typeorm/driver/types/IsolationLevel"
|
||||
import { FindConfig } from "../types/common"
|
||||
|
||||
type Selector<TEntity> = { [key in keyof TEntity]?: unknown }
|
||||
|
||||
/**
|
||||
* Common functionality for Services
|
||||
* @interface
|
||||
*/
|
||||
class BaseService<
|
||||
TChild extends BaseService<TChild, TContainer>,
|
||||
TContainer = unknown
|
||||
> {
|
||||
protected transactionManager_: EntityManager | undefined
|
||||
protected manager_: EntityManager
|
||||
private readonly container_: TContainer
|
||||
|
||||
constructor(
|
||||
container: TContainer,
|
||||
protected readonly configModule: Record<string, unknown>
|
||||
) {
|
||||
this.container_ = container
|
||||
}
|
||||
|
||||
withTransaction(): this
|
||||
withTransaction(transactionManager: EntityManager): TChild
|
||||
withTransaction(transactionManager?: EntityManager): this | TChild {
|
||||
if (!transactionManager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new (<typeof BaseService>this.constructor)<
|
||||
TChild,
|
||||
TContainer
|
||||
>(
|
||||
{
|
||||
...this.container_,
|
||||
manager: transactionManager,
|
||||
},
|
||||
this.configModule
|
||||
)
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
|
||||
return cloned as TChild
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to build TypeORM queries.
|
||||
* @param selector The selector
|
||||
* @param config The config
|
||||
* @return The QueryBuilderConfig
|
||||
*/
|
||||
buildQuery_<TEntity = unknown>(
|
||||
selector: Selector<TEntity>,
|
||||
config: FindConfig<TEntity> = {}
|
||||
): FindConfig<TEntity> & {
|
||||
where: { [key in keyof TEntity]?: unknown }
|
||||
withDeleted?: boolean
|
||||
} {
|
||||
const build = (
|
||||
obj: Record<string, unknown>
|
||||
): { [key in keyof TEntity]?: unknown } => {
|
||||
return Object.entries(obj).reduce((acc, [key, value]: any) => {
|
||||
// Undefined values indicate that they have no significance to the query.
|
||||
// If the query is looking for rows where a column is not set it should use null instead of undefined
|
||||
if (typeof value === "undefined") {
|
||||
return acc
|
||||
}
|
||||
|
||||
const subquery: {
|
||||
operator: "<" | ">" | "<=" | ">="
|
||||
value: unknown
|
||||
}[] = []
|
||||
|
||||
switch (true) {
|
||||
case value instanceof FindOperator:
|
||||
acc[key] = value
|
||||
break
|
||||
case Array.isArray(value):
|
||||
acc[key] = In([...(value as unknown[])])
|
||||
break
|
||||
case value !== null && typeof value === "object":
|
||||
Object.entries(value as Record<string, unknown>).map(
|
||||
([modifier, val]) => {
|
||||
switch (modifier) {
|
||||
case "lt":
|
||||
subquery.push({ operator: "<", value: val })
|
||||
break
|
||||
case "gt":
|
||||
subquery.push({ operator: ">", value: val })
|
||||
break
|
||||
case "lte":
|
||||
subquery.push({ operator: "<=", value: val })
|
||||
break
|
||||
case "gte":
|
||||
subquery.push({ operator: ">=", value: val })
|
||||
break
|
||||
default:
|
||||
acc[key] = value
|
||||
break
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (subquery.length) {
|
||||
acc[key] = Raw(
|
||||
(a) =>
|
||||
subquery
|
||||
.map((s, index) => `${a} ${s.operator} :${index}`)
|
||||
.join(" AND "),
|
||||
subquery.map((s) => s.value)
|
||||
)
|
||||
}
|
||||
break
|
||||
default:
|
||||
acc[key] = value
|
||||
break
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {} as { [key in keyof TEntity]?: unknown })
|
||||
}
|
||||
|
||||
const query: FindConfig<TEntity> & {
|
||||
where: { [key in keyof TEntity]?: unknown }
|
||||
withDeleted?: boolean
|
||||
} = {
|
||||
where: build(selector),
|
||||
}
|
||||
|
||||
if ("deleted_at" in selector) {
|
||||
query.withDeleted = true
|
||||
}
|
||||
|
||||
if ("skip" in config) {
|
||||
query.skip = config.skip
|
||||
}
|
||||
|
||||
if ("take" in config) {
|
||||
query.take = config.take
|
||||
}
|
||||
|
||||
if ("relations" in config) {
|
||||
query.relations = config.relations
|
||||
}
|
||||
|
||||
if ("select" in config) {
|
||||
query.select = config.select
|
||||
}
|
||||
|
||||
if ("order" in config) {
|
||||
query.order = config.order
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms whether a given raw id is valid. Fails if the provided
|
||||
* id is null or undefined. The validate function takes an optional config
|
||||
* param, to support checking id prefix and length.
|
||||
* @param rawId - the id to validate.
|
||||
* @param config - optional config
|
||||
* @returns the rawId given that nothing failed
|
||||
*/
|
||||
validateId_(
|
||||
rawId: string,
|
||||
config: { prefix?: string; length?: number } = {}
|
||||
): string {
|
||||
const { prefix, length } = config
|
||||
if (!rawId) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Failed to validate id: ${rawId}`
|
||||
)
|
||||
}
|
||||
|
||||
if (prefix || length) {
|
||||
const [pre, rand] = rawId.split("_")
|
||||
if (prefix && pre !== prefix) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`The provided id: ${rawId} does not adhere to prefix constraint: ${prefix}`
|
||||
)
|
||||
}
|
||||
|
||||
if (length && length !== rand.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`The provided id: ${rawId} does not adhere to length constraint: ${length}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return rawId
|
||||
}
|
||||
|
||||
shouldRetryTransaction(
|
||||
err: { code: string } | Record<string, unknown>
|
||||
): boolean {
|
||||
if (!(err as { code: string })?.code) {
|
||||
return false
|
||||
}
|
||||
const code = (err as { code: string })?.code
|
||||
return code === "40001" || code === "40P01"
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps some work within a transactional block. If the service already has
|
||||
* a transaction manager attached this will be reused, otherwise a new
|
||||
* transaction manager is created.
|
||||
* @param work - the transactional work to be done
|
||||
* @param isolationOrErrorHandler - the isolation level to be used for the work.
|
||||
* @param maybeErrorHandlerOrDontFail Potential error handler
|
||||
* @return the result of the transactional work
|
||||
*/
|
||||
async atomicPhase_(
|
||||
work: (transactionManager: EntityManager) => Promise<unknown>,
|
||||
isolationOrErrorHandler?:
|
||||
| IsolationLevel
|
||||
| ((error: unknown) => Promise<unknown>),
|
||||
maybeErrorHandlerOrDontFail?: (error: unknown) => Promise<unknown>
|
||||
): Promise<unknown | never> {
|
||||
let errorHandler = maybeErrorHandlerOrDontFail
|
||||
let isolation:
|
||||
| IsolationLevel
|
||||
| ((error: unknown) => Promise<unknown>)
|
||||
| undefined
|
||||
| null = isolationOrErrorHandler
|
||||
let dontFail = false
|
||||
if (typeof isolationOrErrorHandler === "function") {
|
||||
isolation = null
|
||||
errorHandler = isolationOrErrorHandler
|
||||
dontFail = !!maybeErrorHandlerOrDontFail
|
||||
}
|
||||
|
||||
if (this.transactionManager_) {
|
||||
const doWork = async (m: EntityManager): Promise<unknown | never> => {
|
||||
this.manager_ = m
|
||||
this.transactionManager_ = m
|
||||
try {
|
||||
return await work(m)
|
||||
} catch (error) {
|
||||
if (errorHandler) {
|
||||
const queryRunner = this.transactionManager_.queryRunner
|
||||
if (queryRunner && queryRunner.isTransactionActive) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
}
|
||||
|
||||
await errorHandler(error)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return doWork(this.transactionManager_)
|
||||
} else {
|
||||
const temp = this.manager_
|
||||
const doWork = async (m: EntityManager): Promise<unknown | never> => {
|
||||
this.manager_ = m
|
||||
this.transactionManager_ = m
|
||||
try {
|
||||
const result = await work(m)
|
||||
this.manager_ = temp
|
||||
this.transactionManager_ = undefined
|
||||
return result
|
||||
} catch (error) {
|
||||
this.manager_ = temp
|
||||
this.transactionManager_ = undefined
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
if (isolation) {
|
||||
let result
|
||||
try {
|
||||
result = await this.manager_.transaction(
|
||||
isolation as IsolationLevel,
|
||||
(m) => doWork(m)
|
||||
)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.shouldRetryTransaction(error)) {
|
||||
return this.manager_.transaction(isolation as IsolationLevel, (m) =>
|
||||
doWork(m)
|
||||
)
|
||||
} else {
|
||||
if (errorHandler) {
|
||||
await errorHandler(error)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.manager_.transaction((m) => doWork(m))
|
||||
} catch (error) {
|
||||
if (errorHandler) {
|
||||
const result = await errorHandler(error)
|
||||
if (dontFail) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated method to set metadata.
|
||||
* @param obj - the entity to apply metadata to.
|
||||
* @param metadata - the metadata to set
|
||||
* @return resolves to the updated result.
|
||||
*/
|
||||
setMetadata_(
|
||||
obj: { metadata: Record<string, unknown> },
|
||||
metadata: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
const existing = obj.metadata || {}
|
||||
const newData = {}
|
||||
for (const [key, value] of Object.entries(metadata)) {
|
||||
if (typeof key !== "string") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"Key type is invalid. Metadata keys must be strings"
|
||||
)
|
||||
}
|
||||
newData[key] = value
|
||||
}
|
||||
|
||||
return {
|
||||
...existing,
|
||||
...newData,
|
||||
}
|
||||
}
|
||||
}
|
||||
export default BaseService
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from "./tax-calculation-strategy"
|
||||
export * from "./cart-completion-strategy"
|
||||
export * from "./tax-service"
|
||||
export * from "./base-service"
|
||||
export * from "./transaction-base-service"
|
||||
|
||||
149
packages/medusa/src/interfaces/transaction-base-service.ts
Normal file
149
packages/medusa/src/interfaces/transaction-base-service.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { EntityManager } from "typeorm"
|
||||
import { IsolationLevel } from "typeorm/driver/types/IsolationLevel"
|
||||
|
||||
export abstract class TransactionBaseService<
|
||||
TChild extends TransactionBaseService<TChild, TContainer>,
|
||||
TContainer = unknown
|
||||
> {
|
||||
protected abstract manager_: EntityManager
|
||||
protected abstract transactionManager_: EntityManager | undefined
|
||||
|
||||
protected constructor(
|
||||
protected readonly container: TContainer,
|
||||
protected readonly configModule?: Record<string, unknown>
|
||||
) {}
|
||||
|
||||
withTransaction(transactionManager?: EntityManager): this | TChild {
|
||||
if (!transactionManager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new (<any>this.constructor)(
|
||||
{
|
||||
...this.container,
|
||||
manager: transactionManager,
|
||||
},
|
||||
this.configModule
|
||||
)
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
|
||||
return cloned as TChild
|
||||
}
|
||||
|
||||
protected shouldRetryTransaction_(
|
||||
err: { code: string } | Record<string, unknown>
|
||||
): boolean {
|
||||
if (!(err as { code: string })?.code) {
|
||||
return false
|
||||
}
|
||||
const code = (err as { code: string })?.code
|
||||
return code === "40001" || code === "40P01"
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps some work within a transactional block. If the service already has
|
||||
* a transaction manager attached this will be reused, otherwise a new
|
||||
* transaction manager is created.
|
||||
* @param work - the transactional work to be done
|
||||
* @param isolationOrErrorHandler - the isolation level to be used for the work.
|
||||
* @param maybeErrorHandlerOrDontFail Potential error handler
|
||||
* @return the result of the transactional work
|
||||
*/
|
||||
protected async atomicPhase_<TResult, TError>(
|
||||
work: (transactionManager: EntityManager) => Promise<TResult | never>,
|
||||
isolationOrErrorHandler?:
|
||||
| IsolationLevel
|
||||
| ((error: TError) => Promise<never | TResult | void>),
|
||||
maybeErrorHandlerOrDontFail?: (
|
||||
error: TError
|
||||
) => Promise<never | TResult | void>
|
||||
): Promise<never | TResult> {
|
||||
let errorHandler = maybeErrorHandlerOrDontFail
|
||||
let isolation:
|
||||
| IsolationLevel
|
||||
| ((error: TError) => Promise<never | TResult | void>)
|
||||
| undefined
|
||||
| null = isolationOrErrorHandler
|
||||
let dontFail = false
|
||||
if (typeof isolationOrErrorHandler === "function") {
|
||||
isolation = null
|
||||
errorHandler = isolationOrErrorHandler
|
||||
dontFail = !!maybeErrorHandlerOrDontFail
|
||||
}
|
||||
|
||||
if (this.transactionManager_) {
|
||||
const doWork = async (m: EntityManager): Promise<never | TResult> => {
|
||||
this.manager_ = m
|
||||
this.transactionManager_ = m
|
||||
try {
|
||||
return await work(m)
|
||||
} catch (error) {
|
||||
if (errorHandler) {
|
||||
const queryRunner = this.transactionManager_.queryRunner
|
||||
if (queryRunner && queryRunner.isTransactionActive) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
}
|
||||
|
||||
await errorHandler(error)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return await doWork(this.transactionManager_)
|
||||
} else {
|
||||
const temp = this.manager_
|
||||
const doWork = async (m: EntityManager): Promise<never | TResult> => {
|
||||
this.manager_ = m
|
||||
this.transactionManager_ = m
|
||||
try {
|
||||
const result = await work(m)
|
||||
this.manager_ = temp
|
||||
this.transactionManager_ = undefined
|
||||
return result
|
||||
} catch (error) {
|
||||
this.manager_ = temp
|
||||
this.transactionManager_ = undefined
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
if (isolation && this.manager_) {
|
||||
let result
|
||||
try {
|
||||
result = await this.manager_.transaction(
|
||||
isolation as IsolationLevel,
|
||||
(m) => doWork(m)
|
||||
)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.shouldRetryTransaction_(error)) {
|
||||
return this.manager_.transaction(
|
||||
isolation as IsolationLevel,
|
||||
(m): Promise<never | TResult> => doWork(m)
|
||||
)
|
||||
} else {
|
||||
if (errorHandler) {
|
||||
await errorHandler(error)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.manager_.transaction((m) => doWork(m))
|
||||
} catch (error) {
|
||||
if (errorHandler) {
|
||||
const result = await errorHandler(error)
|
||||
if (dontFail) {
|
||||
return result as TResult
|
||||
}
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import _ from "lodash"
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { DeepPartial, EntityManager, In } from "typeorm"
|
||||
import { IPriceSelectionStrategy } from "../interfaces/price-selection-strategy"
|
||||
import { Address } from "../models/address"
|
||||
@@ -37,6 +36,8 @@ import InventoryService from "./inventory"
|
||||
import CustomShippingOptionService from "./custom-shipping-option"
|
||||
import LineItemAdjustmentService from "./line-item-adjustment"
|
||||
import { LineItemRepository } from "../repositories/line-item"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import { buildQuery, setMetadata, validateId } from "../utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
@@ -70,14 +71,16 @@ type TotalsConfig = {
|
||||
/* Provides layer to manipulate carts.
|
||||
* @implements BaseService
|
||||
*/
|
||||
class CartService extends BaseService {
|
||||
static Events = {
|
||||
class CartService extends TransactionBaseService<CartService> {
|
||||
static readonly Events = {
|
||||
CUSTOMER_UPDATED: "cart.customer_updated",
|
||||
CREATED: "cart.created",
|
||||
UPDATED: "cart.updated",
|
||||
}
|
||||
|
||||
protected readonly manager_: EntityManager
|
||||
protected manager_: EntityManager
|
||||
protected transactionManager_: EntityManager | undefined
|
||||
|
||||
protected readonly shippingMethodRepository_: typeof ShippingMethodRepository
|
||||
protected readonly cartRepository_: typeof CartRepository
|
||||
protected readonly addressRepository_: typeof AddressRepository
|
||||
@@ -124,7 +127,8 @@ class CartService extends BaseService {
|
||||
lineItemAdjustmentService,
|
||||
priceSelectionStrategy,
|
||||
}: InjectedDependencies) {
|
||||
super()
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
|
||||
this.manager_ = manager
|
||||
this.shippingMethodRepository_ = shippingMethodRepository
|
||||
@@ -150,41 +154,6 @@ class CartService extends BaseService {
|
||||
this.priceSelectionStrategy_ = priceSelectionStrategy
|
||||
}
|
||||
|
||||
withTransaction(transactionManager: EntityManager): CartService {
|
||||
if (!transactionManager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new CartService({
|
||||
manager: transactionManager,
|
||||
taxProviderService: this.taxProviderService_,
|
||||
cartRepository: this.cartRepository_,
|
||||
lineItemRepository: this.lineItemRepository_,
|
||||
eventBusService: this.eventBus_,
|
||||
paymentProviderService: this.paymentProviderService_,
|
||||
paymentSessionRepository: this.paymentSessionRepository_,
|
||||
shippingMethodRepository: this.shippingMethodRepository_,
|
||||
productService: this.productService_,
|
||||
productVariantService: this.productVariantService_,
|
||||
regionService: this.regionService_,
|
||||
lineItemService: this.lineItemService_,
|
||||
shippingOptionService: this.shippingOptionService_,
|
||||
customerService: this.customerService_,
|
||||
discountService: this.discountService_,
|
||||
totalsService: this.totalsService_,
|
||||
addressRepository: this.addressRepository_,
|
||||
giftCardService: this.giftCardService_,
|
||||
inventoryService: this.inventoryService_,
|
||||
customShippingOptionService: this.customShippingOptionService_,
|
||||
lineItemAdjustmentService: this.lineItemAdjustmentService_,
|
||||
priceSelectionStrategy: this.priceSelectionStrategy_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
protected transformQueryForTotals_(
|
||||
config: FindConfig<Cart>
|
||||
): FindConfig<Cart> & { totalsToSelect: TotalField[] } {
|
||||
@@ -292,7 +261,7 @@ class CartService extends BaseService {
|
||||
this.cartRepository_
|
||||
)
|
||||
|
||||
const query = this.buildQuery_(selector, config)
|
||||
const query = buildQuery<Cart>(selector, config)
|
||||
return await cartRepo.find(query)
|
||||
}
|
||||
)
|
||||
@@ -315,12 +284,12 @@ class CartService extends BaseService {
|
||||
const cartRepo = transactionManager.getCustomRepository(
|
||||
this.cartRepository_
|
||||
)
|
||||
const validatedId = this.validateId_(cartId)
|
||||
const validatedId = validateId(cartId)
|
||||
|
||||
const { select, relations, totalsToSelect } =
|
||||
this.transformQueryForTotals_(options)
|
||||
|
||||
const query = this.buildQuery_(
|
||||
const query = buildQuery<Cart>(
|
||||
{ id: validatedId },
|
||||
{ ...options, select, relations }
|
||||
)
|
||||
@@ -714,16 +683,20 @@ class CartService extends BaseService {
|
||||
* @param shouldAdd - flag to indicate, if we should add or remove
|
||||
* @return void
|
||||
*/
|
||||
async adjustFreeShipping_(cart: Cart, shouldAdd: boolean): Promise<void> {
|
||||
protected async adjustFreeShipping_(
|
||||
cart: Cart,
|
||||
shouldAdd: boolean
|
||||
): Promise<void> {
|
||||
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||
|
||||
if (cart.shipping_methods?.length) {
|
||||
const shippingMethodRepository =
|
||||
this.transactionManager_.getCustomRepository(
|
||||
this.shippingMethodRepository_
|
||||
)
|
||||
const shippingMethodRepository = transactionManager.getCustomRepository(
|
||||
this.shippingMethodRepository_
|
||||
)
|
||||
|
||||
// if any free shipping discounts, we ensure to update shipping method amount
|
||||
if (shouldAdd) {
|
||||
return shippingMethodRepository.update(
|
||||
await shippingMethodRepository.update(
|
||||
{
|
||||
id: In(
|
||||
cart.shipping_methods.map((shippingMethod) => shippingMethod.id)
|
||||
@@ -855,8 +828,8 @@ class CartService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
if ("metadata" in data) {
|
||||
cart.metadata = this.setMetadata_(cart, data.metadata)
|
||||
if (data?.metadata) {
|
||||
cart.metadata = setMetadata(cart, data.metadata)
|
||||
}
|
||||
|
||||
if ("context" in data) {
|
||||
@@ -1352,7 +1325,7 @@ class CartService extends BaseService {
|
||||
* @param cartOrCartId - the id of the cart to set payment session for
|
||||
* @return the result of the update operation.
|
||||
*/
|
||||
async setPaymentSessions(cartOrCartId: Cart | string): Promise<Cart> {
|
||||
async setPaymentSessions(cartOrCartId: Cart | string): Promise<void> {
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const psRepo = transactionManager.getCustomRepository(
|
||||
@@ -1679,6 +1652,8 @@ class CartService extends BaseService {
|
||||
regionId?: string,
|
||||
customer_id?: string
|
||||
): Promise<void> {
|
||||
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||
|
||||
// If the cart contains items, we update the price of the items
|
||||
// to match the updated region or customer id (keeping the old
|
||||
// value if it exists)
|
||||
@@ -1693,7 +1668,7 @@ class CartService extends BaseService {
|
||||
await Promise.all(
|
||||
cart.items.map(async (item) => {
|
||||
const availablePrice = await this.priceSelectionStrategy_
|
||||
.withTransaction(this.transactionManager_)
|
||||
.withTransaction(transactionManager)
|
||||
.calculateVariantPrice(item.variant_id, {
|
||||
region_id: region.id,
|
||||
currency_code: region.currency_code,
|
||||
@@ -1708,14 +1683,14 @@ class CartService extends BaseService {
|
||||
availablePrice.calculatedPrice !== null
|
||||
) {
|
||||
return this.lineItemService_
|
||||
.withTransaction(this.transactionManager_)
|
||||
.withTransaction(transactionManager)
|
||||
.update(item.id, {
|
||||
has_shipping: false,
|
||||
unit_price: availablePrice.calculatedPrice,
|
||||
})
|
||||
} else {
|
||||
await this.lineItemService_
|
||||
.withTransaction(this.transactionManager_)
|
||||
.withTransaction(transactionManager)
|
||||
.delete(item.id)
|
||||
return
|
||||
}
|
||||
@@ -1737,6 +1712,8 @@ class CartService extends BaseService {
|
||||
regionId: string,
|
||||
countryCode: string | null
|
||||
): Promise<void> {
|
||||
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||
|
||||
if (cart.completed_at || cart.payment_authorized_at) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
@@ -1750,7 +1727,7 @@ class CartService extends BaseService {
|
||||
.retrieve(regionId, {
|
||||
relations: ["countries"],
|
||||
})
|
||||
const addrRepo = this.transactionManager_.getCustomRepository(
|
||||
const addrRepo = transactionManager.getCustomRepository(
|
||||
this.addressRepository_
|
||||
)
|
||||
cart.region = region
|
||||
@@ -1828,7 +1805,7 @@ class CartService extends BaseService {
|
||||
// new shipping method
|
||||
if (cart.shipping_methods && cart.shipping_methods.length) {
|
||||
await this.shippingOptionService_
|
||||
.withTransaction(this.transactionManager_)
|
||||
.withTransaction(transactionManager)
|
||||
.deleteShippingMethods(cart.shipping_methods)
|
||||
}
|
||||
|
||||
@@ -1841,7 +1818,7 @@ class CartService extends BaseService {
|
||||
cart.gift_cards = []
|
||||
|
||||
if (cart.payment_sessions && cart.payment_sessions.length) {
|
||||
const paymentSessionRepo = this.transactionManager_.getCustomRepository(
|
||||
const paymentSessionRepo = transactionManager.getCustomRepository(
|
||||
this.paymentSessionRepository_
|
||||
)
|
||||
await paymentSessionRepo.delete({
|
||||
@@ -1859,7 +1836,7 @@ class CartService extends BaseService {
|
||||
* @param cartId - the id of the cart to delete
|
||||
* @return the deleted cart or undefined if the cart was not found.
|
||||
*/
|
||||
async delete(cartId: string): Promise<string> {
|
||||
async delete(cartId: string): Promise<Cart> {
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const cart = await this.retrieve(cartId, {
|
||||
@@ -1913,7 +1890,7 @@ class CartService extends BaseService {
|
||||
this.cartRepository_
|
||||
)
|
||||
|
||||
const validatedId = this.validateId_(cartId)
|
||||
const validatedId = validateId(cartId)
|
||||
if (typeof key !== "string") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
@@ -1972,20 +1949,22 @@ class CartService extends BaseService {
|
||||
}
|
||||
|
||||
protected async refreshAdjustments_(cart: Cart): Promise<void> {
|
||||
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||
|
||||
const nonReturnLineIDs = cart.items
|
||||
.filter((item) => !item.is_return)
|
||||
.map((i) => i.id)
|
||||
|
||||
// delete all old non return line item adjustments
|
||||
await this.lineItemAdjustmentService_
|
||||
.withTransaction(this.transactionManager_)
|
||||
.withTransaction(transactionManager)
|
||||
.delete({
|
||||
item_id: nonReturnLineIDs,
|
||||
})
|
||||
|
||||
// potentially create/update line item adjustments
|
||||
await this.lineItemAdjustmentService_
|
||||
.withTransaction(this.transactionManager_)
|
||||
.withTransaction(transactionManager)
|
||||
.createAdjustments(cart)
|
||||
}
|
||||
|
||||
@@ -2001,7 +1980,7 @@ class CartService extends BaseService {
|
||||
const cartRepo = transactionManager.getCustomRepository(
|
||||
this.cartRepository_
|
||||
)
|
||||
const validatedId = this.validateId_(cartId)
|
||||
const validatedId = validateId(cartId)
|
||||
|
||||
if (typeof key !== "string") {
|
||||
throw new MedusaError(
|
||||
|
||||
@@ -71,5 +71,5 @@ export type CartUpdateProps = {
|
||||
discounts?: Discount[]
|
||||
customer_id?: string
|
||||
context?: object
|
||||
metadata?: object
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
@@ -13,6 +13,22 @@ export type PartialPick<T, K extends keyof T> = {
|
||||
[P in K]?: T[P]
|
||||
}
|
||||
|
||||
export type Writable<T> = { -readonly [key in keyof T]: T[key] }
|
||||
|
||||
export type ExtendedFindConfig<TEntity> = FindConfig<TEntity> & {
|
||||
where: Partial<Writable<TEntity>>
|
||||
withDeleted?: boolean
|
||||
}
|
||||
|
||||
export type Selector<TEntity> = {
|
||||
[key in keyof TEntity]?:
|
||||
| TEntity[key]
|
||||
| TEntity[key][]
|
||||
| DateComparisonOperator
|
||||
| StringComparisonOperator
|
||||
| NumericalComparisonOperator
|
||||
}
|
||||
|
||||
export type TotalField =
|
||||
| "shipping_total"
|
||||
| "discount_total"
|
||||
|
||||
28
packages/medusa/src/utils/__tests__/build-query.spec.ts
Normal file
28
packages/medusa/src/utils/__tests__/build-query.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { In, Not } from "typeorm"
|
||||
import { buildQuery } from "../build-query"
|
||||
|
||||
describe('buildQuery', () => {
|
||||
describe("buildQuery_", () => {
|
||||
it("successfully creates query", () => {
|
||||
const q = buildQuery(
|
||||
{
|
||||
id: "1234",
|
||||
test1: ["123", "12", "1"],
|
||||
test2: Not("this"),
|
||||
},
|
||||
{
|
||||
relations: ["1234"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(q).toEqual({
|
||||
where: {
|
||||
id: "1234",
|
||||
test1: In(["123", "12", "1"]),
|
||||
test2: Not("this"),
|
||||
},
|
||||
relations: ["1234"],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
113
packages/medusa/src/utils/build-query.ts
Normal file
113
packages/medusa/src/utils/build-query.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
ExtendedFindConfig,
|
||||
FindConfig,
|
||||
Selector,
|
||||
Writable,
|
||||
} from "../types/common"
|
||||
import { FindOperator, In, Raw } from "typeorm"
|
||||
|
||||
/**
|
||||
* Used to build TypeORM queries.
|
||||
* @param selector The selector
|
||||
* @param config The config
|
||||
* @return The QueryBuilderConfig
|
||||
*/
|
||||
export function buildQuery<TEntity = unknown>(
|
||||
selector: Selector<TEntity>,
|
||||
config: FindConfig<TEntity> = {}
|
||||
): ExtendedFindConfig<TEntity> {
|
||||
const build = (
|
||||
obj: Selector<TEntity>
|
||||
): Partial<Writable<TEntity>> => {
|
||||
return Object.entries(obj).reduce((acc, [key, value]: any) => {
|
||||
// Undefined values indicate that they have no significance to the query.
|
||||
// If the query is looking for rows where a column is not set it should use null instead of undefined
|
||||
if (typeof value === "undefined") {
|
||||
return acc
|
||||
}
|
||||
|
||||
const subquery: {
|
||||
operator: "<" | ">" | "<=" | ">="
|
||||
value: unknown
|
||||
}[] = []
|
||||
|
||||
switch (true) {
|
||||
case value instanceof FindOperator:
|
||||
acc[key] = value
|
||||
break
|
||||
case Array.isArray(value):
|
||||
acc[key] = In([...(value as unknown[])])
|
||||
break
|
||||
case value !== null && typeof value === "object":
|
||||
Object.entries(value).map(([modifier, val]) => {
|
||||
switch (modifier) {
|
||||
case "lt":
|
||||
subquery.push({ operator: "<", value: val })
|
||||
break
|
||||
case "gt":
|
||||
subquery.push({ operator: ">", value: val })
|
||||
break
|
||||
case "lte":
|
||||
subquery.push({ operator: "<=", value: val })
|
||||
break
|
||||
case "gte":
|
||||
subquery.push({ operator: ">=", value: val })
|
||||
break
|
||||
default:
|
||||
acc[key] = value
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
if (subquery.length) {
|
||||
acc[key] = Raw(
|
||||
(a) =>
|
||||
subquery
|
||||
.map((s, index) => `${a} ${s.operator} :${index}`)
|
||||
.join(" AND "),
|
||||
subquery.map((s) => s.value)
|
||||
)
|
||||
}
|
||||
break
|
||||
default:
|
||||
acc[key] = value
|
||||
break
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {} as Partial<Writable<TEntity>>)
|
||||
}
|
||||
|
||||
const query: FindConfig<TEntity> & {
|
||||
where: Partial<Writable<TEntity>>
|
||||
withDeleted?: boolean
|
||||
} = {
|
||||
where: build(selector),
|
||||
}
|
||||
|
||||
if ("deleted_at" in selector) {
|
||||
query.withDeleted = true
|
||||
}
|
||||
|
||||
if ("skip" in config) {
|
||||
query.skip = config.skip
|
||||
}
|
||||
|
||||
if ("take" in config) {
|
||||
query.take = config.take
|
||||
}
|
||||
|
||||
if ("relations" in config) {
|
||||
query.relations = config.relations
|
||||
}
|
||||
|
||||
if ("select" in config) {
|
||||
query.select = config.select
|
||||
}
|
||||
|
||||
if ("order" in config) {
|
||||
query.order = config.order
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
3
packages/medusa/src/utils/index.ts
Normal file
3
packages/medusa/src/utils/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './build-query'
|
||||
export * from './set-metadata'
|
||||
export * from './validate-id'
|
||||
29
packages/medusa/src/utils/set-metadata.ts
Normal file
29
packages/medusa/src/utils/set-metadata.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { MedusaError } from "medusa-core-utils/dist"
|
||||
|
||||
/**
|
||||
* Dedicated method to set metadata.
|
||||
* @param obj - the entity to apply metadata to.
|
||||
* @param metadata - the metadata to set
|
||||
* @return resolves to the updated result.
|
||||
*/
|
||||
export function setMetadata(
|
||||
obj: { metadata: Record<string, unknown> },
|
||||
metadata: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
const existing = obj.metadata || {}
|
||||
const newData = {}
|
||||
for (const [key, value] of Object.entries(metadata)) {
|
||||
if (typeof key !== "string") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"Key type is invalid. Metadata keys must be strings"
|
||||
)
|
||||
}
|
||||
newData[key] = value
|
||||
}
|
||||
|
||||
return {
|
||||
...existing,
|
||||
...newData,
|
||||
}
|
||||
}
|
||||
41
packages/medusa/src/utils/validate-id.ts
Normal file
41
packages/medusa/src/utils/validate-id.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Confirms whether a given raw id is valid. Fails if the provided
|
||||
* id is null or undefined. The validate function takes an optional config
|
||||
* param, to support checking id prefix and length.
|
||||
* @param rawId - the id to validate.
|
||||
* @param config - optional config
|
||||
* @returns the rawId given that nothing failed
|
||||
*/
|
||||
import { MedusaError } from "medusa-core-utils/dist"
|
||||
|
||||
export function validateId(
|
||||
rawId: string,
|
||||
config: { prefix?: string; length?: number } = {}
|
||||
): string {
|
||||
const { prefix, length } = config
|
||||
if (!rawId) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Failed to validate id: ${rawId}`
|
||||
)
|
||||
}
|
||||
|
||||
if (prefix || length) {
|
||||
const [pre, rand] = rawId.split("_")
|
||||
if (prefix && pre !== prefix) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`The provided id: ${rawId} does not adhere to prefix constraint: ${prefix}`
|
||||
)
|
||||
}
|
||||
|
||||
if (length && length !== rand.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`The provided id: ${rawId} does not adhere to length constraint: ${length}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return rawId
|
||||
}
|
||||
Reference in New Issue
Block a user