**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
243 lines
6.0 KiB
TypeScript
243 lines
6.0 KiB
TypeScript
import {
|
|
RemoteFetchDataCallback,
|
|
RemoteJoiner,
|
|
toRemoteJoinerQuery,
|
|
} from "@medusajs/orchestration"
|
|
import {
|
|
JoinerRelationship,
|
|
JoinerServiceConfig,
|
|
LoadedModule,
|
|
ModuleJoinerConfig,
|
|
RemoteExpandProperty,
|
|
RemoteJoinerQuery,
|
|
} from "@medusajs/types"
|
|
import { isString, toPascalCase } from "@medusajs/utils"
|
|
|
|
import { MedusaModule } from "./medusa-module"
|
|
|
|
export class RemoteQuery {
|
|
private remoteJoiner: RemoteJoiner
|
|
private modulesMap: Map<string, LoadedModule> = new Map()
|
|
private customRemoteFetchData?: RemoteFetchDataCallback
|
|
|
|
constructor({
|
|
modulesLoaded,
|
|
customRemoteFetchData,
|
|
servicesConfig = [],
|
|
}: {
|
|
modulesLoaded?: LoadedModule[]
|
|
customRemoteFetchData?: RemoteFetchDataCallback
|
|
servicesConfig?: ModuleJoinerConfig[]
|
|
}) {
|
|
const servicesConfig_ = [...servicesConfig]
|
|
|
|
if (!modulesLoaded?.length) {
|
|
modulesLoaded = MedusaModule.getLoadedModules().map(
|
|
(mod) => Object.values(mod)[0]
|
|
)
|
|
}
|
|
|
|
for (const mod of modulesLoaded) {
|
|
if (!mod.__definition.isQueryable) {
|
|
continue
|
|
}
|
|
|
|
const serviceName = mod.__definition.key
|
|
|
|
if (this.modulesMap.has(serviceName)) {
|
|
throw new Error(
|
|
`Duplicated instance of module ${serviceName} is not allowed.`
|
|
)
|
|
}
|
|
|
|
this.modulesMap.set(serviceName, mod)
|
|
servicesConfig_!.push(mod.__joinerConfig)
|
|
}
|
|
|
|
this.customRemoteFetchData = customRemoteFetchData
|
|
|
|
this.remoteJoiner = new RemoteJoiner(
|
|
servicesConfig_ as JoinerServiceConfig[],
|
|
this.remoteFetchData.bind(this),
|
|
{ autoCreateServiceNameAlias: false }
|
|
)
|
|
}
|
|
|
|
public setFetchDataCallback(
|
|
remoteFetchData: (
|
|
expand: RemoteExpandProperty,
|
|
keyField: string,
|
|
ids?: (unknown | unknown[])[],
|
|
relationship?: any
|
|
) => Promise<{
|
|
data: unknown[] | { [path: string]: unknown[] }
|
|
path?: string
|
|
}>
|
|
): void {
|
|
this.remoteJoiner.setFetchDataCallback(remoteFetchData)
|
|
}
|
|
|
|
public static getAllFieldsAndRelations(
|
|
data: any,
|
|
prefix = "",
|
|
args: Record<string, unknown[]> = {}
|
|
): {
|
|
select: string[]
|
|
relations: string[]
|
|
args: Record<string, unknown[]>
|
|
} {
|
|
let fields: Set<string> = new Set()
|
|
let relations: string[] = []
|
|
|
|
data.fields?.forEach((field: string) => {
|
|
if (field === "*") {
|
|
// Select all, so we don't specify any field and rely on relation only
|
|
return
|
|
}
|
|
fields.add(prefix ? `${prefix}.${field}` : field)
|
|
})
|
|
args[prefix] = data.args
|
|
|
|
if (data.expands) {
|
|
for (const property in data.expands) {
|
|
const newPrefix = prefix ? `${prefix}.${property}` : property
|
|
|
|
relations.push(newPrefix)
|
|
fields.delete(newPrefix)
|
|
|
|
const result = RemoteQuery.getAllFieldsAndRelations(
|
|
data.expands[property],
|
|
newPrefix,
|
|
args
|
|
)
|
|
|
|
result.select.forEach(fields.add, fields)
|
|
relations = relations.concat(result.relations)
|
|
}
|
|
}
|
|
|
|
return { select: [...fields], relations, args }
|
|
}
|
|
|
|
private hasPagination(options: { [attr: string]: unknown }): boolean {
|
|
if (!options) {
|
|
return false
|
|
}
|
|
|
|
const attrs = ["skip", "cursor"]
|
|
return Object.keys(options).some((key) => attrs.includes(key))
|
|
}
|
|
|
|
private buildPagination(options, count) {
|
|
return {
|
|
skip: options.skip,
|
|
take: options.take,
|
|
cursor: options.cursor,
|
|
// TODO: next cursor
|
|
count,
|
|
}
|
|
}
|
|
|
|
public async remoteFetchData(
|
|
expand: RemoteExpandProperty,
|
|
keyField: string,
|
|
ids?: (unknown | unknown[])[],
|
|
relationship?: JoinerRelationship
|
|
): Promise<{
|
|
data: unknown[] | { [path: string]: unknown }
|
|
path?: string
|
|
}> {
|
|
if (this.customRemoteFetchData) {
|
|
const resp = await this.customRemoteFetchData(expand, keyField, ids)
|
|
if (resp !== undefined) {
|
|
return resp
|
|
}
|
|
}
|
|
|
|
const serviceConfig = expand.serviceConfig
|
|
const service = this.modulesMap.get(serviceConfig.serviceName)!
|
|
|
|
let filters = {}
|
|
const options = {
|
|
...RemoteQuery.getAllFieldsAndRelations(expand),
|
|
}
|
|
|
|
const availableOptions = [
|
|
"skip",
|
|
"take",
|
|
"limit",
|
|
"offset",
|
|
"cursor",
|
|
"sort",
|
|
"withDeleted",
|
|
]
|
|
const availableOptionsAlias = new Map([
|
|
["limit", "take"],
|
|
["offset", "skip"],
|
|
])
|
|
|
|
for (const arg of expand.args || []) {
|
|
if (arg.name === "filters" && arg.value) {
|
|
filters = { ...arg.value }
|
|
} else if (availableOptions.includes(arg.name)) {
|
|
const argName = availableOptionsAlias.has(arg.name)
|
|
? availableOptionsAlias.get(arg.name)!
|
|
: arg.name
|
|
options[argName] = arg.value
|
|
}
|
|
}
|
|
|
|
if (ids) {
|
|
filters[keyField] = ids
|
|
}
|
|
|
|
const hasPagination = this.hasPagination(options)
|
|
|
|
let methodName = hasPagination ? "listAndCount" : "list"
|
|
|
|
if (relationship?.args?.methodSuffix) {
|
|
methodName += toPascalCase(relationship.args.methodSuffix)
|
|
} else if (serviceConfig?.args?.methodSuffix) {
|
|
methodName += toPascalCase(serviceConfig.args.methodSuffix)
|
|
}
|
|
|
|
if (typeof service[methodName] !== "function") {
|
|
throw new Error(
|
|
`Method "${methodName}" does not exist on "${serviceConfig.serviceName}"`
|
|
)
|
|
}
|
|
|
|
const result = await service[methodName](filters, options)
|
|
|
|
if (hasPagination) {
|
|
const [data, count] = result
|
|
return {
|
|
data: {
|
|
rows: data,
|
|
metadata: this.buildPagination(options, count),
|
|
},
|
|
path: "rows",
|
|
}
|
|
}
|
|
|
|
return {
|
|
data: result,
|
|
}
|
|
}
|
|
|
|
public async query(
|
|
query: string | RemoteJoinerQuery | object,
|
|
variables?: Record<string, unknown>
|
|
): Promise<any> {
|
|
let finalQuery: RemoteJoinerQuery = query as RemoteJoinerQuery
|
|
|
|
if (isString(query)) {
|
|
finalQuery = RemoteJoiner.parseQuery(query, variables)
|
|
} else if (!isString(finalQuery?.service) && !isString(finalQuery?.alias)) {
|
|
finalQuery = toRemoteJoinerQuery(query, variables)
|
|
}
|
|
|
|
return await this.remoteJoiner.query(finalQuery)
|
|
}
|
|
}
|