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:
Harminder Virk
2024-10-03 09:42:00 +00:00
committed by GitHub
co-authored by Adrien de Peretti
parent 193f93464f
commit 48e00169d2
557 changed files with 2365 additions and 3499 deletions
@@ -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
View File
@@ -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: {},
},
}
}
-2
View File
@@ -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"
-34
View File
@@ -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
}
}
}