feat(modules-sdk): Parse filters based on loaded modules graph (#9158)

This commit is contained in:
Adrien de Peretti
2024-09-17 19:20:04 +02:00
committed by GitHub
parent 812b80b6a3
commit c6795dfc47
12 changed files with 1206 additions and 93 deletions

View File

@@ -1,4 +1,4 @@
import { IRegionModuleService } from "@medusajs/types"
import { IRegionModuleService, RemoteQueryFunction } from "@medusajs/types"
import { ContainerRegistrationKeys, Modules } from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import { createAdminUser } from "../../..//helpers/create-admin-user"
@@ -195,5 +195,103 @@ medusaIntegrationTestRunner({
).resolves.toHaveLength(1)
})
})
describe("Query", () => {
let appContainer
let query: RemoteQueryFunction
beforeAll(() => {
appContainer = getContainer()
query = appContainer.resolve(ContainerRegistrationKeys.QUERY)
})
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, appContainer)
const payload = {
title: "Test Giftcard",
is_giftcard: true,
description: "test-giftcard-description",
options: [{ title: "Denominations", values: ["100"] }],
variants: [
{
title: "Test variant",
prices: [{ currency_code: "usd", amount: 100 }],
options: {
Denominations: "100",
},
},
],
}
await api
.post("/admin/products", payload, adminHeaders)
.catch((err) => {
console.log(err)
})
})
it(`should perform cross module query and apply filters correctly to the correct modules [1]`, async () => {
const { data } = await query.graph({
entity: "product",
fields: ["id", "title", "variants.*", "variants.prices.amount"],
filters: {
variants: {
prices: {
amount: {
$gt: 100,
},
},
},
},
})
expect(data).toEqual([
expect.objectContaining({
id: expect.any(String),
title: "Test Giftcard",
variants: [
expect.objectContaining({
title: "Test variant",
prices: [],
}),
],
}),
])
})
it(`should perform cross module query and apply filters correctly to the correct modules [2]`, async () => {
const { data: dataWithPrice } = await query.graph({
entity: "product",
fields: ["id", "title", "variants.*", "variants.prices.amount"],
filters: {
variants: {
prices: {
amount: {
$gt: 50,
},
},
},
},
})
expect(dataWithPrice).toEqual([
expect.objectContaining({
id: expect.any(String),
title: "Test Giftcard",
variants: [
expect.objectContaining({
title: "Test variant",
prices: [
expect.objectContaining({
amount: 100,
}),
],
}),
],
}),
])
})
})
},
})

View File

