feat(): Update transformer middleware and API (#6647)
**What** Update all transform middleware to support the new API - deprecate `defaultRelations` - deprecate `allowedRelations` - Add `defaults` and `allowed` in replacement for `defaultFields` and `allowedFields` respectively - in the `defaults` it is possible to specify a field such as `*variants` in order to be recognized as a relation only without specifying any property - add support for `remoteQueryConfig` assigned to req like we have for `listConfig` and `retrieveConfig` - add support to override `allowed|allowedFields` if a previous middleware have set it up on the req.allowed - The api now accepts `fields` as the only accepted fields to manage the requested props and relations, the `expand` property have been deprecated. New supported symbols have been added in complement of the fields - `+` (e.g `/store/products?fields=+description`) to specify that description should be added as part of the returned data among the other defined fields - `-` (e.g `/store/products?fields=-description`) to specify that description should be removed as part of the returned data - `*` (e.g `/store/products?fields=*variants`) to specify that the variants relations should be added as part of the returned data among the other defined fields without having to specify which property of the variants should be returned. In the `defaults` config of the transform middleware it is also possible to use this symbol - In the case no symbol is provided, it will replace the default fields and mean that only the specified fields must be returned About the allowed validation, all fields in the `defaults` configuration must be present in the `allowed` configuration. In case the `defaults` contains full relation selection (e.g `*product.variants`) it should be present in the `allowed` as `product.variants`. In case in the `defaults` you add `product.variants.id`, it will be allowed if the `allowed` configuration includes either `product.variants.id` as full match or `product.variants` as it means that we allow all properties from `product.variants` Also, support for `*` selection on the remote query/joiner have been added **Note** All v2 end points refactoring can be done separately
This commit is contained in:
@@ -2,6 +2,7 @@ import { pick } from "lodash"
|
||||
import { FindConfig, QueryConfig, RequestQueryFields } from "../types/common"
|
||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
||||
import { BaseEntity } from "../interfaces"
|
||||
import { getSetDifference, stringToSelectRelationObject } from "@medusajs/utils"
|
||||
|
||||
export function pickByConfig<TModel extends BaseEntity>(
|
||||
obj: TModel | TModel[],
|
||||
@@ -19,93 +20,146 @@ export function pickByConfig<TModel extends BaseEntity>(
|
||||
return obj
|
||||
}
|
||||
|
||||
export function getRetrieveConfig<TModel extends BaseEntity>(
|
||||
defaultFields: (keyof TModel)[],
|
||||
defaultRelations: string[],
|
||||
fields?: (keyof TModel)[],
|
||||
expand?: string[]
|
||||
): FindConfig<TModel> {
|
||||
let includeFields: (keyof TModel)[] = []
|
||||
if (isDefined(fields)) {
|
||||
includeFields = Array.from(new Set([...fields, "id"])).map((field) => {
|
||||
return typeof field === "string" ? field.trim() : field
|
||||
}) as (keyof TModel)[]
|
||||
}
|
||||
|
||||
let expandFields: string[] = []
|
||||
if (isDefined(expand)) {
|
||||
expandFields = expand.map((expandRelation) => expandRelation.trim())
|
||||
}
|
||||
|
||||
return {
|
||||
select: includeFields.length ? includeFields : defaultFields,
|
||||
relations: isDefined(expand) ? expandFields : defaultRelations,
|
||||
}
|
||||
}
|
||||
|
||||
export function getListConfig<TModel extends BaseEntity>(
|
||||
defaultFields: (keyof TModel)[],
|
||||
defaultRelations: string[],
|
||||
fields?: (keyof TModel)[],
|
||||
expand?: string[],
|
||||
limit = 50,
|
||||
offset = 0,
|
||||
order: { [k: string | symbol]: "DESC" | "ASC" } = {}
|
||||
): FindConfig<TModel> {
|
||||
let includeFields: (keyof TModel)[] = []
|
||||
if (isDefined(fields)) {
|
||||
const fieldSet = new Set(fields)
|
||||
// Ensure created_at is included, since we are sorting on this
|
||||
fieldSet.add("created_at")
|
||||
fieldSet.add("id")
|
||||
includeFields = Array.from(fieldSet) as (keyof TModel)[]
|
||||
}
|
||||
|
||||
let expandFields: string[] = []
|
||||
if (isDefined(expand)) {
|
||||
expandFields = expand
|
||||
}
|
||||
|
||||
const orderBy = order
|
||||
|
||||
if (!Object.keys(order).length) {
|
||||
orderBy["created_at"] = "DESC"
|
||||
}
|
||||
|
||||
return {
|
||||
select: includeFields.length ? includeFields : defaultFields,
|
||||
relations: isDefined(expand) ? expandFields : defaultRelations,
|
||||
skip: offset,
|
||||
take: limit,
|
||||
order: orderBy,
|
||||
}
|
||||
}
|
||||
|
||||
export function prepareListQuery<
|
||||
T extends RequestQueryFields,
|
||||
TEntity extends BaseEntity
|
||||
>(validated: T, queryConfig?: QueryConfig<TEntity>) {
|
||||
const { order, fields, expand, limit, offset } = validated
|
||||
>(validated: T, queryConfig: QueryConfig<TEntity> = {}) {
|
||||
const { order, fields, limit = 50, expand, offset = 0 } = validated
|
||||
let {
|
||||
allowed = [],
|
||||
defaults = [],
|
||||
defaultFields = [],
|
||||
defaultLimit,
|
||||
allowedFields = [],
|
||||
allowedRelations = [],
|
||||
defaultRelations = [],
|
||||
isList,
|
||||
} = queryConfig
|
||||
|
||||
let expandRelations: string[] | undefined = undefined
|
||||
if (isDefined(expand)) {
|
||||
expandRelations = expand.split(",").filter((v) => v)
|
||||
}
|
||||
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>
|
||||
|
||||
let expandFields: (keyof TEntity)[] | undefined = undefined
|
||||
if (isDefined(fields)) {
|
||||
expandFields = (fields.split(",") as (keyof TEntity)[]).filter((v) => v)
|
||||
const customFields = fields.split(",").filter(Boolean)
|
||||
const shouldReplaceDefaultFields =
|
||||
!customFields.length ||
|
||||
customFields.some((field) => {
|
||||
return !(
|
||||
field.startsWith("-") ||
|
||||
field.startsWith("+") ||
|
||||
field.startsWith("*")
|
||||
)
|
||||
})
|
||||
if (shouldReplaceDefaultFields) {
|
||||
allFields = new Set(customFields.map((f) => f.replace(/^[+-]/, "")))
|
||||
} else {
|
||||
customFields.forEach((field) => {
|
||||
if (field.startsWith("+")) {
|
||||
allFields.add(field.replace(/^\+/, ""))
|
||||
} else if (field.startsWith("-")) {
|
||||
allFields.delete(field.replace(/^-/, ""))
|
||||
} else {
|
||||
allFields.add(field)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Maintain backward compatibility, remove in future. the created at was only added in the list query for default order
|
||||
if (queryConfig.isList) {
|
||||
allFields.add("created_at")
|
||||
}
|
||||
allFields.add("id")
|
||||
}
|
||||
|
||||
if (expandFields?.length && queryConfig?.allowedFields?.length) {
|
||||
validateFields(expandFields as string[], queryConfig.allowedFields)
|
||||
allFields.forEach((field) => {
|
||||
if (field.startsWith("*")) {
|
||||
starFields.add(field.replace(/^\*/, ""))
|
||||
allFields.delete(field)
|
||||
}
|
||||
})
|
||||
|
||||
const allAllowedFields = new Set(allowedFields) // In case there is no allowedFields, allow all fields
|
||||
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 (expandRelations?.length && queryConfig?.allowedRelations?.length) {
|
||||
validateRelations(expandRelations, queryConfig.allowedRelations)
|
||||
if (allFields.size && notAllowedFields.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Requested fields [${Array.from(notAllowedFields).join(
|
||||
", "
|
||||
)}] are not valid`
|
||||
)
|
||||
}
|
||||
|
||||
let orderBy: { [k: symbol]: "DESC" | "ASC" } | undefined
|
||||
const { select, relations } = stringToSelectRelationObject(
|
||||
Array.from(allFields)
|
||||
)
|
||||
|
||||
// TODO: maintain backward compatibility, remove in the future
|
||||
let allRelations = new Set([
|
||||
...relations,
|
||||
...defaultRelations,
|
||||
...Array.from(starFields),
|
||||
])
|
||||
|
||||
if (isDefined(expand)) {
|
||||
allRelations = new Set(expand.split(",").filter(Boolean))
|
||||
}
|
||||
|
||||
const allAllowedRelations = new Set([
|
||||
...Array.from(allAllowedFields),
|
||||
...allowedRelations,
|
||||
])
|
||||
const notAllowedRelations = !allowedRelations.length
|
||||
? new Set()
|
||||
: 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("-")) {
|
||||
@@ -125,82 +179,52 @@ export function prepareListQuery<
|
||||
`Order field ${orderField} is not valid`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
orderBy["created_at"] = "DESC"
|
||||
}
|
||||
|
||||
return getListConfig<TEntity>(
|
||||
queryConfig?.defaultFields as (keyof TEntity)[],
|
||||
(queryConfig?.defaultRelations ?? []) as string[],
|
||||
expandFields,
|
||||
expandRelations,
|
||||
limit ?? queryConfig?.defaultLimit,
|
||||
offset ?? 0,
|
||||
orderBy
|
||||
)
|
||||
return {
|
||||
listConfig: {
|
||||
select: select.length ? select : undefined,
|
||||
relations: Array.from(allRelations),
|
||||
skip: offset,
|
||||
take: limit ?? defaultLimit,
|
||||
order: orderBy,
|
||||
},
|
||||
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: orderBy,
|
||||
}
|
||||
: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function prepareRetrieveQuery<
|
||||
T extends RequestQueryFields,
|
||||
TEntity extends BaseEntity
|
||||
>(validated: T, queryConfig?: QueryConfig<TEntity>) {
|
||||
const { fields, expand } = validated
|
||||
|
||||
let expandRelations: string[] | undefined = undefined
|
||||
if (isDefined(expand)) {
|
||||
expandRelations = expand.split(",").filter((v) => v)
|
||||
}
|
||||
|
||||
let expandFields: (keyof TEntity)[] | undefined = undefined
|
||||
if (isDefined(fields)) {
|
||||
expandFields = (fields.split(",") as (keyof TEntity)[]).filter((v) => v)
|
||||
}
|
||||
|
||||
if (expandFields?.length && queryConfig?.allowedFields?.length) {
|
||||
validateFields(expandFields as string[], queryConfig.allowedFields)
|
||||
}
|
||||
|
||||
if (expandRelations?.length && queryConfig?.allowedRelations?.length) {
|
||||
validateRelations(expandRelations, queryConfig.allowedRelations)
|
||||
}
|
||||
|
||||
return getRetrieveConfig<TEntity>(
|
||||
queryConfig?.defaultFields as (keyof TEntity)[],
|
||||
(queryConfig?.defaultRelations ?? []) as string[],
|
||||
expandFields,
|
||||
expandRelations
|
||||
const { listConfig, remoteQueryConfig } = prepareListQuery(
|
||||
validated,
|
||||
queryConfig
|
||||
)
|
||||
}
|
||||
|
||||
function validateRelations(
|
||||
relations: string[],
|
||||
allowed: string[]
|
||||
): void | never {
|
||||
const disallowedRelationsFound: string[] = []
|
||||
relations?.forEach((field) => {
|
||||
if (!allowed.includes(field as string)) {
|
||||
disallowedRelationsFound.push(field)
|
||||
}
|
||||
})
|
||||
|
||||
if (disallowedRelationsFound.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Relations [${disallowedRelationsFound.join(", ")}] are not valid`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function validateFields(fields: string[], allowed: string[]): void | never {
|
||||
const disallowedFieldsFound: string[] = []
|
||||
fields?.forEach((field) => {
|
||||
if (!allowed.includes(field as string)) {
|
||||
disallowedFieldsFound.push(field)
|
||||
}
|
||||
})
|
||||
|
||||
if (disallowedFieldsFound.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Fields [${disallowedFieldsFound.join(", ")}] are not valid`
|
||||
)
|
||||
return {
|
||||
retrieveConfig: {
|
||||
select: listConfig.select,
|
||||
relations: listConfig.relations,
|
||||
},
|
||||
remoteQueryConfig: {
|
||||
fields: remoteQueryConfig.fields,
|
||||
pagination: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user