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:
12 files changed
+432
-439
No files matched your search
+9
-32
@@ -1,12 +1,15 @@
|
|||||||
import BaseService from "../base-service"
|
import { EntityManager } from "typeorm"
|
||||||
import { In, Not } from "typeorm"
|
|
||||||
import { MockManager } from "medusa-test-utils"
|
import { MockManager } from "medusa-test-utils"
|
||||||
|
import { TransactionBaseService } from "../transaction-base-service"
|
||||||
|
|
||||||
describe("BaseService", () => {
|
describe("TransactionBaseService", () => {
|
||||||
it("should cloned the child class withTransaction", () => {
|
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) {
|
constructor(protected readonly container) {
|
||||||
super(container, {});
|
super(container);
|
||||||
this.container = container
|
this.container = container
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,30 +35,4 @@ describe("BaseService", () => {
|
|||||||
expect(child2.getTransactionManager()).toBeTruthy()
|
expect(child2.getTransactionManager()).toBeTruthy()
|
||||||
expect((child2.getTransactionManager() as any)?.testProp).toBe('testProp')
|
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 "./tax-calculation-strategy"
|
||||||
export * from "./cart-completion-strategy"
|
export * from "./cart-completion-strategy"
|
||||||
export * from "./tax-service"
|
export * from "./tax-service"
|
||||||
export * from "./base-service"
|
export * from "./transaction-base-service"
|
||||||
@@ -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 _ from "lodash"
|
||||||
import { MedusaError, Validator } from "medusa-core-utils"
|
import { MedusaError, Validator } from "medusa-core-utils"
|
||||||
import { BaseService } from "medusa-interfaces"
|
|
||||||
import { DeepPartial, EntityManager, In } from "typeorm"
|
import { DeepPartial, EntityManager, In } from "typeorm"
|
||||||
import { IPriceSelectionStrategy } from "../interfaces/price-selection-strategy"
|
import { IPriceSelectionStrategy } from "../interfaces/price-selection-strategy"
|
||||||
import { Address } from "../models/address"
|
import { Address } from "../models/address"
|
||||||
@@ -37,6 +36,8 @@ import InventoryService from "./inventory"
|
|||||||
import CustomShippingOptionService from "./custom-shipping-option"
|
import CustomShippingOptionService from "./custom-shipping-option"
|
||||||
import LineItemAdjustmentService from "./line-item-adjustment"
|
import LineItemAdjustmentService from "./line-item-adjustment"
|
||||||
import { LineItemRepository } from "../repositories/line-item"
|
import { LineItemRepository } from "../repositories/line-item"
|
||||||
|
import { TransactionBaseService } from "../interfaces"
|
||||||
|
import { buildQuery, setMetadata, validateId } from "../utils"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
@@ -70,14 +71,16 @@ type TotalsConfig = {
|
|||||||
/* Provides layer to manipulate carts.
|
/* Provides layer to manipulate carts.
|
||||||
* @implements BaseService
|
* @implements BaseService
|
||||||
*/
|
*/
|
||||||
class CartService extends BaseService {
|
class CartService extends TransactionBaseService<CartService> {
|
||||||
static Events = {
|
static readonly Events = {
|
||||||
CUSTOMER_UPDATED: "cart.customer_updated",
|
CUSTOMER_UPDATED: "cart.customer_updated",
|
||||||
CREATED: "cart.created",
|
CREATED: "cart.created",
|
||||||
UPDATED: "cart.updated",
|
UPDATED: "cart.updated",
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly manager_: EntityManager
|
protected manager_: EntityManager
|
||||||
|
protected transactionManager_: EntityManager | undefined
|
||||||
|
|
||||||
protected readonly shippingMethodRepository_: typeof ShippingMethodRepository
|
protected readonly shippingMethodRepository_: typeof ShippingMethodRepository
|
||||||
protected readonly cartRepository_: typeof CartRepository
|
protected readonly cartRepository_: typeof CartRepository
|
||||||
protected readonly addressRepository_: typeof AddressRepository
|
protected readonly addressRepository_: typeof AddressRepository
|
||||||
@@ -124,7 +127,8 @@ class CartService extends BaseService {
|
|||||||
lineItemAdjustmentService,
|
lineItemAdjustmentService,
|
||||||
priceSelectionStrategy,
|
priceSelectionStrategy,
|
||||||
}: InjectedDependencies) {
|
}: InjectedDependencies) {
|
||||||
super()
|
// eslint-disable-next-line prefer-rest-params
|
||||||
|
super(arguments[0])
|
||||||
|
|
||||||
this.manager_ = manager
|
this.manager_ = manager
|
||||||
this.shippingMethodRepository_ = shippingMethodRepository
|
this.shippingMethodRepository_ = shippingMethodRepository
|
||||||
@@ -150,41 +154,6 @@ class CartService extends BaseService {
|
|||||||
this.priceSelectionStrategy_ = priceSelectionStrategy
|
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_(
|
protected transformQueryForTotals_(
|
||||||
config: FindConfig<Cart>
|
config: FindConfig<Cart>
|
||||||
): FindConfig<Cart> & { totalsToSelect: TotalField[] } {
|
): FindConfig<Cart> & { totalsToSelect: TotalField[] } {
|
||||||
@@ -292,7 +261,7 @@ class CartService extends BaseService {
|
|||||||
this.cartRepository_
|
this.cartRepository_
|
||||||
)
|
)
|
||||||
|
|
||||||
const query = this.buildQuery_(selector, config)
|
const query = buildQuery<Cart>(selector, config)
|
||||||
return await cartRepo.find(query)
|
return await cartRepo.find(query)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -315,12 +284,12 @@ class CartService extends BaseService {
|
|||||||
const cartRepo = transactionManager.getCustomRepository(
|
const cartRepo = transactionManager.getCustomRepository(
|
||||||
this.cartRepository_
|
this.cartRepository_
|
||||||
)
|
)
|
||||||
const validatedId = this.validateId_(cartId)
|
const validatedId = validateId(cartId)
|
||||||
|
|
||||||
const { select, relations, totalsToSelect } =
|
const { select, relations, totalsToSelect } =
|
||||||
this.transformQueryForTotals_(options)
|
this.transformQueryForTotals_(options)
|
||||||
|
|
||||||
const query = this.buildQuery_(
|
const query = buildQuery<Cart>(
|
||||||
{ id: validatedId },
|
{ id: validatedId },
|
||||||
{ ...options, select, relations }
|
{ ...options, select, relations }
|
||||||
)
|
)
|
||||||
@@ -714,16 +683,20 @@ class CartService extends BaseService {
|
|||||||
* @param shouldAdd - flag to indicate, if we should add or remove
|
* @param shouldAdd - flag to indicate, if we should add or remove
|
||||||
* @return void
|
* @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) {
|
if (cart.shipping_methods?.length) {
|
||||||
const shippingMethodRepository =
|
const shippingMethodRepository = transactionManager.getCustomRepository(
|
||||||
this.transactionManager_.getCustomRepository(
|
this.shippingMethodRepository_
|
||||||
this.shippingMethodRepository_
|
)
|
||||||
)
|
|
||||||
|
|
||||||
// if any free shipping discounts, we ensure to update shipping method amount
|
// if any free shipping discounts, we ensure to update shipping method amount
|
||||||
if (shouldAdd) {
|
if (shouldAdd) {
|
||||||
return shippingMethodRepository.update(
|
await shippingMethodRepository.update(
|
||||||
{
|
{
|
||||||
id: In(
|
id: In(
|
||||||
cart.shipping_methods.map((shippingMethod) => shippingMethod.id)
|
cart.shipping_methods.map((shippingMethod) => shippingMethod.id)
|
||||||
@@ -855,8 +828,8 @@ class CartService extends BaseService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("metadata" in data) {
|
if (data?.metadata) {
|
||||||
cart.metadata = this.setMetadata_(cart, data.metadata)
|
cart.metadata = setMetadata(cart, data.metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("context" in data) {
|
if ("context" in data) {
|
||||||
@@ -1352,7 +1325,7 @@ class CartService extends BaseService {
|
|||||||
* @param cartOrCartId - the id of the cart to set payment session for
|
* @param cartOrCartId - the id of the cart to set payment session for
|
||||||
* @return the result of the update operation.
|
* @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_(
|
return await this.atomicPhase_(
|
||||||
async (transactionManager: EntityManager) => {
|
async (transactionManager: EntityManager) => {
|
||||||
const psRepo = transactionManager.getCustomRepository(
|
const psRepo = transactionManager.getCustomRepository(
|
||||||
@@ -1679,6 +1652,8 @@ class CartService extends BaseService {
|
|||||||
regionId?: string,
|
regionId?: string,
|
||||||
customer_id?: string
|
customer_id?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||||
|
|
||||||
// If the cart contains items, we update the price of the items
|
// If the cart contains items, we update the price of the items
|
||||||
// to match the updated region or customer id (keeping the old
|
// to match the updated region or customer id (keeping the old
|
||||||
// value if it exists)
|
// value if it exists)
|
||||||
@@ -1693,7 +1668,7 @@ class CartService extends BaseService {
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
cart.items.map(async (item) => {
|
cart.items.map(async (item) => {
|
||||||
const availablePrice = await this.priceSelectionStrategy_
|
const availablePrice = await this.priceSelectionStrategy_
|
||||||
.withTransaction(this.transactionManager_)
|
.withTransaction(transactionManager)
|
||||||
.calculateVariantPrice(item.variant_id, {
|
.calculateVariantPrice(item.variant_id, {
|
||||||
region_id: region.id,
|
region_id: region.id,
|
||||||
currency_code: region.currency_code,
|
currency_code: region.currency_code,
|
||||||
@@ -1708,14 +1683,14 @@ class CartService extends BaseService {
|
|||||||
availablePrice.calculatedPrice !== null
|
availablePrice.calculatedPrice !== null
|
||||||
) {
|
) {
|
||||||
return this.lineItemService_
|
return this.lineItemService_
|
||||||
.withTransaction(this.transactionManager_)
|
.withTransaction(transactionManager)
|
||||||
.update(item.id, {
|
.update(item.id, {
|
||||||
has_shipping: false,
|
has_shipping: false,
|
||||||
unit_price: availablePrice.calculatedPrice,
|
unit_price: availablePrice.calculatedPrice,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await this.lineItemService_
|
await this.lineItemService_
|
||||||
.withTransaction(this.transactionManager_)
|
.withTransaction(transactionManager)
|
||||||
.delete(item.id)
|
.delete(item.id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1737,6 +1712,8 @@ class CartService extends BaseService {
|
|||||||
regionId: string,
|
regionId: string,
|
||||||
countryCode: string | null
|
countryCode: string | null
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||||
|
|
||||||
if (cart.completed_at || cart.payment_authorized_at) {
|
if (cart.completed_at || cart.payment_authorized_at) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.NOT_ALLOWED,
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
@@ -1750,7 +1727,7 @@ class CartService extends BaseService {
|
|||||||
.retrieve(regionId, {
|
.retrieve(regionId, {
|
||||||
relations: ["countries"],
|
relations: ["countries"],
|
||||||
})
|
})
|
||||||
const addrRepo = this.transactionManager_.getCustomRepository(
|
const addrRepo = transactionManager.getCustomRepository(
|
||||||
this.addressRepository_
|
this.addressRepository_
|
||||||
)
|
)
|
||||||
cart.region = region
|
cart.region = region
|
||||||
@@ -1828,7 +1805,7 @@ class CartService extends BaseService {
|
|||||||
// new shipping method
|
// new shipping method
|
||||||
if (cart.shipping_methods && cart.shipping_methods.length) {
|
if (cart.shipping_methods && cart.shipping_methods.length) {
|
||||||
await this.shippingOptionService_
|
await this.shippingOptionService_
|
||||||
.withTransaction(this.transactionManager_)
|
.withTransaction(transactionManager)
|
||||||
.deleteShippingMethods(cart.shipping_methods)
|
.deleteShippingMethods(cart.shipping_methods)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1841,7 +1818,7 @@ class CartService extends BaseService {
|
|||||||
cart.gift_cards = []
|
cart.gift_cards = []
|
||||||
|
|
||||||
if (cart.payment_sessions && cart.payment_sessions.length) {
|
if (cart.payment_sessions && cart.payment_sessions.length) {
|
||||||
const paymentSessionRepo = this.transactionManager_.getCustomRepository(
|
const paymentSessionRepo = transactionManager.getCustomRepository(
|
||||||
this.paymentSessionRepository_
|
this.paymentSessionRepository_
|
||||||
)
|
)
|
||||||
await paymentSessionRepo.delete({
|
await paymentSessionRepo.delete({
|
||||||
@@ -1859,7 +1836,7 @@ class CartService extends BaseService {
|
|||||||
* @param cartId - the id of the cart to delete
|
* @param cartId - the id of the cart to delete
|
||||||
* @return the deleted cart or undefined if the cart was not found.
|
* @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_(
|
return await this.atomicPhase_(
|
||||||
async (transactionManager: EntityManager) => {
|
async (transactionManager: EntityManager) => {
|
||||||
const cart = await this.retrieve(cartId, {
|
const cart = await this.retrieve(cartId, {
|
||||||
@@ -1913,7 +1890,7 @@ class CartService extends BaseService {
|
|||||||
this.cartRepository_
|
this.cartRepository_
|
||||||
)
|
)
|
||||||
|
|
||||||
const validatedId = this.validateId_(cartId)
|
const validatedId = validateId(cartId)
|
||||||
if (typeof key !== "string") {
|
if (typeof key !== "string") {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_ARGUMENT,
|
MedusaError.Types.INVALID_ARGUMENT,
|
||||||
@@ -1972,20 +1949,22 @@ class CartService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async refreshAdjustments_(cart: Cart): Promise<void> {
|
protected async refreshAdjustments_(cart: Cart): Promise<void> {
|
||||||
|
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||||
|
|
||||||
const nonReturnLineIDs = cart.items
|
const nonReturnLineIDs = cart.items
|
||||||
.filter((item) => !item.is_return)
|
.filter((item) => !item.is_return)
|
||||||
.map((i) => i.id)
|
.map((i) => i.id)
|
||||||
|
|
||||||
// delete all old non return line item adjustments
|
// delete all old non return line item adjustments
|
||||||
await this.lineItemAdjustmentService_
|
await this.lineItemAdjustmentService_
|
||||||
.withTransaction(this.transactionManager_)
|
.withTransaction(transactionManager)
|
||||||
.delete({
|
.delete({
|
||||||
item_id: nonReturnLineIDs,
|
item_id: nonReturnLineIDs,
|
||||||
})
|
})
|
||||||
|
|
||||||
// potentially create/update line item adjustments
|
// potentially create/update line item adjustments
|
||||||
await this.lineItemAdjustmentService_
|
await this.lineItemAdjustmentService_
|
||||||
.withTransaction(this.transactionManager_)
|
.withTransaction(transactionManager)
|
||||||
.createAdjustments(cart)
|
.createAdjustments(cart)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2001,7 +1980,7 @@ class CartService extends BaseService {
|
|||||||
const cartRepo = transactionManager.getCustomRepository(
|
const cartRepo = transactionManager.getCustomRepository(
|
||||||
this.cartRepository_
|
this.cartRepository_
|
||||||
)
|
)
|
||||||
const validatedId = this.validateId_(cartId)
|
const validatedId = validateId(cartId)
|
||||||
|
|
||||||
if (typeof key !== "string") {
|
if (typeof key !== "string") {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
|
|||||||
@@ -71,5 +71,5 @@ export type CartUpdateProps = {
|
|||||||
discounts?: Discount[]
|
discounts?: Discount[]
|
||||||
customer_id?: string
|
customer_id?: string
|
||||||
context?: object
|
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]
|
[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 =
|
export type TotalField =
|
||||||
| "shipping_total"
|
| "shipping_total"
|
||||||
| "discount_total"
|
| "discount_total"
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './build-query'
|
||||||
|
export * from './set-metadata'
|
||||||
|
export * from './validate-id'
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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