@@ -413,10 +413,12 @@ async function MedusaApp_({
const loadedSchema = getLoadedSchema()
const { schema, notFound } = cleanAndMergeSchema(loadedSchema)
const entitiesMap = schema.getTypeMap() as unknown as Map<string, any>
const remoteQuery = new RemoteQuery({
servicesConfig,
customRemoteFetchData: remoteFetchData,
entitiesMap,
})
const applyMigration = async ({
@@ -521,7 +523,7 @@ async function MedusaApp_({
modules: allModules,
link: remoteLink,
query: createQuery(remoteQuery) as any, // TODO: rm any once we remove the old RemoteQueryFunction and rely on the Query object instead,
entitiesMap: schema.getTypeMap(),
entitiesMap,
gqlSchema: schema,
notFound,
runMigrations,

View File

@@ -0,0 +1,13 @@
import { mergeTypeDefs } from "@graphql-tools/merge"
import { makeExecutableSchema } from "@graphql-tools/schema"
import { cleanGraphQLSchema } from "../../utils/clean-graphql-schema"
export function getEntitiesMap(loadedSchema): Map<string, any> {
const defaultMedusaSchema = `
scalar DateTime
scalar JSON
`
const { schema } = cleanGraphQLSchema(defaultMedusaSchema + loadedSchema)
const mergedSchema = mergeTypeDefs(schema)
return makeExecutableSchema({ typeDefs: mergedSchema }).getTypeMap() as any
}

View File

@@ -0,0 +1,160 @@
import { defineJoinerConfig } from "@medusajs/utils"
import { MedusaModule } from "../../medusa-module"
import { ModuleJoinerConfig } from "@medusajs/types"
const productJoinerConfig = defineJoinerConfig("product", {
schema: `
type Product {
id: ID
title: String
variants: [Variant]
}
type Variant {
id: ID
sku: String
}
`,
alias: [
{
name: ["product"],
entity: "Product",
args: {
methodSuffix: "Products",
},
},
{
name: ["variant", "variants"],
entity: "Variant",
args: {
methodSuffix: "Variants",
},
},
],
})
const pricingJoinerConfig = defineJoinerConfig("pricing", {
schema: `
type PriceSet {
id: ID
prices: [Price]
}
type Price {
amount: Int
deep_nested_price: DeepNestedPrice
}
type DeepNestedPrice {
amount: Int
}
`,
alias: [
{
name: ["price", "prices"],
entity: "Price",
args: {
methodSuffix: "price",
},
},
{
name: ["price_set", "price_sets"],
entity: "PriceSet",
args: {
methodSuffix: "priceSet",
},
},
{
name: ["deep_nested_price", "deep_nested_prices"],
entity: "DeepNestedPrice",
args: {
methodSuffix: "deepNestedPrice",
},
},
],
})
const linkProductVariantPriceSet = {
serviceName: "link-product-variant-price-set",
isLink: true,
databaseConfig: {
tableName: "product_variant_price_set",
idPrefix: "pvps",
},
alias: [
{
name: ["product_variant_price_set", "product_variant_price_sets"],
entity: "LinkProductVariantPriceSet",
},
],
primaryKeys: ["id", "variant_id", "price_set_id"],
relationships: [
{
serviceName: "product",
entity: "ProductVariant",
primaryKey: "id",
foreignKey: "variant_id",
alias: "variant",
args: {
methodSuffix: "ProductVariants",
},
},
{
serviceName: "pricing",
entity: "PriceSet",
primaryKey: "id",
foreignKey: "price_set_id",
alias: "price_set",
args: {
methodSuffix: "PriceSets",
},
deleteCascade: true,
},
],
extends: [
{
serviceName: "product",
fieldAlias: {
price_set: "price_set_link.price_set",
prices: {
path: "price_set_link.price_set.prices",
isList: true,
forwardArgumentsOnPath: ["price_set_link.price_set"],
},
deep_nested_price: {
path: "price_set_link.price_set.deep_nested_price",
forwardArgumentsOnPath: ["price_set_link.price_set"],
},
calculated_price: {
path: "price_set_link.price_set.calculated_price",
forwardArgumentsOnPath: ["price_set_link.price_set"],
},
},
relationship: {
serviceName: "link-product-variant-price-set",
primaryKey: "variant_id",
foreignKey: "id",
alias: "price_set_link",
},
},
{
serviceName: "pricing",
relationship: {
serviceName: "link-product-variant-price-set",
primaryKey: "price_set_id",
foreignKey: "id",
alias: "variant_link",
},
fieldAlias: {
variant: "variant_link.variant",
},
},
],
} as ModuleJoinerConfig
MedusaModule.setJoinerConfig("product", productJoinerConfig)
MedusaModule.setJoinerConfig("pricing", pricingJoinerConfig)
MedusaModule.setJoinerConfig(
"link-product-variant-price-set",
linkProductVariantPriceSet
)

View File

@@ -0,0 +1,548 @@
import { MedusaModule } from "../../medusa-module"
import { getEntitiesMap } from "../__fixtures__/get-entities-map"
import "../__fixtures__/parse-filters"
import { parseAndAssignFilters } from "../parse-filters"
const entitiesMap = getEntitiesMap(
MedusaModule.getAllJoinerConfigs()
.map((m) => m.schema)
.join("\n")
)
describe("parse-filters", () => {
describe("Without operator map usage", () => {
it("should parse filter for a single level module", () => {
const filters = {
id: "string",
}
const remoteQueryObject = {
product: {
fields: ["id", "title", "variants"],
},
}
parseAndAssignFilters(
{
remoteQueryObject,
entryPoint: "product",
filters,
},
entitiesMap
)
expect(remoteQueryObject).toEqual({
product: {
fields: ["id", "title", "variants"],
__args: {
filters: {
id: "string",
},
},
},
})
})
it("should parse filters through linked immediate relations", () => {
const filters = {
id: "string",
variants: {
sku: {
$eq: "string",
},
price_set: {
id: "id_test",
prices: {
amount: 50,
},
},
},
}
const remoteQueryObject = {
product: {
fields: ["id", "title", "variants"],
variants: {
fields: ["id", "sku", "prices"],
price_set: {
fields: ["id", "amount"],
prices: {
fields: ["id", "amount"],
},
},
},
},
}
parseAndAssignFilters(
{
remoteQueryObject,
entryPoint: "product",
filters,
},
entitiesMap
)
expect(remoteQueryObject).toEqual({
product: {
fields: ["id", "title", "variants"],
__args: {
filters: {
id: "string",
variants: {
sku: {
$eq: "string",
},
},
},
},
variants: {
fields: ["id", "sku", "prices"],
price_set: {
fields: ["id", "amount"],
__args: {
filters: {
id: "id_test",
prices: {
amount: 50,
},
},
},
prices: {
fields: ["id", "amount"],
},
},
},
},
})
})
it("should parse filters through linked nested relations through configured field alias with forward args", () => {
const filters = {
id: "string",
variants: {
sku: {
$eq: "string",
},
prices: {
amount: 50,
},
},
}
const remoteQueryObject = {
product: {
fields: ["id", "title", "variants"],
variants: {
fields: ["id", "sku", "prices"],
prices: {
fields: ["id", "amount"],
},
},
},
}
parseAndAssignFilters(
{
remoteQueryObject,
entryPoint: "product",
filters,
},
entitiesMap
)
expect(remoteQueryObject).toEqual({
product: {
fields: ["id", "title", "variants"],
__args: {
filters: {
id: "string",
variants: {
sku: {
$eq: "string",
},
},
},
},
variants: {
fields: ["id", "sku", "prices"],
prices: {
fields: ["id", "amount"],
__args: {
filters: {
prices: {
amount: 50,
},
},
},
},
},
},
})
})
it("should parse filters through linked deep nested relations through configured field alias with forward args", () => {
const filters = {
id: "string",
variants: {
sku: {
$eq: "string",
},
prices: {
amount: 50,
deep_nested_price: {
amount: 100,
},
},
},
}
const remoteQueryObject = {
product: {
fields: ["id", "title", "variants"],
variants: {
fields: ["id", "sku", "prices"],
prices: {
fields: ["id", "amount"],
deep_nested_price: {
fields: ["id", "amount"],
},
},
},
},
}
parseAndAssignFilters(
{
remoteQueryObject,
entryPoint: "product",
filters,
},
entitiesMap
)
expect(remoteQueryObject).toEqual({
product: {
fields: ["id", "title", "variants"],
__args: {
filters: {
id: "string",
variants: {
sku: {
$eq: "string",
},
},
},
},
variants: {
fields: ["id", "sku", "prices"],
prices: {
fields: ["id", "amount"],
__args: {
filters: {
prices: {
amount: 50,
deep_nested_price: {
amount: 100,
},
},
},
},
deep_nested_price: {
fields: ["id", "amount"],
},
},
},
},
})
})
})
describe("With operator map usage", () => {
it("should parse filter for a single level module", () => {
const filters = {
id: { $ilike: "%string%" },
}
const remoteQueryObject = {
product: {
fields: ["id", "title", "variants"],
},
}
parseAndAssignFilters(
{
remoteQueryObject,
entryPoint: "product",
filters,
},
entitiesMap
)
expect(remoteQueryObject).toEqual({
product: {
fields: ["id", "title", "variants"],
__args: {
filters: {
id: { $ilike: "%string%" },
},
},
},
})
})
it("should parse filters through linked immediate relations", () => {
const filters = {
id: "string",
variants: {
sku: {
$eq: "string",
},
price_set: {
id: "id_test",
prices: {
amount: {
$gte: 50,
},
},
},
},
}
const remoteQueryObject = {
product: {
fields: ["id", "title", "variants"],
variants: {
fields: ["id", "sku", "prices"],
price_set: {
fields: ["id", "amount"],
prices: {
fields: ["id", "amount"],
},
},
},
},
}
parseAndAssignFilters(
{
remoteQueryObject,
entryPoint: "product",
filters,
},
entitiesMap
)
expect(remoteQueryObject).toEqual({
product: {
fields: ["id", "title", "variants"],
__args: {
filters: {
id: "string",
variants: {
sku: {
$eq: "string",
},
},
},
},
variants: {
fields: ["id", "sku", "prices"],
price_set: {
fields: ["id", "amount"],
__args: {
filters: {
id: "id_test",
prices: {
amount: {
$gte: 50,
},
},
},
},
prices: {
fields: ["id", "amount"],
},
},
},
},
})
})
it("should parse filters through linked nested relations through configured field alias with forward args", () => {
const filters = {
id: "string",
variants: {
sku: {
$eq: "string",
},
prices: {
amount: {
$lt: 50,
},
},
},
}
const remoteQueryObject = {
product: {
fields: ["id", "title", "variants"],
variants: {
fields: ["id", "sku", "prices"],
prices: {
fields: ["id", "amount"],
},
},
},
}
parseAndAssignFilters(
{
remoteQueryObject,
entryPoint: "product",
filters,
},
entitiesMap
)
expect(remoteQueryObject).toEqual({
product: {
fields: ["id", "title", "variants"],
__args: {
filters: {
id: "string",
variants: {
sku: {
$eq: "string",
},
},
},
},
variants: {
fields: ["id", "sku", "prices"],
prices: {
fields: ["id", "amount"],
__args: {
filters: {
prices: {
amount: { $lt: 50 },
},
},
},
},
},
},
})
})
it("should parse filters through linked deep nested relations through configured field alias with forward args", () => {
const filters = {
id: "string",
variants: {
sku: {
$eq: "string",
},
prices: {
deep_nested_price: {
amount: {
$gte: 100,
},
},
},
},
}
const remoteQueryObject = {
product: {
fields: ["id", "title", "variants"],
variants: {
fields: ["id", "sku", "prices"],
prices: {
fields: ["id", "amount"],
deep_nested_price: {
fields: ["id", "amount"],
},
},
},
},
}
parseAndAssignFilters(
{
remoteQueryObject,
entryPoint: "product",
filters,
},
entitiesMap
)
expect(remoteQueryObject).toEqual({
product: {
fields: ["id", "title", "variants"],
__args: {
filters: {
id: "string",
variants: {
sku: {
$eq: "string",
},
},
},
},
variants: {
fields: ["id", "sku", "prices"],
prices: {
fields: ["id", "amount"],
__args: {
filters: {
prices: {
deep_nested_price: {
amount: { $gte: 100 },
},
},
},
},
deep_nested_price: {
fields: ["id", "amount"],
},
},
},
},
})
})
})
})

View File

@@ -1,18 +1,30 @@
import { QueryContext, QueryFilter } from "@medusajs/utils"
import { QueryContext } from "@medusajs/utils"
import { MedusaModule } from "../../medusa-module"
import { getEntitiesMap } from "../__fixtures__/get-entities-map"
import "../__fixtures__/parse-filters"
import "../__fixtures__/remote-query-type"
import { toRemoteQuery } from "../to-remote-query"
const entitiesMap = getEntitiesMap(
MedusaModule.getAllJoinerConfigs()
.map((m) => m.schema)
.join("\n")
)
describe("toRemoteQuery", () => {
it("should transform a query with top level filtering", () => {
const format = toRemoteQuery({
entity: "product",
fields: ["id", "handle", "description"],
filters: QueryFilter<"product">({
handle: {
$ilike: "abc%",
const format = toRemoteQuery(
{
entity: "product",
fields: ["id", "handle", "description"],
filters: {
handle: {
$ilike: "abc%",
},
},
}),
})
},
entitiesMap
)
expect(format).toEqual({
product: {
@@ -29,14 +41,17 @@ describe("toRemoteQuery", () => {
})
it("should transform a query with pagination", () => {
const format = toRemoteQuery({
entity: "product",
fields: ["id", "handle", "description"],
pagination: {
skip: 5,
take: 10,
const format = toRemoteQuery(
{
entity: "product",
fields: ["id", "handle", "description"],
pagination: {
skip: 5,
take: 10,
},
},
})
entitiesMap
)
expect(format).toEqual({
product: {
@@ -50,19 +65,22 @@ describe("toRemoteQuery", () => {
})
it("should transform a query with top level filtering and pagination", () => {
const format = toRemoteQuery({
entity: "product",
fields: ["id", "handle", "description"],
pagination: {
skip: 5,
take: 10,
},
filters: QueryFilter<"product">({
handle: {
$ilike: "abc%",
const format = toRemoteQuery(
{
entity: "product",
fields: ["id", "handle", "description"],
pagination: {
skip: 5,
take: 10,
},
}),
})
filters: {
handle: {
$ilike: "abc%",
},
},
},
entitiesMap
)
expect(format).toEqual({
product: {
@@ -81,43 +99,48 @@ describe("toRemoteQuery", () => {
})
it("should transform a query with filters and context into remote query input [1]", () => {
const format = toRemoteQuery({
entity: "product",
fields: [
"id",
"description",
"variants.title",
"variants.calculated_price",
"variants.options.*",
],
filters: {
variants: QueryFilter<"variants">({
sku: {
$ilike: "abc%",
const format = toRemoteQuery(
{
entity: "product",
fields: [
"id",
"description",
"variants.title",
"variants.calculated_price",
"variants.options.*",
],
filters: {
variants: {
sku: {
$ilike: "abc%",
},
},
},
context: {
variants: {
calculated_price: QueryContext({
region_id: "reg_123",
currency_code: "usd",
}),
},
}),
},
context: {
variants: {
calculated_price: QueryContext({
region_id: "reg_123",
currency_code: "usd",
}),
},
},
})
entitiesMap
)
expect(format).toEqual({
product: {
__fields: ["id", "description"],
variants: {
__args: {
filters: {
__args: {
filters: {
variants: {
sku: {
$ilike: "abc%",
},
},
},
},
variants: {
calculated_price: {
__args: {
context: {
@@ -142,31 +165,34 @@ describe("toRemoteQuery", () => {
},
})
const format = toRemoteQuery({
entity: "product",
fields: [
"id",
"title",
"description",
"product_translation.*",
"categories.*",
"categories.category_translation.*",
"variants.*",
"variants.variant_translation.*",
],
filters: QueryFilter<"product">({
id: "prod_01J742X0QPFW3R2ZFRTRC34FS8",
}),
context: {
product_translation: langContext,
categories: {
category_translation: langContext,
const format = toRemoteQuery(
{
entity: "product",
fields: [
"id",
"title",
"description",
"product_translation.*",
"categories.*",
"categories.category_translation.*",
"variants.*",
"variants.variant_translation.*",
],
filters: {
id: "prod_01J742X0QPFW3R2ZFRTRC34FS8",
},
variants: {
variant_translation: langContext,
context: {
product_translation: langContext,
categories: {
category_translation: langContext,
},
variants: {
variant_translation: langContext,
},
},
},
})
entitiesMap
)
expect(format).toEqual({
product: {

View File

@@ -0,0 +1,239 @@
import {
JoinerServiceConfig,
JoinerServiceConfigAlias,
ModuleJoinerConfig,
} from "@medusajs/types"
import { isObject, isString } from "@medusajs/utils"
import { MedusaModule } from "../medusa-module"
const joinerConfigMapCache = new Map()
/**
* Parse and assign filters to remote query object to the corresponding relation level
* @param entryPoint
* @param filters
* @param remoteQueryObject
* @param isFieldAliasNestedRelation
* @param entitiesMap
*/
export function parseAndAssignFilters(
{
entryPoint,
filters,
remoteQueryObject,
isFieldAliasNestedRelation,
}: {
remoteQueryObject: object
entryPoint: string
filters: object
isFieldAliasNestedRelation?: boolean
},
entitiesMap: Map<string, any>
) {
const joinerConfigs = MedusaModule.getAllJoinerConfigs()
for (const [filterKey, filterValue] of Object.entries(filters)) {
let entryAlias!: JoinerServiceConfigAlias
let entryJoinerConfig!: JoinerServiceConfig
const { joinerConfig, alias } = retrieveJoinerConfigFromPropertyName({
entryPoint: entryPoint,
joinerConfigs,
})
entryAlias = alias
entryJoinerConfig = joinerConfig
const entryEntity = entitiesMap[entryAlias.entity!]
if (!entryEntity) {
throw new Error(
`Entity ${entryAlias.entity} not found in the public schema of the joiner config from ${entryJoinerConfig.serviceName}`
)
}
if (isObject(filterValue)) {
for (const [nestedFilterKey, nestedFilterValue] of Object.entries(
filterValue
)) {
const { joinerConfig: filterKeyJoinerConfig } =
retrieveJoinerConfigFromPropertyName({
entryPoint: nestedFilterKey,
joinerConfigs,
})
if (
!filterKeyJoinerConfig ||
filterKeyJoinerConfig.serviceName === entryJoinerConfig.serviceName
) {
assignNestedRemoteQueryObject({
entryPoint,
filterKey,
nestedFilterKey,
filterValue,
nestedFilterValue,
remoteQueryObject,
isFieldAliasNestedRelation,
})
} else {
const isFieldAliasNestedRelation_ = isFieldAliasNestedRelationHelper({
nestedFilterKey,
entryJoinerConfig,
joinerConfigs,
filterKeyJoinerConfig,
})
parseAndAssignFilters(
{
entryPoint: nestedFilterKey,
filters: nestedFilterValue,
remoteQueryObject: remoteQueryObject[entryPoint][filterKey],
isFieldAliasNestedRelation: isFieldAliasNestedRelation_,
},
entitiesMap
)
}
}
continue
}
assignRemoteQueryObject({
entryPoint,
filterKey,
filterValue,
remoteQueryObject,
isFieldAliasNestedRelation,
})
}
}
function retrieveJoinerConfigFromPropertyName({ entryPoint, joinerConfigs }) {
if (joinerConfigMapCache.has(entryPoint)) {
return joinerConfigMapCache.get(entryPoint)!
}
for (const joinerConfig of joinerConfigs) {
const aliases = joinerConfig.alias
const entryPointAlias = aliases.find((alias) => {
const aliasNames = Array.isArray(alias.name) ? alias.name : [alias.name]
return aliasNames.includes(entryPoint)
})
if (entryPointAlias) {
joinerConfigMapCache.set(entryPoint, {
joinerConfig,
alias: entryPointAlias,
})
return { joinerConfig, alias: entryPointAlias }
}
}
return {}
}
function assignRemoteQueryObject({
entryPoint,
filterKey,
filterValue,
remoteQueryObject,
isFieldAliasNestedRelation,
}: {
entryPoint: string
filterKey: string
filterValue: any
remoteQueryObject: object
isFieldAliasNestedRelation?: boolean
}) {
remoteQueryObject[entryPoint] ??= {}
remoteQueryObject[entryPoint].__args ??= {}
remoteQueryObject[entryPoint].__args["filters"] ??= {}
if (!isFieldAliasNestedRelation) {
remoteQueryObject[entryPoint].__args["filters"][filterKey] = filterValue
} else {
// In case of field alias that refers to a relation of linked entity we need to assign the filter on the relation filter itself instead of top level of the args\
remoteQueryObject[entryPoint].__args["filters"][entryPoint] ??= {}
remoteQueryObject[entryPoint].__args["filters"][entryPoint][filterKey] =
filterValue
}
}
function assignNestedRemoteQueryObject({
entryPoint,
filterKey,
nestedFilterKey,
nestedFilterValue,
remoteQueryObject,
isFieldAliasNestedRelation,
}: {
entryPoint: string
filterKey: string
filterValue: any
nestedFilterKey: string
nestedFilterValue: any
remoteQueryObject: object
isFieldAliasNestedRelation?: boolean
}) {
remoteQueryObject[entryPoint] ??= {}
remoteQueryObject[entryPoint]["__args"] ??= {}
remoteQueryObject[entryPoint]["__args"]["filters"] ??= {}
if (!isFieldAliasNestedRelation) {
remoteQueryObject[entryPoint]["__args"]["filters"][filterKey] ??= {}
remoteQueryObject[entryPoint]["__args"]["filters"][filterKey][
nestedFilterKey
] = nestedFilterValue
} else {
// In case of field alias that refers to a relation of linked entity we need to assign the filter on the relation filter itself instead of top level of the args
remoteQueryObject[entryPoint]["__args"]["filters"][entryPoint] ??= {}
remoteQueryObject[entryPoint]["__args"]["filters"][entryPoint][
filterKey
] ??= {}
remoteQueryObject[entryPoint]["__args"]["filters"][entryPoint][filterKey][
nestedFilterKey
] = nestedFilterValue
}
}
function isFieldAliasNestedRelationHelper({
nestedFilterKey,
entryJoinerConfig,
joinerConfigs,
filterKeyJoinerConfig,
}: {
nestedFilterKey: string
entryJoinerConfig: ModuleJoinerConfig
joinerConfigs: ModuleJoinerConfig[]
filterKeyJoinerConfig: ModuleJoinerConfig
}): boolean {
const linkJoinerConfig = joinerConfigs.find((joinerConfig) => {
return joinerConfig.relationships?.every(
(rel) =>
rel.serviceName === entryJoinerConfig.serviceName ||
rel.serviceName === filterKeyJoinerConfig.serviceName
)
})
const relationsAlias = linkJoinerConfig?.relationships?.map((r) => r.alias)
let isFieldAliasNestedRelation = false
if (linkJoinerConfig && relationsAlias?.length) {
const fieldAlias = linkJoinerConfig.extends?.find(
(extend) => extend.fieldAlias?.[nestedFilterKey]
)?.fieldAlias
if (fieldAlias) {
const path = isString(fieldAlias?.[nestedFilterKey])
? fieldAlias?.[nestedFilterKey]
: (fieldAlias?.[nestedFilterKey] as any).path
if (!relationsAlias.includes(path.split(".").pop())) {
isFieldAliasNestedRelation = true
}
}
}
return isFieldAliasNestedRelation
}

View File

@@ -9,8 +9,8 @@ import {
RemoteQueryObjectFromStringResult,
} from "@medusajs/types"
import {
isObject,
MedusaError,
isObject,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { RemoteQuery } from "./remote-query"
@@ -69,7 +69,10 @@ export class Query {
if ("__value" in config) {
normalizedQuery = config.__value
} else if ("entity" in normalizedQuery) {
normalizedQuery = toRemoteQuery(normalizedQuery)
normalizedQuery = toRemoteQuery(
normalizedQuery,
this.#remoteQuery.getEntitiesMap()
)
} else if (
"entryPoint" in normalizedQuery ||
"service" in normalizedQuery
@@ -141,7 +144,10 @@ export class Query {
queryOptions: RemoteQueryInput<TEntry>,
options?: RemoteJoinerOptions
): Promise<GraphResultSet<TEntry>> {
const normalizedQuery = toRemoteQuery<TEntry>(queryOptions)
const normalizedQuery = toRemoteQuery<TEntry>(
queryOptions,
this.#remoteQuery.getEntitiesMap()
)
let response:
| any[]
| { rows: any[]; metadata: RemoteQueryFunctionReturnPagination }

View File

@@ -21,6 +21,7 @@ export class RemoteQuery {
private remoteJoiner: RemoteJoiner
private modulesMap: Map<string, LoadedModule> = new Map()
private customRemoteFetchData?: RemoteFetchDataCallback
private entitiesMap: Map<string, any> = new Map()
static traceFetchRemoteData?: (
fetcher: () => Promise<any>,
@@ -33,12 +34,15 @@ export class RemoteQuery {
modulesLoaded,
customRemoteFetchData,
servicesConfig = [],
entitiesMap,
}: {
modulesLoaded?: LoadedModule[]
customRemoteFetchData?: RemoteFetchDataCallback
servicesConfig?: ModuleJoinerConfig[]
entitiesMap: Map<string, any>
}) {
const servicesConfig_ = [...servicesConfig]
this.entitiesMap = entitiesMap
if (!modulesLoaded?.length) {
modulesLoaded = MedusaModule.getLoadedModules().map(
@@ -72,6 +76,10 @@ export class RemoteQuery {
)
}
public getEntitiesMap() {
return this.entitiesMap
}
public setFetchDataCallback(
remoteFetchData: (
expand: RemoteExpandProperty,

View File

@@ -5,6 +5,7 @@ import {
RemoteQueryObjectConfig,
} from "@medusajs/types"
import { QueryContext, QueryFilter, isObject } from "@medusajs/utils"
import { parseAndAssignFilters } from "./parse-filters"
const FIELDS = "__fields"
const ARGUMENTS = "__args"
@@ -27,16 +28,19 @@ const ARGUMENTS = "__args"
* console.log(remoteQueryObject);
*/
export function toRemoteQuery<const TEntity extends string>(config: {
entity: TEntity | keyof RemoteQueryEntryPoints
fields: RemoteQueryObjectConfig<TEntity>["fields"]
filters?: RemoteQueryFilters<TEntity>
pagination?: {
skip?: number
take?: number
}
context?: Record<string, any>
}): RemoteQueryGraph<TEntity> {
export function toRemoteQuery<const TEntity extends string>(
config: {
entity: TEntity | keyof RemoteQueryEntryPoints
fields: RemoteQueryObjectConfig<TEntity>["fields"]
filters?: RemoteQueryFilters<TEntity>
pagination?: {
skip?: number
take?: number
}
context?: Record<string, any>
},
entitiesMap: Map<string, any>
): RemoteQueryGraph<TEntity> {
const { entity, fields = [], filters = {}, context = {} } = config
const joinerQuery: Record<string, any> = {
@@ -84,7 +88,6 @@ export function toRemoteQuery<const TEntity extends string>(config: {
}
// Process filters and context recursively
processNestedObjects(joinerQuery[entity], filters)
processNestedObjects(joinerQuery[entity], context)
for (const field of fields) {
@@ -116,5 +119,14 @@ export function toRemoteQuery<const TEntity extends string>(config: {
}
}
parseAndAssignFilters(
{
entryPoint: entity,
filters: filters,
remoteQueryObject: joinerQuery,
},
entitiesMap
)
return joinerQuery as RemoteQueryGraph<TEntity>
}

View File

@@ -46,6 +46,7 @@ export const ProductVariantPriceSet: ModuleJoinerConfig = {
prices: {
path: "price_set_link.price_set.prices",
isList: true,
forwardArgumentsOnPath: ["price_set_link.price_set"],
},
calculated_price: {
path: "price_set_link.price_set.calculated_price",

View File

@@ -1,11 +1,11 @@
export const schema = `
type PriceSet {
id: ID!
prices: [MoneyAmount]
prices: [Price]
calculated_price: CalculatedPriceSet
}
type MoneyAmount {
type Price {
id: ID!
currency_code: String
amount: Float