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:
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}`
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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`
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user