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:
Adrien de Peretti
2022-05-09 17:05:18 +02:00
committed by GitHub
12 changed files with 432 additions and 439 deletions

View File

@@ -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"],
})
})
})
})
})

View File

@@ -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

View File

@@ -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"

View 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
}
}
}
}

View File

@@ -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(

View File

@@ -71,5 +71,5 @@ export type CartUpdateProps = {
discounts?: Discount[]
customer_id?: string
context?: object
metadata?: object
metadata?: Record<string, unknown>
}

View File

@@ -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"

View 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"],
})
})
})
})

View 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
}

View File

@@ -0,0 +1,3 @@
export * from './build-query'
export * from './set-metadata'
export * from './validate-id'

View 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,
}
}

View 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
}