Remove v1-related code from medusa app (#7326)

* chore: Remove unused validations and utilities

* chore: Remove all resources that are not being loaded

* chore: Remove unused dependencies, typeorm related code and fix tests

* chore: Use createAdminUser in all module tests
This commit is contained in:
Stevche Radevski
2024-05-14 13:21:30 +02:00
committed by GitHub
parent 9dae86daa2
commit d680c7ee4c
226 changed files with 110 additions and 13506 deletions
@@ -1,318 +0,0 @@
import {
And,
FindOptionsOrder,
FindOptionsSelect,
In,
LessThanOrEqual,
MoreThan,
MoreThanOrEqual,
Not,
} from "typeorm"
import {
addOrderToSelect,
buildLegacyFieldsListFrom,
buildQuery,
} from "../build-query"
describe("buildQuery", () => {
it("successfully creates query", () => {
const date = new Date()
const q = buildQuery(
{
id: "1234",
test1: ["123", "12", "1"],
test2: Not("this"),
date: { gt: date },
amount: { gt: 10 },
rule: {
type: "fixed",
},
updated_at: {
gte: "value",
lte: "value",
},
},
{
select: [
"order",
"order.items",
"order.swaps",
"order.swaps.additional_items",
"order.discounts",
"order.discounts.rule",
"order.claims",
"order.claims.additional_items",
"additional_items",
"additional_items.variant",
"return_order",
"return_order.items",
"return_order.shipping_method",
"return_order.shipping_method.tax_lines",
],
relations: [
"order",
"order.items",
"order.swaps",
"order.swaps.additional_items",
"order.discounts",
"order.discounts.rule",
"order.claims",
"order.claims.additional_items",
"additional_items",
"additional_items.variant",
"return_order",
"return_order.items",
"return_order.shipping_method",
"return_order.shipping_method.tax_lines",
"items.variants",
"items.variants.product",
"items",
"items.tax_lines",
"items.adjustments",
],
order: {
id: "ASC",
"items.id": "ASC",
"items.variant.id": "ASC",
},
}
)
expect(q).toEqual({
where: {
id: "1234",
test1: In(["123", "12", "1"]),
test2: Not("this"),
date: MoreThan(date),
amount: MoreThan(10),
rule: {
type: "fixed",
},
updated_at: And(MoreThanOrEqual("value"), LessThanOrEqual("value")),
},
select: {
order: {
items: true,
swaps: {
additional_items: true,
},
discounts: {
rule: true,
},
claims: {
additional_items: true,
},
},
additional_items: {
variant: true,
},
return_order: {
items: true,
shipping_method: {
tax_lines: true,
},
},
},
relations: {
order: {
items: true,
swaps: {
additional_items: true,
},
discounts: {
rule: true,
},
claims: {
additional_items: true,
},
},
additional_items: {
variant: true,
},
return_order: {
items: true,
shipping_method: {
tax_lines: true,
},
},
items: {
variants: {
product: true,
},
tax_lines: true,
adjustments: true,
},
},
order: {
id: "ASC",
items: {
id: "ASC",
variant: {
id: "ASC",
},
},
},
})
})
})
describe("buildLegacyFieldsListFrom", () => {
it("successfully build back select object shape to list", () => {
const q = buildLegacyFieldsListFrom({
order: {
items: true,
swaps: {
additional_items: true,
},
discounts: {
rule: true,
},
claims: {
additional_items: true,
},
},
additional_items: {
variant: true,
},
return_order: {
items: true,
shipping_method: {
tax_lines: true,
},
},
})
expect(q.length).toBe(14)
expect(q).toEqual(
expect.arrayContaining([
"order",
"order.items",
"order.swaps",
"order.swaps.additional_items",
"order.discounts",
"order.discounts.rule",
"order.claims",
"order.claims.additional_items",
"additional_items",
"additional_items.variant",
"return_order",
"return_order.items",
"return_order.shipping_method",
"return_order.shipping_method.tax_lines",
])
)
})
it("successfully build back relation object shape to list", () => {
const q = buildLegacyFieldsListFrom({
order: {
items: true,
swaps: {
additional_items: true,
},
discounts: {
rule: true,
},
claims: {
additional_items: true,
},
},
additional_items: {
variant: true,
},
return_order: {
items: true,
shipping_method: {
tax_lines: true,
},
},
items: {
variants: {
product: true,
},
tax_lines: true,
adjustments: true,
},
})
expect(q.length).toBe(19)
expect(q).toEqual(
expect.arrayContaining([
"order",
"order.items",
"order.swaps",
"order.swaps.additional_items",
"order.discounts",
"order.discounts.rule",
"order.claims",
"order.claims.additional_items",
"additional_items",
"additional_items.variant",
"return_order",
"return_order.items",
"return_order.shipping_method",
"return_order.shipping_method.tax_lines",
"items.variants",
"items.variants.product",
"items",
"items.tax_lines",
"items.adjustments",
])
)
})
it("successfully build back order object shape to list", () => {
const q = buildLegacyFieldsListFrom({
id: "ASC",
items: {
id: "ASC",
variant: {
id: "ASC",
},
},
})
expect(q.length).toBe(5)
expect(q).toEqual(
expect.arrayContaining([
"id",
"items",
"items.id",
"items.variant",
"items.variant.id",
])
)
})
describe("addOrderToSelect", function () {
it("successfully add the order fields to the select object", () => {
const select: FindOptionsSelect<any> = {
item: {
variant: {
id: true,
},
},
}
const order: FindOptionsOrder<any> = {
item: {
variant: {
rank: "ASC",
},
},
}
addOrderToSelect(order, select)
expect(select).toEqual({
item: {
variant: {
id: true,
rank: true,
},
},
})
})
})
})
@@ -1,45 +0,0 @@
import { FlagRouter } from "@medusajs/utils"
import { calculatePriceTaxAmount } from "../calculate-price-tax-amount"
describe("calculatePriceTaxAmount", () => {
describe("Calculate taxes from a given price", () => {
beforeAll(() => {
jest.spyOn(FlagRouter.prototype, "isFeatureEnabled").mockReturnValue(true)
})
it("Tax NOT included", () => {
const tax = calculatePriceTaxAmount({
price: 150,
taxRate: 0.19,
includesTax: false,
})
expect(tax).toBeCloseTo(28.5, 2)
const tax2 = calculatePriceTaxAmount({
price: 120,
taxRate: 0.17,
})
expect(tax2).toBeCloseTo(20.4, 2)
})
it("Tax included", () => {
const tax = calculatePriceTaxAmount({
price: 115,
taxRate: 0.15,
includesTax: true,
})
expect(tax).toBeCloseTo(15, 2)
const tax2 = calculatePriceTaxAmount({
price: 2150,
taxRate: 0.17,
includesTax: true,
})
expect(tax2).toBeCloseTo(312.39, 2)
})
})
})
@@ -1,59 +0,0 @@
import { csvCellContentFormatter } from "../csv-cell-content-formatter"
type Case = {
str: string
expected: string
}
const cases: [string, Case][] = [
[
"should return a the exact input content",
{
str: "Hello my name is Adrien and I like writing single line content.",
expected:
'Hello my name is Adrien and I like writing single line content.',
},
],
[
"should return a formatted string escaping the coma",
{
str: "Hello, my name is Adrien and I like writing single line content.",
expected:
'"Hello, my name is Adrien and I like writing single line content."',
},
],
[
"should return a formatted string escaping the semicolon",
{
str: "Hello; my name is Adrien and I like writing single line content.",
expected:
'"Hello; my name is Adrien and I like writing single line content."',
},
],
[
"should return a formatted string escaping new line when there is new line chars",
{
str: `Hello,
my name is Adrien and
I like writing multiline content
in a template string`,
expected:
'"Hello,\nmy name is Adrien and\nI like writing multiline content\nin a template string"',
},
],
[
"should return a formatted string escaping new line when there is new line chars and escape the double quote when there is double quotes",
{
str: 'Hello,\nmy name is "Adrien" and\nI like writing multiline content\nin a string',
expected:
'"Hello,\nmy name is ""Adrien"" and\nI like writing multiline content\nin a string"',
},
],
]
describe("csvCellContentFormatter", function () {
it.each(cases)("%s", (title: string, { str, expected }: Case) => {
const formattedStr = csvCellContentFormatter(str)
expect(formattedStr).toBe(expected)
})
})
@@ -1,26 +0,0 @@
import { generateEntityId } from "../generate-entity-id"
import { Entity, PrimaryColumn } from "typeorm"
@Entity()
class GenerateIdSpecEntity {
@PrimaryColumn()
id: string
}
describe("generateAndApplyEntityId", () => {
it('should return the id if already set', () => {
const entity = new GenerateIdSpecEntity()
entity.id = "fakeId"
const generatedId = generateEntityId(entity.id, "prefix")
expect(generatedId).toBe(entity.id)
})
it('should return the new generated id if not set already', () => {
const entity = new GenerateIdSpecEntity()
entity.id = generateEntityId(entity.id, "prefix")
expect(entity.id).toBeTruthy()
expect(entity.id).toEqual(expect.stringMatching(/prefix_*/))
})
})
@@ -1,59 +0,0 @@
import { hasChanges } from "../has-changes"
describe("hasChanges", function () {
it("should return true the data differ and false otherwise", () => {
const objToCompareTo = {
prop1: "test",
prop2: "test",
prop3: "test",
prop4: {
prop4_1: "test",
prop4_2: "test",
prop4_3: "test",
},
}
const obj = {
prop1: "test",
prop2: "test",
prop3: "test",
prop4: {
prop4_1: "test",
prop4_2: "test",
prop4_3: "test",
},
}
let res = hasChanges(objToCompareTo, obj)
expect(res).toBeFalsy()
const obj2 = {
...obj,
prop3: "tes",
}
res = hasChanges(objToCompareTo, obj2)
expect(res).toBeTruthy()
const obj3 = {
...obj,
prop4: {
prop4_1: "",
prop4_2: "test",
prop4_3: "test",
},
}
res = hasChanges(objToCompareTo, obj3)
expect(res).toBeTruthy()
const obj4 = {
...obj,
}
/* @ts-ignore */
delete obj4.prop4
res = hasChanges(objToCompareTo, obj4)
expect(res).toBeFalsy()
})
})
-243
View File
@@ -1,243 +0,0 @@
import {
And,
FindManyOptions,
FindOperator,
FindOptionsRelations,
FindOptionsSelect,
FindOptionsWhere,
ILike,
In,
IsNull,
LessThan,
LessThanOrEqual,
MoreThan,
MoreThanOrEqual,
} from "typeorm"
import { ExtendedFindConfig, FindConfig } from "../types/common"
import { FindOptionsOrder } from "typeorm/find-options/FindOptionsOrder"
import { isObject } from "./is-object"
import { buildOrder, buildRelations, buildSelects } from "@medusajs/utils"
const operatorsMap = {
lt: (value) => LessThan(value),
gt: (value) => MoreThan(value),
lte: (value) => LessThanOrEqual(value),
gte: (value) => MoreThanOrEqual(value),
contains: (value) => ILike(`%${value}%`),
starts_with: (value) => ILike(`${value}%`),
ends_with: (value) => ILike(`%${value}`),
}
/**
* Used to build TypeORM queries.
* @param selector The selector
* @param config The config
* @return The QueryBuilderConfig
*/
export function buildQuery<TWhereKeys extends object, TEntity = unknown>(
selector: TWhereKeys,
config: FindConfig<TEntity> = {}
) {
const query: ExtendedFindConfig<TEntity> = {
where: buildWhere<TWhereKeys, TEntity>(selector),
}
if ("deleted_at" in selector) {
query.withDeleted = true
}
if ("skip" in config) {
;(query as FindManyOptions<TEntity>).skip = config.skip ?? undefined
}
if ("take" in config) {
;(query as FindManyOptions<TEntity>).take = config.take ?? undefined
}
if (config.relations) {
query.relations = buildRelations(
config.relations
) as FindOptionsRelations<TEntity>
}
if (config.select) {
query.select = buildSelects(
config.select as string[]
) as FindOptionsSelect<TEntity>
}
if (config.order) {
query.order = buildOrder(config.order) as FindOptionsOrder<TEntity>
}
return query
}
/**
* @param constraints
*
* @example
* const q = buildWhere(
* {
* id: "1234",
* test1: ["123", "12", "1"],
* test2: Not("this"),
* date: { gt: date },
* amount: { gt: 10 },
* },
*)
*
* // Output
* {
* id: "1234",
* test1: In(["123", "12", "1"]),
* test2: Not("this"),
* date: MoreThan(date),
* amount: MoreThan(10)
* }
*/
function buildWhere<TWhereKeys extends object, TEntity>(
constraints: TWhereKeys
): FindOptionsWhere<TEntity> | FindOptionsWhere<TEntity>[] {
let where: FindOptionsWhere<TEntity> | FindOptionsWhere<TEntity>[] = {}
if (Array.isArray(constraints)) {
where = []
constraints.forEach((constraint) => {
;(where as FindOptionsWhere<TEntity>[]).push(
buildWhere(constraint) as FindOptionsWhere<TEntity>
)
})
return where
}
for (const [key, value] of Object.entries(constraints)) {
if (value === undefined) {
continue
}
if (value === null) {
where[key] = IsNull()
continue
}
if (value instanceof FindOperator) {
where[key] = value
continue
}
if (Array.isArray(value)) {
where[key] = In(value)
continue
}
if (typeof value === "object") {
Object.entries(value).forEach(([objectKey, objectValue]) => {
where[key] = where[key] || []
if (operatorsMap[objectKey]) {
where[key].push(operatorsMap[objectKey](objectValue))
} else {
if (objectValue != undefined && typeof objectValue === "object") {
where[key] = buildWhere<any, TEntity>(objectValue)
return
}
where[key] = value
}
return
})
if (!Array.isArray(where[key])) {
continue
}
if (where[key].length === 1) {
where[key] = where[key][0]
} else {
where[key] = And(...where[key])
}
continue
}
where[key] = value
}
return where
}
/**
* Revert new object structure of find options to the legacy structure of previous version
* @deprecated in favor of import { objectToStringPath } from "@medusajs/utils"
* @example
* input: {
* test: {
* test1: true,
* test2: true,
* test3: {
* test4: true
* },
* },
* test2: true
* }
* output: ['test.test1', 'test.test2', 'test.test3.test4', 'test2']
* @param input
*/
export function buildLegacyFieldsListFrom<TEntity>(
input:
| FindOptionsWhere<TEntity>
| FindOptionsSelect<TEntity>
| FindOptionsOrder<TEntity>
| FindOptionsRelations<TEntity> = {}
): (keyof TEntity)[] {
if (!Object.keys(input).length) {
return []
}
const output: Set<string> = new Set(Object.keys(input))
for (const key of Object.keys(input)) {
if (input[key] != undefined && typeof input[key] === "object") {
const deepRes = buildLegacyFieldsListFrom(input[key])
const items = deepRes.reduce((acc, val) => {
acc.push(`${key}.${val}`)
return acc
}, [] as string[])
items.forEach((item) => output.add(item))
continue
}
output.add(key)
}
return Array.from(output) as (keyof TEntity)[]
}
export function addOrderToSelect<TEntity>(
order: FindOptionsOrder<TEntity>,
select: FindOptionsSelect<TEntity>
): void {
for (const orderBy of Object.keys(order)) {
if (isObject(order[orderBy])) {
select[orderBy] =
select[orderBy] && isObject(select[orderBy]) ? select[orderBy] : {}
addOrderToSelect(order[orderBy], select[orderBy])
continue
}
select[orderBy] = isObject(select[orderBy])
? { ...select[orderBy], id: true, [orderBy]: true }
: true
}
}
export function nullableValue(value: any): FindOperator<any> {
if (value === null) {
return IsNull()
} else {
return value
}
}
@@ -1,21 +0,0 @@
/**
* Return the tax amount that
*
* - is includes in the price if it is tax inclusive
* - will be applied on to the price if it is tax exclusive
*/
export function calculatePriceTaxAmount({
price,
includesTax,
taxRate,
}: {
price: number
includesTax?: boolean
taxRate: number
}): number {
if (includesTax) {
return (taxRate * price) / (1 + taxRate)
}
return price * taxRate
}
-2
View File
@@ -1,2 +0,0 @@
import { defaultCountries, Country } from "@medusajs/utils"
export const countries: Country[] = defaultCountries
@@ -1,24 +0,0 @@
export function csvCellContentFormatter(str: string): string {
const newLineRegexp = new RegExp(/\n/g)
const doubleQuoteRegexp = new RegExp(/"/g)
const comaRegexp = new RegExp(/,/g)
const semicolonRegexp = new RegExp(/;/g)
const hasNewLineChar = !!str.match(newLineRegexp)
const hasComaChar = !!str.match(comaRegexp)
const hasSemicolonChar = !!str.match(semicolonRegexp)
if (!hasNewLineChar && !hasComaChar && !hasSemicolonChar) {
return str
}
const formatterStr = str.replace(doubleQuoteRegexp, `""`)
return `"${formatterStr}"`
}
export function csvRevertCellContentFormatter(str: string): string {
if (str.startsWith(`"`)) {
str = str.substring(1, str.length - 1)
}
return str
}
-2
View File
@@ -1,2 +0,0 @@
import { defaultCurrencies, Currency } from "@medusajs/utils"
export const currencies: Record<string, Currency> = defaultCurrencies
@@ -1,26 +0,0 @@
import { Column, ColumnOptions, ColumnType } from "typeorm"
export function resolveDbType(pgSqlType: ColumnType): ColumnType {
return pgSqlType
}
export function resolveDbGenerationStrategy(
pgSqlType: "increment" | "uuid" | "rowid"
): "increment" | "uuid" | "rowid" {
return pgSqlType
}
export function DbAwareColumn(columnOptions: ColumnOptions): PropertyDecorator {
const pre = columnOptions.type
if (columnOptions.type) {
columnOptions.type = resolveDbType(columnOptions.type)
}
if (pre === "jsonb" && pre !== columnOptions.type) {
if ("default" in columnOptions) {
columnOptions.default = JSON.stringify(columnOptions.default)
}
}
return Column(columnOptions)
}
@@ -1,87 +0,0 @@
import { Column, ColumnOptions, Entity, EntityOptions } from "typeorm"
import { featureFlagRouter } from "../loaders/feature-flags"
import { Equals, ValidateIf } from "class-validator"
import { isDefined } from "@medusajs/utils"
/**
* If that file is required in a non node environment then the setImmediate timer does not exists.
* This can happen when a client package require a server based package and that one of the import
* require to import that file which is using the setImmediate.
* In order to take care of those cases, the setImmediate timer will use the one provided by the api (node)
* if possible and will provide a mock in a browser like environment.
*/
let setImmediate_
try {
setImmediate_ = setImmediate
} catch (e) {
console.warn(
"[feature-flag-decorator.ts] setImmediate will use a mock, this happen when this file is required in a browser environment and should not impact you"
)
setImmediate_ = async (callback: () => void | Promise<void>) => callback()
}
export function FeatureFlagColumn(
featureFlag: string,
columnOptions: ColumnOptions = {}
): PropertyDecorator {
return function (target, propertyName) {
setImmediate_((): any => {
if (!featureFlagRouter.isFeatureEnabled(featureFlag)) {
return
}
Column(columnOptions)(target, propertyName)
})
}
}
export function FeatureFlagDecorators(
featureFlag: string | string[],
decorators: PropertyDecorator[]
): PropertyDecorator {
return function (target, propertyName) {
setImmediate_((): any => {
if (!featureFlagRouter.isFeatureEnabled(featureFlag)) {
ValidateIf((o) => isDefined(o[propertyName]))(target, propertyName)
Equals(undefined, {
message: `${propertyName as string} should not exist`,
})(target, propertyName)
return
}
decorators.forEach((decorator: PropertyDecorator) => {
decorator(target, propertyName)
})
})
}
}
export function FeatureFlagClassDecorators(
featureFlag: string | string[],
decorators: ClassDecorator[]
): ClassDecorator {
return function (target) {
setImmediate_((): any => {
if (!featureFlagRouter.isFeatureEnabled(featureFlag)) {
return
}
decorators.forEach((decorator: ClassDecorator) => {
decorator(target)
})
})
}
}
export function FeatureFlagEntity(
featureFlag: string | string[],
name?: string,
options?: EntityOptions
): ClassDecorator {
return function (target: Function): void {
target["isFeatureEnabled"] = function (): boolean {
return featureFlagRouter.isFeatureEnabled(featureFlag)
}
Entity(name, options)(target)
}
}
@@ -1,16 +0,0 @@
import { ulid } from "ulid"
/**
* Generate a composed id based on the input parameters and return either the is if it exists or the generated one.
* @param idProperty
* @param prefix
*/
export function generateEntityId(idProperty?: string, prefix?: string): string {
if (idProperty) {
return idProperty
}
const id = ulid()
prefix = prefix ? `${prefix}_` : ""
return `${prefix}${id}`
}
+9 -11
View File
@@ -6,11 +6,9 @@ import {
import { pick } from "lodash"
import { MedusaError, isDefined } from "medusa-core-utils"
import { RequestQueryFields } from "@medusajs/types"
import { BaseEntity } from "../interfaces"
import { featureFlagRouter } from "../loaders/feature-flags"
import { FindConfig, QueryConfig } from "../types/common"
export function pickByConfig<TModel extends BaseEntity>(
export function pickByConfig<TModel>(
obj: TModel | TModel[],
config: FindConfig<TModel>
): Partial<TModel> | Partial<TModel>[] {
@@ -26,10 +24,10 @@ export function pickByConfig<TModel extends BaseEntity>(
return obj
}
export function prepareListQuery<
T extends RequestQueryFields,
TEntity extends BaseEntity
>(validated: T, queryConfig: QueryConfig<TEntity> = {}) {
export function prepareListQuery<T extends RequestQueryFields, TEntity>(
validated: T,
queryConfig: QueryConfig<TEntity> = {}
) {
// TODO: this function will be simplified a lot once we drop support for the old api
const { order, fields, limit = 50, expand, offset = 0 } = validated
let {
@@ -215,10 +213,10 @@ export function prepareListQuery<
}
}
export function prepareRetrieveQuery<
T extends RequestQueryFields,
TEntity extends BaseEntity
>(validated: T, queryConfig?: QueryConfig<TEntity>) {
export function prepareRetrieveQuery<T extends RequestQueryFields, TEntity>(
validated: T,
queryConfig?: QueryConfig<TEntity>
) {
const { listConfig, remoteQueryConfig } = prepareListQuery(
validated,
queryConfig
-26
View File
@@ -1,26 +0,0 @@
import { isObject } from "./is-object"
/**
* Compare two objects and return true if there is changes detected from obj2 compared to obj1
* @param obj1
* @param obj2
*/
export function hasChanges<T1 extends Object, T2 extends Object>(
obj1: T1,
obj2: T2
): boolean {
for (const [key, value] of Object.entries(obj2)) {
if (isObject(obj1[key])) {
if (hasChanges(obj1[key], value)) {
return true
}
continue
}
if (obj1[key] !== value) {
return true
}
}
return false
}
@@ -1,2 +0,0 @@
export * from "./run-idempotency-step"
export * from "./initialize-idempotency-request"
@@ -1,26 +0,0 @@
import { Request, Response } from "express"
import { IdempotencyKey } from "../../models"
import IdempotencyKeyService from "../../services/idempotency-key"
import { EntityManager } from "typeorm"
export async function initializeIdempotencyRequest(
req: Request,
res: Response
): Promise<IdempotencyKey> {
const idempotencyKeyService: IdempotencyKeyService = req.scope.resolve(
"idempotencyKeyService"
)
const manager: EntityManager = req.scope.resolve("manager")
const headerKey = req.get("Idempotency-Key") || ""
let idempotencyKey
idempotencyKey = await idempotencyKeyService
.withTransaction(manager)
.initializeRequest(headerKey, req.method, req.params, req.path)
res.setHeader("Access-Control-Expose-Headers", "Idempotency-Key")
res.setHeader("Idempotency-Key", idempotencyKey.idempotency_key)
return idempotencyKey
}
@@ -1,44 +0,0 @@
import { IdempotencyCallbackResult } from "../../types/idempotency-key"
import { EntityManager } from "typeorm"
import { IdempotencyKey } from "../../models"
import { AwilixContainer } from "awilix"
import IdempotencyKeyService from "../../services/idempotency-key"
import { IsolationLevel } from "typeorm/driver/types/IsolationLevel"
export type RunIdempotencyStepOptions = {
manager: EntityManager
idempotencyKey: IdempotencyKey
container: AwilixContainer
isolationLevel: IsolationLevel
}
export async function runIdempotencyStep(
handler: ({
manager,
}: {
manager: EntityManager
}) => Promise<IdempotencyCallbackResult>,
{
manager,
idempotencyKey,
container,
isolationLevel,
}: RunIdempotencyStepOptions
) {
const idempotencyKeyService: IdempotencyKeyService = container.resolve(
"idempotencyKeyService"
)
return await manager.transaction(
isolationLevel,
async (transactionManager) => {
const idempotencyKey_ = await idempotencyKeyService
.withTransaction(transactionManager)
.workStage(idempotencyKey.idempotency_key, async (stageManager) => {
return await handler({ manager: stageManager })
})
idempotencyKey.response_code = idempotencyKey_.response_code
idempotencyKey.response_body = idempotencyKey_.response_body
idempotencyKey.recovery_point = idempotencyKey_.recovery_point
}
)
}
-9
View File
@@ -1,11 +1,5 @@
export * from "./build-query"
export * from "./calculate-price-tax-amount"
export * from "./clean-response-data"
export * from "./csv-cell-content-formatter"
export * from "./db-aware-column"
export * from "./exception-formatter"
export * from "./generate-entity-id"
export * from "./has-changes"
export * from "./is-date"
export * from "./is-object"
export * from "./is-string"
@@ -13,7 +7,4 @@ export * from "./omit-deep"
export * from "./remote-query-fetch-data"
export * from "./remove-undefined-properties"
export * from "./set-metadata"
export * from "./validate-id"
export { registerOverriddenValidators, validator } from "./validator"
export * from "./validators/is-type"
export * from "./middlewares"
-20
View File
@@ -1,20 +0,0 @@
import { isEmail } from "class-validator"
import { MedusaError } from "medusa-core-utils"
/**
* Used to validate user email.
* @param {string} email - email to validate
* @return {string} the validated email
*/
export function validateEmail(email: string): string {
const validatedEmail = isEmail(email)
if (!validatedEmail) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"The email is not valid"
)
}
return email.toLowerCase()
}
@@ -1,5 +0,0 @@
export async function manualAutoIncrement(
tableName: string
): Promise<number | null> {
return null
}
@@ -1,716 +0,0 @@
import { NextFunction, Request, Response } from "express"
import { transformQuery } from "../transform-query"
import { extendedFindParamsMixin } from "../../../types/common"
import { MedusaError } from "medusa-core-utils"
describe("transformQuery", () => {
afterEach(() => {
jest.clearAllMocks()
})
it("should transform the input query", async () => {
let mockRequest = {
query: {},
} as Request
const mockResponse = {} as Response
const nextFunction: NextFunction = jest.fn()
const expectations = ({
offset,
limit,
inputOrder,
transformedOrder,
}: {
offset: number
limit: number
inputOrder: string | undefined
transformedOrder?: Record<string, "ASC" | "DESC">
relations?: string[]
}) => {
expect(mockRequest.validatedQuery).toEqual({
offset,
limit,
order: inputOrder,
})
expect(mockRequest.filterableFields).toEqual({})
expect(mockRequest.allowedProperties).toEqual([
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
"metadata",
"metadata.parent",
"metadata.children",
"metadata.product",
])
expect(mockRequest.listConfig).toEqual({
take: limit,
skip: offset,
select: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
relations: [
"metadata",
"metadata.parent",
"metadata.children",
"metadata.product",
],
order: transformedOrder,
})
expect(mockRequest.remoteQueryConfig).toEqual({
fields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
pagination: {
order: transformedOrder,
skip: offset,
take: limit,
},
})
}
let queryConfig: any = {
defaultFields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
defaultRelations: [
"metadata",
"metadata.parent",
"metadata.children",
"metadata.product",
],
isList: true,
}
let middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expectations({
limit: 20,
offset: 0,
inputOrder: undefined,
})
//////////////////////////////
mockRequest = {
query: {
limit: "10",
offset: "5",
order: "created_at",
},
} as unknown as Request
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expectations({
limit: 10,
offset: 5,
inputOrder: "created_at",
transformedOrder: { created_at: "ASC" },
})
//////////////////////////////
mockRequest = {
query: {
limit: "10",
offset: "5",
order: "created_at",
},
} as unknown as Request
queryConfig = {
defaults: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
isList: true,
}
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expectations({
limit: 10,
offset: 5,
inputOrder: "created_at",
transformedOrder: { created_at: "ASC" },
})
})
it("should transform the input query taking into account the fields symbols (+,- or no symbol)", async () => {
let mockRequest = {
query: {
fields: "id",
},
} as unknown as Request
const mockResponse = {} as Response
const nextFunction: NextFunction = jest.fn()
let queryConfig: any = {
defaultFields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
defaultRelations: [
"metadata",
"metadata.parent",
"metadata.children",
"metadata.product",
],
isList: true,
}
let middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(mockRequest.listConfig).toEqual(
expect.objectContaining({
select: ["id", "created_at"],
})
)
//////////////////////////////
mockRequest = {
query: {
fields: "+test_prop,-prop-test-something",
},
} as unknown as Request
queryConfig = {
defaultFields: [
"id",
"prop-test-something",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
defaultRelations: [
"metadata",
"metadata.parent",
"metadata.children",
"metadata.product",
],
isList: true,
}
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(mockRequest.listConfig).toEqual(
expect.objectContaining({
select: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
"test_prop",
],
})
)
//////////////////////////////
mockRequest = {
query: {
fields: "+test_prop,-updated_at",
},
} as unknown as Request
queryConfig = {
defaults: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
isList: true,
}
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(mockRequest.listConfig).toEqual(
expect.objectContaining({
select: [
"id",
"created_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
"test_prop",
],
})
)
})
it(`should transform the input and manage the allowed fields and relations properly without error`, async () => {
let mockRequest = {
query: {
fields: "*product.variants,+product.id",
},
} as unknown as Request
const mockResponse = {} as Response
const nextFunction: NextFunction = jest.fn()
let queryConfig: any = {
defaults: [
"id",
"created_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
allowed: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
"product",
"product.variants",
],
isList: true,
}
let middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(mockRequest.listConfig).toEqual(
expect.objectContaining({
select: [
"id",
"created_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
"product.id",
],
relations: [
"metadata",
"metadata.parent",
"metadata.children",
"metadata.product",
"product",
"product.variants",
],
})
)
expect(mockRequest.remoteQueryConfig).toEqual(
expect.objectContaining({
fields: [
"id",
"created_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
"product.id",
"product.variants.*",
],
})
)
//////////////////////////////
mockRequest = {
query: {
fields: "store.name",
},
} as unknown as Request
queryConfig = {
defaultFields: [
"id",
"created_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
allowedFields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
"product",
"product.variants",
"store.name",
],
allowedRelations: ["metadata", "product"],
isList: true,
}
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(mockRequest.listConfig).toEqual(
expect.objectContaining({
select: ["store.name", "created_at", "id"],
relations: ["store"],
})
)
expect(mockRequest.remoteQueryConfig).toEqual(
expect.objectContaining({
fields: ["store.name", "created_at", "id"],
})
)
})
it("should throw when attempting to transform the input if disallowed fields are requested", async () => {
let mockRequest = {
query: {
fields: "+test_prop",
},
} as unknown as Request
const mockResponse = {} as Response
const nextFunction: NextFunction = jest.fn()
let queryConfig: any = {
defaultFields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
defaultRelations: [
"metadata",
"metadata.parent",
"metadata.children",
"metadata.product",
],
allowedFields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
isList: true,
}
let middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(nextFunction).toHaveBeenCalledWith(
new MedusaError(
MedusaError.Types.INVALID_DATA,
`Requested fields [test_prop] are not valid`
)
)
//////////////////////////////
mockRequest = {
query: {
expand: "product",
},
} as unknown as Request
queryConfig = {
defaultFields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
defaultRelations: [
"metadata",
"metadata.parent",
"metadata.children",
"metadata.product",
],
allowedFields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
allowedRelations: [
"metadata",
"metadata.parent",
"metadata.children",
"metadata.product",
],
isList: true,
}
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(nextFunction).toHaveBeenCalledWith(
new MedusaError(
MedusaError.Types.INVALID_DATA,
`Requested fields [product] are not valid`
)
)
//////////////////////////////
mockRequest = {
query: {
expand: "store",
},
} as unknown as Request
queryConfig = {
defaultFields: [
"id",
"created_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
allowedFields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
"product",
"product.variants",
"store.name",
],
allowedRelations: ["metadata", "product"],
isList: true,
}
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(nextFunction).toHaveBeenCalledWith(
new MedusaError(
MedusaError.Types.INVALID_DATA,
`Requested fields [store] are not valid`
)
)
//////////////////////////////
mockRequest = {
query: {
fields: "*product",
},
} as unknown as Request
queryConfig = {
defaults: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
allowed: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
isList: true,
}
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(nextFunction).toHaveBeenCalledWith(
new MedusaError(
MedusaError.Types.INVALID_DATA,
`Requested fields [product] are not valid`
)
)
//////////////////////////////
mockRequest = {
query: {
fields: "*product.variants",
},
} as unknown as Request
queryConfig = {
defaults: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
allowed: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
"product",
],
isList: true,
}
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(nextFunction).toHaveBeenCalledWith(
new MedusaError(
MedusaError.Types.INVALID_DATA,
`Requested fields [product.variants] are not valid`
)
)
//////////////////////////////
mockRequest = {
query: {
fields: "product",
},
} as unknown as Request
queryConfig = {
defaults: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
allowed: [
"id",
"created_at",
"updated_at",
"deleted_at",
"metadata.id",
"metadata.parent.id",
"metadata.children.id",
"metadata.product.id",
],
isList: true,
}
middleware = transformQuery(extendedFindParamsMixin(), queryConfig)
await middleware(mockRequest, mockResponse, nextFunction)
expect(nextFunction).toHaveBeenCalledWith(
new MedusaError(
MedusaError.Types.INVALID_DATA,
`Requested fields [product] are not valid`
)
)
})
})
@@ -4,8 +4,4 @@ export { default as authenticateCustomer } from "./authenticate-customer"
export { default as wrapHandler } from "./await-middleware"
export { default as errorHandler } from "./error-handler"
export { isFeatureFlagEnabled } from "./feature-flag-enabled"
export { default as normalizeQuery } from "./normalized-query"
export { default as requireCustomerAuthentication } from "./require-customer-authentication"
export { transformBody } from "./transform-body"
export { transformIncludesOptions } from "./transform-includes-options"
export { transformQuery, transformStoreQuery } from "./transform-query"
@@ -1,24 +0,0 @@
import { NextFunction, Request, Response } from "express"
/**
* Normalize an input query, especially from array like query params to an array type
* e.g: /admin/orders/?fields[]=id,status,cart_id becomes { fields: ["id", "status", "cart_id"] }
*/
export default (): ((
req: Request,
res: Response,
next: NextFunction
) => void) => {
return (req: Request, res: Response, next: NextFunction): void => {
req.query = Object.entries(req.query).reduce((acc, [key, val]) => {
if (Array.isArray(val) && val.length === 1) {
acc[key] = (val as string[])[0].split(",")
} else {
acc[key] = val
}
return acc
}, {})
next()
}
}
@@ -1,20 +0,0 @@
import { ValidatorOptions } from "class-validator"
import { NextFunction, Request, Response } from "express"
import { ClassConstructor } from "../../types/global"
import { validator } from "../../utils/validator"
export function transformBody<T>(
plainToClass: ClassConstructor<T>,
config: ValidatorOptions = {
forbidUnknownValues: false,
}
): (req: Request, res: Response, next: NextFunction) => Promise<void> {
return async (req: Request, res: Response, next: NextFunction) => {
try {
req.validatedBody = await validator(plainToClass, req.body, config)
next()
} catch (e) {
next(e)
}
}
}
@@ -1,57 +0,0 @@
import { NextFunction, Request, Response } from "express"
import { MedusaError } from "medusa-core-utils"
/**
* Retrieve the includes options from the fields query param.
* If the include option is present then assigned it to includes on req
* @param allowedIncludes The list of fields that can be passed and assign to req.includes
* @param expectedIncludes The list of fields that the consumer can pass to the end point using this middleware. It is a subset of `allowedIncludes`
*/
export function transformIncludesOptions(
allowedIncludes: string[] = [],
expectedIncludes: string[] = []
) {
return (req: Request, res: Response, next: NextFunction): void => {
if (!allowedIncludes.length || !req.query.expand) {
return next()
}
const expand = (req.query.expand as string).split(",") ?? []
for (const includes of allowedIncludes) {
const fieldIndex = expand.indexOf(includes) ?? -1
const isPresent = fieldIndex !== -1
if (isPresent) {
expand.splice(fieldIndex, 1)
if (!expectedIncludes.includes(includes)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`The field "${includes}" is not supported by this end point. ${
expectedIncludes.length
? `The includes fields can be one of entity properties or in [${expectedIncludes.join(
", "
)}]`
: ""
}`
)
}
req.includes = req.includes ?? {}
req.includes[includes] = true
}
}
if (req.query.expand) {
if (expand.length) {
req.query.expand = expand.join(",")
} else {
delete req.query.expand
}
}
next()
}
}
@@ -1,140 +0,0 @@
import { ValidatorOptions } from "class-validator"
import { NextFunction, Request, Response } from "express"
import { omit } from "lodash"
import { RequestQueryFields } from "@medusajs/types"
import { BaseEntity } from "../../interfaces"
import { FindConfig, QueryConfig } from "../../types/common"
import { ClassConstructor } from "../../types/global"
import { removeUndefinedProperties } from "../../utils"
import {
prepareListQuery,
prepareRetrieveQuery,
} from "../../utils/get-query-config"
import { validator } from "../../utils/validator"
import { default as normalizeQuery } from "./normalized-query"
/**
* Middleware that transform the query input for the admin end points
* @param plainToClass
* @param queryConfig
* @param config
*/
export function transformQuery<
T extends RequestQueryFields,
TEntity extends BaseEntity
>(
plainToClass: ClassConstructor<T>,
queryConfig: QueryConfig<TEntity> = {},
config: ValidatorOptions = {}
): (req: Request, res: Response, next: NextFunction) => Promise<void> {
return async (req: Request, res: Response, next: NextFunction) => {
try {
normalizeQuery()(req, res, () => void 0)
const validated: T = await validator<T, Record<string, unknown>>(
plainToClass,
req.query,
config
)
req.validatedQuery = validated
req.filterableFields = getFilterableFields(validated)
attachListOrRetrieveConfig<TEntity>(req, {
...queryConfig,
allowed:
req.allowed ?? queryConfig.allowed ?? queryConfig.allowedFields ?? [],
})
/**
* TODO: the bellow allowedProperties should probably need to be reworked which would create breaking changes everywhere
* cleanResponseData is used. It is in fact, what is expected to be returned which IMO
* should correspond to the select/relations
*
* Kept it as it is to maintain backward compatibility
*/
const queryConfigRes = !queryConfig.isList
? req.retrieveConfig
: req.listConfig
const includesRelations = Object.keys(req.includes ?? {})
req.allowedProperties = Array.from(
new Set(
[
...(req.validatedQuery.fields
? queryConfigRes.select ?? []
: req.allowed ??
queryConfig.allowed ??
queryConfig.allowedFields ??
(queryConfig.defaults as string[]) ??
queryConfig.defaultFields ??
[]),
...(req.validatedQuery.expand || includesRelations.length
? [...(validated.expand?.split(",") || []), ...includesRelations] // For backward compatibility, the includes takes precedence over the relations for the returnable fields
: queryConfig.allowedRelations ?? queryConfigRes.relations ?? []), // For backward compatibility, the allowedRelations takes precedence over the relations for the returnable fields
].filter(Boolean)
)
)
next()
} catch (e) {
next(e)
}
}
}
/**
* Middleware that transform the query input for the store endpoints
* @param plainToClass
* @param queryConfig
* @param config
*
* @deprecated use `transformQuery` instead
*/
export function transformStoreQuery<
T extends RequestQueryFields,
TEntity extends BaseEntity
>(
plainToClass: ClassConstructor<T>,
queryConfig?: QueryConfig<TEntity>,
config: ValidatorOptions = {}
): (req: Request, res: Response, next: NextFunction) => Promise<void> {
return transformQuery(plainToClass, queryConfig, config)
}
/**
* Omit the non filterable config from the validated object
* @param obj
*/
function getFilterableFields<T extends RequestQueryFields>(obj: T): T {
const result = omit(obj, [
"limit",
"offset",
/**
* @deprecated
*/
"expand",
"fields",
"order",
]) as T
return removeUndefinedProperties(result)
}
/**
* build and attach the `retrieveConfig` or `listConfig` and remoteQueryConfig to the request object
* @param req
* @param queryConfig
*/
function attachListOrRetrieveConfig<TEntity extends BaseEntity>(
req: Request,
queryConfig: QueryConfig<TEntity> = {}
) {
const validated = req.validatedQuery
const config = queryConfig.isList
? prepareListQuery(validated, queryConfig)
: prepareRetrieveQuery(validated, queryConfig)
req.listConfig = ("listConfig" in config &&
config.listConfig) as FindConfig<any>
req.retrieveConfig = ("retrieveConfig" in config &&
config.retrieveConfig) as FindConfig<any>
req.remoteQueryConfig = config.remoteQueryConfig
}
@@ -1,15 +0,0 @@
/**
*
* @param relations relations from which a relation should be removed
* @param relation relation to be removed
* @returns tuple containing the new relations and a boolean indicating whether the relation was found in the relations array
*/
export const omitRelationIfExists = (
relations: string[],
relation: string
): [string[], boolean] => {
const filteredRelations = relations.filter((rel) => rel !== relation)
const includesRelation = relations.length !== filteredRelations.length
return [relations, includesRelation]
}
@@ -60,8 +60,6 @@ export function remoteQueryFetchData(container: MedusaContainer) {
}
const expandRelations = Object.keys(expand.expands ?? {})
// filter out links from relations because TypeORM will throw if the relation doesn't exist
options.relations = options.relations.filter(
(relation) => !expandRelations.some((ex) => relation.startsWith(ex))
)
-381
View File
@@ -1,381 +0,0 @@
import { promiseAll } from "@medusajs/utils"
import { flatten, groupBy, map, merge } from "lodash"
import {
EntityMetadata,
ObjectLiteral,
Repository,
SelectQueryBuilder,
} from "typeorm"
import { ExtendedFindConfig } from "../types/common"
// Regex matches all '.' except the rightmost
export const positiveLookaheadDotReplacer = new RegExp(/\.(?=[^.]*\.)/, "g")
// Replace all '.' with '__' to avoid typeorm's automatic aliasing
export const dotReplacer = new RegExp(/\./, "g")
/**
* Custom query entity, it is part of the creation of a custom findWithRelationsAndCount needs.
* Allow to query the relations for the specified entity ids
*
* @param repository
* @param entityIds
* @param groupedRelations
* @param withDeleted
* @param select
* @param customJoinBuilders
*/
export async function queryEntityWithIds<T extends ObjectLiteral>({
repository,
entityIds,
groupedRelations,
withDeleted = false,
select = [],
customJoinBuilders = [],
}: {
repository: Repository<T>
entityIds: string[]
groupedRelations: { [toplevel: string]: string[] }
withDeleted?: boolean
select?: (keyof T)[]
customJoinBuilders?: ((
qb: SelectQueryBuilder<T>,
alias: string,
toplevel: string
) => false | undefined)[]
}): Promise<T[]> {
const alias = repository.metadata.name.toLowerCase()
return await promiseAll(
Object.entries(groupedRelations).map(
async ([toplevel, topLevelRelations]) => {
let querybuilder = repository.createQueryBuilder(alias)
if (select?.length) {
querybuilder.select(
(select as string[])
.filter(function (s) {
return s.startsWith(toplevel) || !s.includes(".")
})
.map((column) => {
// In case the column is the toplevel relation, we need to replace the dot with a double underscore if it also contains top level relations
if (column.includes(toplevel)) {
return topLevelRelations.some((rel) => column.includes(rel))
? column.replace(positiveLookaheadDotReplacer, "__")
: column
}
return `${alias}.${column}`
})
)
}
let shouldAttachDefault: boolean | undefined = true
for (const customJoinBuilder of customJoinBuilders) {
const result = customJoinBuilder(querybuilder, alias, toplevel)
if (result === undefined) {
continue
}
shouldAttachDefault = shouldAttachDefault && result
}
if (shouldAttachDefault) {
const regexp = new RegExp(`^${toplevel}\\.\\w+$`)
const joinMethod = (select as string[]).filter(
(key) => !!key.match(regexp)
).length
? "leftJoin"
: "leftJoinAndSelect"
querybuilder = querybuilder[joinMethod](
`${alias}.${toplevel}`,
toplevel
)
}
for (const rel of topLevelRelations) {
const [_, rest] = rel.split(".")
if (!rest) {
continue
}
const regexp = new RegExp(`^${rel}\\.\\w+$`)
const joinMethod = (select as string[]).filter(
(key) => !!key.match(regexp)
).length
? "leftJoin"
: "leftJoinAndSelect"
querybuilder = querybuilder[joinMethod](
rel.replace(positiveLookaheadDotReplacer, "__"),
rel.replace(dotReplacer, "__")
)
}
querybuilder = querybuilder.where(`${alias}.id IN (:...entitiesIds)`, {
entitiesIds: entityIds,
})
if (withDeleted) {
querybuilder.withDeleted()
}
return querybuilder.getMany()
}
)
).then(flatten)
}
/**
* Custom query entity without relations, it is part of the creation of a custom findWithRelationsAndCount needs.
* Allow to query the entities without taking into account the relations. The relations will be queried separately
* using the queryEntityWithIds util
*
* @param repository
* @param optionsWithoutRelations
* @param shouldCount
* @param customJoinBuilders
*/
export async function queryEntityWithoutRelations<T extends ObjectLiteral>({
repository,
optionsWithoutRelations,
shouldCount = false,
customJoinBuilders = [],
}: {
repository: Repository<T>
optionsWithoutRelations: Omit<ExtendedFindConfig<T>, "relations">
shouldCount: boolean
customJoinBuilders: ((
qb: SelectQueryBuilder<T>,
alias: string
) => Promise<{ relation: string; preventOrderJoin: boolean } | void>)[]
}): Promise<[T[], number]> {
const alias = repository.metadata.name.toLowerCase()
const qb = repository.createQueryBuilder(alias).select([`${alias}.id`])
if (optionsWithoutRelations.where) {
qb.where(optionsWithoutRelations.where)
}
const shouldJoins: { relation: string; shouldJoin: boolean }[] = []
for (const customJoinBuilder of customJoinBuilders) {
const result = await customJoinBuilder(qb, alias)
if (result) {
shouldJoins.push({
relation: result.relation,
shouldJoin: !result.preventOrderJoin,
})
}
}
applyOrdering({
repository,
order: (optionsWithoutRelations.order as any) ?? {},
qb,
alias,
shouldJoin: (relationToJoin) => {
return shouldJoins.every(
({ relation, shouldJoin }) =>
relation !== relationToJoin ||
(relation === relationToJoin && shouldJoin)
)
},
})
if (optionsWithoutRelations.withDeleted) {
qb.withDeleted()
}
/*
* Deduplicate tuples for join + ordering (e.g. variants.prices.amount) since typeorm doesnt
* know how to manage it by itself
*/
const expressionMapAllOrderBys = qb.expressionMap.allOrderBys
if (
expressionMapAllOrderBys &&
Object.keys(expressionMapAllOrderBys).length
) {
const orderBysString = Object.keys(expressionMapAllOrderBys)
.map((column) => {
return `${column} ${expressionMapAllOrderBys[column]}`
})
.join(", ")
qb.addSelect(
`row_number() OVER (PARTITION BY ${alias}.id ORDER BY ${orderBysString}) AS rownum`
)
} else {
qb.addSelect(`1 AS rownum`)
}
/*
* In typeorm SelectQueryBuilder, the orderBy is removed from the original query when there is pagination
* and join involved together.
*
* This workaround allows us to include the order as part of the original query (including joins) before
* selecting the distinct ids of the main alias entity. The distinct ids deduplication
* is managed by the rownum column added to the select below.
*
* see: node_modules/typeorm/query-builder/SelectQueryBuilder.js(1973)
*/
const outerQb = new SelectQueryBuilder(qb.connection, (qb as any).queryRunner)
.select(`${qb.escape(`${alias}_id`)}`)
.from(`(${qb.getQuery()})`, alias)
.where(`${alias}.rownum = 1`)
.setParameters(qb.getParameters())
.setNativeParameters(qb.expressionMap.nativeParameters)
.offset(optionsWithoutRelations.skip)
.limit(optionsWithoutRelations.take)
const mapToEntities = (array: any) => {
return array.map((rawProduct) => ({
id: rawProduct[`${alias}_id`],
})) as unknown as T[]
}
let entities: T[]
let count = 0
if (shouldCount) {
const outerQbCount = new SelectQueryBuilder(
qb.connection,
(qb as any).queryRunner
)
.select(`COUNT(1)`, `count`)
.from(`(${qb.getQuery()})`, alias)
.where(`${alias}.rownum = 1`)
.setParameters(qb.getParameters())
.setNativeParameters(qb.expressionMap.nativeParameters)
.orderBy()
.groupBy()
.offset(undefined)
.limit(undefined)
.skip(undefined)
.take(undefined)
const result = await promiseAll([
outerQb.getRawMany(),
outerQbCount.getRawOne(),
])
entities = mapToEntities(result[0])
count = Number(result[1].count)
} else {
const result = await outerQb.getRawMany()
entities = mapToEntities(result)
}
return [entities, count]
}
/**
* Grouped the relation to the top level entity
* @param relations
*/
export function getGroupedRelations(relations: string[]): {
[toplevel: string]: string[]
} {
const groupedRelations: { [toplevel: string]: string[] } = {}
for (const rel of relations) {
const [topLevel] = rel.split(".")
if (groupedRelations[topLevel]) {
groupedRelations[topLevel].push(rel)
} else {
groupedRelations[topLevel] = [rel]
}
}
return groupedRelations
}
/**
* Merged the entities and relations that composed by the result of queryEntityWithIds and queryEntityWithoutRelations
* call
* @param entitiesAndRelations
*/
export function mergeEntitiesWithRelations<T>(
entitiesAndRelations: Array<Partial<T>>
): T[] {
const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id")
return map(entitiesAndRelationsById, (entityAndRelations) =>
merge({}, ...entityAndRelations)
)
}
/**
* Apply the appropriate order depending on the requirements
* @param repository
* @param order The field on which to apply the order (e.g { "variants.prices.amount": "DESC" })
* @param qb
* @param alias
* @param shouldJoin In case a join is already applied elsewhere and therefore you want to avoid to re joining the data in that case you can return false for specific relations
*/
export function applyOrdering<T extends ObjectLiteral>({
repository,
order,
qb,
alias,
shouldJoin,
}: {
repository: Repository<T>
order: Record<string, "ASC" | "DESC">
qb: SelectQueryBuilder<T>
alias: string
shouldJoin: (relation: string) => boolean
}) {
const toSelect: string[] = []
const parsed = Object.entries(order).reduce(
(acc, [orderPath, orderDirection]) => {
// If the orderPath (e.g variants.prices.amount) includes a point it means that it is to access
// a child relation of an unknown depth
if (orderPath.includes(".")) {
// We are spliting the path and separating the relations from the property to order. (e.g relations ["variants", "prices"] and property "amount"
const relationsToJoin = orderPath.split(".")
const propToOrder = relationsToJoin.pop()
// For each relation we will retrieve the metadata in order to use the right property name from the relation registered in the entity.
// Each time we will return the child (i.e the relation) and the inverse metadata (corresponding to the child metadata from the parent point of view)
// In order for the next child to know its parent
relationsToJoin.reduce(
([parent, parentMetadata], child) => {
// Find the relation metadata from the parent entity
const relationMetadata = (
parentMetadata as EntityMetadata
).relations.find(
(relationMetadata) => relationMetadata.propertyName === child
)
// The consumer can refuse to apply a join on a relation if the join has already been applied before calling this util
const shouldApplyJoin = shouldJoin(child)
if (shouldApplyJoin) {
qb.leftJoin(`${parent}.${relationMetadata!.propertyPath}`, child)
}
// Return the child relation to be the parent for the next one, as well as the metadata corresponding the child in order
// to find the next relation metadata for the next child
return [child, relationMetadata!.inverseEntityMetadata]
},
[alias, repository.metadata]
)
// The key for variants.prices.amount will be "prices.amount" since we are ordering on the join added to its parent "variants" in this example
const key = `${
relationsToJoin[relationsToJoin.length - 1]
}.${propToOrder}`
acc[key] = orderDirection
toSelect.push(key)
return acc
}
const key = `${alias}.${orderPath}`
// Prevent ambiguous column error when top level entity id is ordered
if (orderPath !== "id") {
toSelect.push(key)
}
acc[key] = orderDirection
return acc
},
{}
)
qb.addSelect(toSelect)
qb.orderBy(parsed)
}
@@ -1,47 +0,0 @@
import { pick } from "lodash"
import { isDefined } from "medusa-core-utils"
import { filter, isNull } from "lodash"
// TODO: When we implement custom queries for tree paths in medusa, remove the transformer
// Adding this here since typeorm tree repo doesn't allow configs to be passed
// onto its children nodes. As an alternative, we are transforming the data post query.
export function transformTreeNodesWithConfig(
object,
config,
scope = {},
isParentNode = false
) {
const selects = (config.select || []) as string[]
const relations = (config.relations || []) as string[]
const selectsAndRelations = selects.concat(relations)
for (const [key, value] of Object.entries(scope)) {
const modelValue = object[key]
if (isDefined(modelValue) && modelValue !== value) {
return null
}
}
if (object.parent_category) {
object.parent_category = transformTreeNodesWithConfig(
object.parent_category,
config,
scope,
true
)
}
if (!isParentNode && (object.category_children || []).length > 0) {
object.category_children = object.category_children.map((child) => {
return transformTreeNodesWithConfig(child, config, scope)
})
object.category_children = filter(
object.category_children,
(el) => !isNull(el)
)
}
return pick(object, selectsAndRelations)
}
-41
View File
@@ -1,41 +0,0 @@
/**
* 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
}
-73
View File
@@ -1,73 +0,0 @@
import { ClassConstructor, plainToInstance } from "class-transformer"
import { validate, ValidationError, ValidatorOptions } from "class-validator"
import { MedusaError } from "medusa-core-utils"
import { Constructor } from "@medusajs/types"
const extendedValidators: Map<string, Constructor<any>> = new Map()
/**
* When overriding a validator, you can register it to be used instead of the original one.
* For example, the place where you are overriding the core validator, you can call this function
* @example
* ```ts
* // /src/api/routes/admin/products/create-product.ts
* import { registerOverriddenValidators } from "@medusajs/medusa"
* import { AdminPostProductsReq as MedusaAdminPostProductsReq } from "@medusajs/medusa/dist/api/routes/admin/products/create-product"
* import { IsString } from "class-validator"
*
* class AdminPostProductsReq extends MedusaAdminPostProductsReq {
* @IsString()
* test: string
* }
*
* registerOverriddenValidators(AdminPostProductsReq)
* ```
* @param extendedValidator
*/
export function registerOverriddenValidators(
extendedValidator: Constructor<any>
): void {
extendedValidators.set(extendedValidator.name, extendedValidator)
}
const reduceErrorMessages = (errs: ValidationError[]): string[] => {
return errs.reduce((acc: string[], next) => {
if (next.constraints) {
for (const [_, msg] of Object.entries(next.constraints)) {
acc.push(msg)
}
}
if (next.children) {
acc.push(...reduceErrorMessages(next.children))
}
return acc
}, [])
}
export async function validator<T, V>(
typedClass: ClassConstructor<T>,
plain: V,
config: ValidatorOptions = {}
): Promise<T> {
typedClass = extendedValidators.get(typedClass.name) ?? typedClass
const toValidate = plainToInstance(typedClass, plain)
// @ts-ignore
const errors = await validate(toValidate, {
whitelist: true,
forbidNonWhitelisted: true,
...config,
})
const errorMessages = reduceErrorMessages(errors)
if (errors?.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
errorMessages.join(", ")
)
}
return toValidate
}
@@ -1,9 +0,0 @@
import { isDate } from "@medusajs/utils"
export const transformDate = ({ value }): Date => {
return isDate(value) ? new Date(value) : new Date(Number(value) * 1000)
}
export const transformOptionalDate = ({ value }) => {
return !isDate(value) ? value : transformDate({ value })
}
@@ -1,34 +0,0 @@
import {
registerDecorator,
ValidationArguments,
ValidationOptions,
isDefined,
} from "class-validator"
import { MedusaError } from "medusa-core-utils"
export function IsGreaterThan(
property: string,
validationOptions?: ValidationOptions
) {
return function (object: any, propertyName: string): void {
registerDecorator({
name: "IsGreaterThan",
target: object.constructor,
propertyName: propertyName,
constraints: [property],
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints
const relatedValue = args.object[relatedPropertyName]
return relatedValue ? value > relatedValue : isDefined(value)
},
defaultMessage(args?: ValidationArguments): string {
return `"${propertyName}" must be greater than ${JSON.stringify(
args?.constraints[0]
)}`
},
},
})
}
}
@@ -1,14 +0,0 @@
// Util function defining the transformation of booleans when part of req.query
// e.g. /admin/shipping-options?is_return=false -> false
//
// We've previously been using @Type(() => Boolean), but this will always return true for strings.
// See https://github.com/typestack/class-transformer/issues/676
// and https://github.com/typestack/class-transformer/issues/306
//
// The solution here is stolen from: https://github.com/typestack/class-transformer/issues/676#issuecomment-822699830
export const optionalBooleanMapper = new Map([
["undefined", undefined],
["null", null],
["true", true],
["false", false],
])
@@ -1,120 +0,0 @@
import {
isArray,
isNumber,
isString,
registerDecorator,
ValidationArguments,
ValidationOptions,
} from "class-validator"
import { isDate } from "lodash"
import { MedusaError } from "medusa-core-utils"
import { validator } from "../validator"
import { promiseAll } from "@medusajs/utils"
async function typeValidator(
typedClass: any,
plain: unknown
): Promise<boolean> {
switch (typedClass) {
case String:
if (!isString(plain)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`String validation failed: ${plain} is not a string`
)
}
return true
case Number:
if (!isNumber(Number(plain))) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Number validation failed: ${plain} is not a number`
)
}
return true
case Date:
if (!isDate(new Date(plain as string))) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Date validation failed: ${plain} is not a date`
)
}
return true
default:
if (isArray(typedClass) && isArray(plain)) {
const errors: Map<any, string> = new Map()
const result = (
await promiseAll(
(plain as any[]).map(
async (p) =>
await typeValidator(typedClass[0], p).catch((e) => {
errors.set(typedClass[0].name, e.message.split(","))
return false
})
)
)
).some(Boolean)
if (result) {
return true
}
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
Object.fromEntries(errors.entries())
)
}
return (
(await validator(typedClass, plain).then(() => true)) &&
typeof plain === "object"
)
}
}
export function IsType(types: any[], validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string): void {
registerDecorator({
name: "IsType",
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
async validate(value: unknown, args: ValidationArguments) {
const errors: Map<any, string> = new Map()
const results = await promiseAll(
types.map(
async (v) =>
await typeValidator(v, value).catch((e) => {
errors.set(v.name, e.message.split(",").filter(Boolean))
return false
})
)
)
if (results.some(Boolean)) {
return true
}
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
JSON.stringify({
message: `${args.property} must be one of: ${types.map(
(t) => `${t.name || (Array.isArray(t) ? t[0]?.name : "")}`
)}`,
details: Object.fromEntries(errors.entries()),
})
)
},
defaultMessage(validationArguments?: ValidationArguments) {
const names = types.map(
(t) => t.name || (isArray(t) ? `${t[0].name}[]` : "")
)
return `${validationArguments?.property} must be one of ${names
.join(", ")
.replace(/, ([^,]*)$/, " or $1")}`
},
},
})
}
}
@@ -1,26 +0,0 @@
import {
registerDecorator,
ValidationArguments,
ValidationOptions,
} from "class-validator"
export function IsISO8601Duration(validationOptions?: ValidationOptions) {
return function (object: any, propertyName: string): void {
registerDecorator({
name: "IsGreaterThan",
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const isoDurationRegex =
/^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/
return isoDurationRegex.test(value)
},
defaultMessage(args?: ValidationArguments): string {
return `"${propertyName}" must be a valid ISO 8601 duration`
},
},
})
}
}