breaking: move shared HTTP utils to the framework (#9402)
Fixes: FRMW-2728, FRMW-2729 After this PR gets merged the following middleware will be exported from the `@medusajs/framework/http` import path. - applyParamsAsFilters - clearFiltersByKey - applyDefaultFilters - setContext - getQueryConfig - httpCompression - maybeApplyLinkFilter - refetchEntities - unlessPath - validateBody - validateQuery Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com>
This commit is contained in:
co-authored by
Adrien de Peretti
parent
193f93464f
commit
48e00169d2
@@ -1,6 +1,6 @@
|
||||
import zod from "zod"
|
||||
import { defineMiddlewares } from "../define-middlewares"
|
||||
import { MedusaRequest, MedusaResponse } from "../../types/routing"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
|
||||
describe("defineMiddlewares", function () {
|
||||
test("define custom middleware for a route", () => {
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { omitDeep } from "../omit-deep"
|
||||
|
||||
describe("omitDeep", () => {
|
||||
it("should omit properties in a nested object", () => {
|
||||
const input = {
|
||||
id: 1,
|
||||
__typename: "123",
|
||||
createdAt: "1020209",
|
||||
address: {
|
||||
id: 1,
|
||||
__typename: "123",
|
||||
},
|
||||
variants: [
|
||||
20,
|
||||
{
|
||||
id: 22,
|
||||
title: "hello world",
|
||||
__typename: "123",
|
||||
createdAt: "1020209",
|
||||
variantOption: {
|
||||
id: 1,
|
||||
__typename: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 32,
|
||||
test: null,
|
||||
__typename: "123",
|
||||
createdAt: "1020209",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const output = {
|
||||
id: 1,
|
||||
address: {
|
||||
id: 1,
|
||||
},
|
||||
variants: [
|
||||
20,
|
||||
{
|
||||
id: 22,
|
||||
title: "hello world",
|
||||
variantOption: {
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 32,
|
||||
test: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
expect(
|
||||
omitDeep(input, ["createdAt", "updatedAt", "__typename"])
|
||||
).toEqual(output)
|
||||
})
|
||||
})
|
||||
@@ -1,47 +0,0 @@
|
||||
import { removeUndefinedProperties } from "../remove-undefined-properties";
|
||||
|
||||
describe("removeUndefinedProperties", () => {
|
||||
it("should remove all undefined properties from an input object", () => {
|
||||
const inputObj = {
|
||||
test: undefined,
|
||||
test1: "test1",
|
||||
test2: null,
|
||||
test3: {
|
||||
test3_1: undefined,
|
||||
test3_2: "test3_2",
|
||||
test3_3: null,
|
||||
},
|
||||
test4: [
|
||||
undefined,
|
||||
null,
|
||||
"null",
|
||||
[1, 2, undefined],
|
||||
{
|
||||
test4_1: undefined,
|
||||
test4_2: "test4_2",
|
||||
test4_3: null,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const cleanObject = removeUndefinedProperties(inputObj)
|
||||
|
||||
expect(cleanObject).toEqual({
|
||||
test1: "test1",
|
||||
test2: null,
|
||||
test3: {
|
||||
test3_2: "test3_2",
|
||||
test3_3: null,
|
||||
},
|
||||
test4: [
|
||||
null,
|
||||
null,
|
||||
[1, 2],
|
||||
{
|
||||
test4_2: "test4_2",
|
||||
test4_3: null
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,38 +0,0 @@
|
||||
import {
|
||||
HttpCompressionOptions,
|
||||
ProjectConfigOptions,
|
||||
} from "@medusajs/framework/types"
|
||||
import compression from "compression"
|
||||
import { Request, Response } from "express"
|
||||
|
||||
export function shouldCompressResponse(req: Request, res: Response) {
|
||||
const { projectConfig } = req.scope.resolve("configModule")
|
||||
const { enabled } = compressionOptions(projectConfig)
|
||||
|
||||
if (!enabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (req.headers["x-no-compression"]) {
|
||||
// don't compress responses with this request header
|
||||
return false
|
||||
}
|
||||
|
||||
// fallback to standard filter function
|
||||
return compression.filter(req, res)
|
||||
}
|
||||
|
||||
export function compressionOptions(
|
||||
config: ProjectConfigOptions
|
||||
): HttpCompressionOptions {
|
||||
const responseCompressionOptions = config.http.compression ?? {}
|
||||
|
||||
responseCompressionOptions.enabled =
|
||||
responseCompressionOptions.enabled ?? false
|
||||
responseCompressionOptions.level = responseCompressionOptions.level ?? 6
|
||||
responseCompressionOptions.memLevel = responseCompressionOptions.memLevel ?? 8
|
||||
responseCompressionOptions.threshold =
|
||||
responseCompressionOptions.threshold ?? 1024
|
||||
|
||||
return responseCompressionOptions
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./http-compression"
|
||||
@@ -1,5 +1,4 @@
|
||||
import { pickDeep } from "@medusajs/framework/utils"
|
||||
import { omitDeep } from "./omit-deep"
|
||||
import { omitDeep, pickDeep } from "@medusajs/framework/utils"
|
||||
|
||||
// TODO: once the legacy totals decoration will be removed.
|
||||
// We will be able to only compute the totals if one of the total fields is present
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
import { RequestQueryFields } from "@medusajs/framework/types"
|
||||
import {
|
||||
getSetDifference,
|
||||
isDefined,
|
||||
isPresent,
|
||||
MedusaError,
|
||||
stringToSelectRelationObject,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { pick } from "lodash"
|
||||
import { FindConfig, QueryConfig } from "../types/common"
|
||||
|
||||
export function pickByConfig<TModel>(
|
||||
obj: TModel | TModel[],
|
||||
config: FindConfig<TModel>
|
||||
): Partial<TModel> | Partial<TModel>[] {
|
||||
const fields = [...(config.select ?? []), ...(config.relations ?? [])]
|
||||
|
||||
if (fields.length) {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((o) => pick(o, fields))
|
||||
} else {
|
||||
return pick(obj, fields)
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
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 {
|
||||
allowed = [],
|
||||
defaults = [],
|
||||
defaultFields = [],
|
||||
defaultLimit,
|
||||
allowedFields = [],
|
||||
allowedRelations = [],
|
||||
defaultRelations = [],
|
||||
isList,
|
||||
} = queryConfig
|
||||
|
||||
allowedFields = allowed.length ? allowed : allowedFields
|
||||
defaultFields = defaults.length ? defaults : defaultFields
|
||||
|
||||
// e.g *product.variants meaning that we want all fields from the product.variants
|
||||
// in that case it wont be part of the select but it will be part of the relations.
|
||||
// For the remote query we will have to add the fields to the fields array as product.variants.*
|
||||
const starFields: Set<string> = new Set()
|
||||
|
||||
let allFields = new Set(defaultFields) as Set<string>
|
||||
|
||||
if (isDefined(fields)) {
|
||||
const customFields = fields.split(",").filter(Boolean)
|
||||
const shouldReplaceDefaultFields =
|
||||
!customFields.length ||
|
||||
customFields.some((field) => {
|
||||
return !(
|
||||
field.startsWith("-") ||
|
||||
field.startsWith("+") ||
|
||||
field.startsWith(" ") ||
|
||||
field.startsWith("*")
|
||||
)
|
||||
})
|
||||
|
||||
if (shouldReplaceDefaultFields) {
|
||||
allFields = new Set(customFields.map((f) => f.replace(/^[+ -]/, "")))
|
||||
} else {
|
||||
customFields.forEach((field) => {
|
||||
if (field.startsWith("+") || field.startsWith(" ")) {
|
||||
allFields.add(field.trim().replace(/^\+/, ""))
|
||||
} else if (field.startsWith("-")) {
|
||||
allFields.delete(field.replace(/^-/, ""))
|
||||
} else {
|
||||
allFields.add(field)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
allFields.add("id")
|
||||
}
|
||||
|
||||
allFields.forEach((field) => {
|
||||
if (field.startsWith("*")) {
|
||||
starFields.add(field.replace(/^\*/, ""))
|
||||
allFields.delete(field)
|
||||
}
|
||||
})
|
||||
|
||||
const notAllowedFields: string[] = []
|
||||
|
||||
if (allowedFields.length) {
|
||||
;[...allFields, ...Array.from(starFields)].forEach((field) => {
|
||||
const hasAllowedField = allowedFields.includes(field)
|
||||
|
||||
if (hasAllowedField) {
|
||||
return
|
||||
}
|
||||
|
||||
// Select full relation in that case it must match an allowed field fully
|
||||
// e.g product.variants in that case we must have a product.variants in the allowedFields
|
||||
if (starFields.has(field)) {
|
||||
if (hasAllowedField) {
|
||||
return
|
||||
}
|
||||
notAllowedFields.push(field)
|
||||
return
|
||||
}
|
||||
|
||||
const fieldStartsWithAllowedField = allowedFields.some((allowedField) =>
|
||||
field.startsWith(allowedField)
|
||||
)
|
||||
|
||||
if (!fieldStartsWithAllowedField) {
|
||||
notAllowedFields.push(field)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (allFields.size && notAllowedFields.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Requested fields [${Array.from(notAllowedFields).join(
|
||||
", "
|
||||
)}] are not valid`
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: maintain backward compatibility, remove in the future
|
||||
const { select, relations } = stringToSelectRelationObject(
|
||||
Array.from(allFields)
|
||||
)
|
||||
|
||||
let allRelations = new Set([
|
||||
...relations,
|
||||
...defaultRelations,
|
||||
...Array.from(starFields),
|
||||
])
|
||||
|
||||
if (isDefined(expand)) {
|
||||
allRelations = new Set(expand.split(",").filter(Boolean))
|
||||
}
|
||||
|
||||
if (allowedRelations.length && expand) {
|
||||
const allAllowedRelations = new Set([...allowedRelations])
|
||||
|
||||
const notAllowedRelations = getSetDifference(
|
||||
allRelations,
|
||||
allAllowedRelations
|
||||
)
|
||||
|
||||
if (allRelations.size && notAllowedRelations.size) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Requested fields [${Array.from(notAllowedRelations).join(
|
||||
", "
|
||||
)}] are not valid`
|
||||
)
|
||||
}
|
||||
}
|
||||
// End of expand compatibility
|
||||
|
||||
let orderBy: { [k: symbol]: "DESC" | "ASC" } | undefined = {}
|
||||
if (isDefined(order)) {
|
||||
let orderField = order
|
||||
if (order.startsWith("-")) {
|
||||
const [, field] = order.split("-")
|
||||
orderField = field
|
||||
orderBy = { [field]: "DESC" }
|
||||
} else {
|
||||
orderBy = { [order]: "ASC" }
|
||||
}
|
||||
|
||||
if (
|
||||
queryConfig?.allowedFields?.length &&
|
||||
!queryConfig?.allowedFields.includes(orderField)
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Order field ${orderField} is not valid`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const finalOrder = isPresent(orderBy) ? orderBy : undefined
|
||||
return {
|
||||
listConfig: {
|
||||
select: select.length ? select : undefined,
|
||||
relations: Array.from(allRelations),
|
||||
skip: offset,
|
||||
take: limit ?? defaultLimit,
|
||||
order: finalOrder,
|
||||
},
|
||||
remoteQueryConfig: {
|
||||
// Add starFields that are relations only on which we want all properties with a dedicated format to the remote query
|
||||
fields: [
|
||||
...Array.from(allFields),
|
||||
...Array.from(starFields).map((f) => `${f}.*`),
|
||||
],
|
||||
pagination: isList
|
||||
? {
|
||||
skip: offset,
|
||||
take: limit ?? defaultLimit,
|
||||
order: finalOrder,
|
||||
}
|
||||
: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function prepareRetrieveQuery<T extends RequestQueryFields, TEntity>(
|
||||
validated: T,
|
||||
queryConfig?: QueryConfig<TEntity>
|
||||
) {
|
||||
const { listConfig, remoteQueryConfig } = prepareListQuery(
|
||||
validated,
|
||||
queryConfig
|
||||
)
|
||||
|
||||
return {
|
||||
retrieveConfig: {
|
||||
select: listConfig.select,
|
||||
relations: listConfig.relations,
|
||||
},
|
||||
remoteQueryConfig: {
|
||||
fields: remoteQueryConfig.fields,
|
||||
pagination: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
export * from "./clean-response-data"
|
||||
export * from "./exception-formatter"
|
||||
export * from "./middlewares"
|
||||
export * from "./omit-deep"
|
||||
export * from "./define-middlewares"
|
||||
export * from "./remove-undefined-properties"
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { isObject } from "@medusajs/framework/utils"
|
||||
|
||||
export function omitDeep<T extends object = object>(
|
||||
input: object,
|
||||
excludes: Array<number | string>
|
||||
): T {
|
||||
if (!input) {
|
||||
return input
|
||||
}
|
||||
|
||||
return Object.entries(input).reduce((nextInput, [key, value]) => {
|
||||
const shouldExclude = excludes.includes(key)
|
||||
if (shouldExclude) {
|
||||
return nextInput
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
nextInput[key] = value.map((arrItem) => {
|
||||
if (isObject(arrItem)) {
|
||||
return omitDeep(arrItem, excludes)
|
||||
}
|
||||
return arrItem
|
||||
})
|
||||
return nextInput
|
||||
} else if (isObject(value)) {
|
||||
nextInput[key] = omitDeep(value, excludes)
|
||||
return nextInput
|
||||
}
|
||||
|
||||
nextInput[key] = value
|
||||
|
||||
return nextInput
|
||||
}, {} as T)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { isDefined } from "@medusajs/framework/utils"
|
||||
|
||||
export function removeUndefinedProperties<T extends object>(inputObj: T): T {
|
||||
const removeProperties = (obj: T) => {
|
||||
const res = {} as T
|
||||
|
||||
Object.keys(obj).reduce((acc: T, key: string) => {
|
||||
if (typeof obj[key] === "undefined") {
|
||||
return acc
|
||||
}
|
||||
acc[key] = removeUndefinedDeeply(obj[key])
|
||||
return acc
|
||||
}, res)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
return removeProperties(inputObj)
|
||||
}
|
||||
|
||||
function removeUndefinedDeeply(input: unknown): any {
|
||||
if (isDefined(input)) {
|
||||
if (input === null || input === "null") {
|
||||
return null
|
||||
} else if (Array.isArray(input)) {
|
||||
return input
|
||||
.map((item) => {
|
||||
return removeUndefinedDeeply(item)
|
||||
})
|
||||
.filter((v) => isDefined(v))
|
||||
} else if (Object.prototype.toString.call(input) === "[object Date]") {
|
||||
return input
|
||||
} else if (typeof input === "object") {
|
||||
return Object.keys(input).reduce(
|
||||
(acc: Record<string, unknown>, key: string) => {
|
||||
if (typeof input[key] === "undefined") {
|
||||
return acc
|
||||
}
|
||||
acc[key] = removeUndefinedDeeply(input[key])
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
} else {
|
||||
return input
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user