diff --git a/integration-tests/modules/__tests__/modules/remote-query.spec.ts b/integration-tests/modules/__tests__/modules/remote-query.spec.ts index e7fd4565df..b9cfd67e00 100644 --- a/integration-tests/modules/__tests__/modules/remote-query.spec.ts +++ b/integration-tests/modules/__tests__/modules/remote-query.spec.ts @@ -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, + }), + ], + }), + ], + }), + ]) + }) + }) }, }) diff --git a/packages/core/modules-sdk/src/medusa-app.ts b/packages/core/modules-sdk/src/medusa-app.ts index 4a43943070..eec9fe032c 100644 --- a/packages/core/modules-sdk/src/medusa-app.ts +++ b/packages/core/modules-sdk/src/medusa-app.ts @@ -413,10 +413,12 @@ async function MedusaApp_({ const loadedSchema = getLoadedSchema() const { schema, notFound } = cleanAndMergeSchema(loadedSchema) + const entitiesMap = schema.getTypeMap() as unknown as Map 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, diff --git a/packages/core/modules-sdk/src/remote-query/__fixtures__/get-entities-map.ts b/packages/core/modules-sdk/src/remote-query/__fixtures__/get-entities-map.ts new file mode 100644 index 0000000000..5a8ee322dd --- /dev/null +++ b/packages/core/modules-sdk/src/remote-query/__fixtures__/get-entities-map.ts @@ -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 { + const defaultMedusaSchema = ` + scalar DateTime + scalar JSON + ` + const { schema } = cleanGraphQLSchema(defaultMedusaSchema + loadedSchema) + const mergedSchema = mergeTypeDefs(schema) + return makeExecutableSchema({ typeDefs: mergedSchema }).getTypeMap() as any +} diff --git a/packages/core/modules-sdk/src/remote-query/__fixtures__/parse-filters.ts b/packages/core/modules-sdk/src/remote-query/__fixtures__/parse-filters.ts new file mode 100644 index 0000000000..fdda72f545 --- /dev/null +++ b/packages/core/modules-sdk/src/remote-query/__fixtures__/parse-filters.ts @@ -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 +) diff --git a/packages/core/modules-sdk/src/remote-query/__tests__/parse-filters.spec.ts b/packages/core/modules-sdk/src/remote-query/__tests__/parse-filters.spec.ts new file mode 100644 index 0000000000..ee5df36d11 --- /dev/null +++ b/packages/core/modules-sdk/src/remote-query/__tests__/parse-filters.spec.ts @@ -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"], + }, + }, + }, + }, + }) + }) + }) +}) diff --git a/packages/core/modules-sdk/src/remote-query/__tests__/to-remote-query.ts b/packages/core/modules-sdk/src/remote-query/__tests__/to-remote-query.ts index 5f0ccd30e1..fe001eadbb 100644 --- a/packages/core/modules-sdk/src/remote-query/__tests__/to-remote-query.ts +++ b/packages/core/modules-sdk/src/remote-query/__tests__/to-remote-query.ts @@ -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: { diff --git a/packages/core/modules-sdk/src/remote-query/parse-filters.ts b/packages/core/modules-sdk/src/remote-query/parse-filters.ts new file mode 100644 index 0000000000..74d047fc35 --- /dev/null +++ b/packages/core/modules-sdk/src/remote-query/parse-filters.ts @@ -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 +) { + 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 +} diff --git a/packages/core/modules-sdk/src/remote-query/query.ts b/packages/core/modules-sdk/src/remote-query/query.ts index 925643ebfd..2d9d783e6e 100644 --- a/packages/core/modules-sdk/src/remote-query/query.ts +++ b/packages/core/modules-sdk/src/remote-query/query.ts @@ -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, options?: RemoteJoinerOptions ): Promise> { - const normalizedQuery = toRemoteQuery(queryOptions) + const normalizedQuery = toRemoteQuery( + queryOptions, + this.#remoteQuery.getEntitiesMap() + ) let response: | any[] | { rows: any[]; metadata: RemoteQueryFunctionReturnPagination } diff --git a/packages/core/modules-sdk/src/remote-query/remote-query.ts b/packages/core/modules-sdk/src/remote-query/remote-query.ts index 7043b5a070..909931fa0c 100644 --- a/packages/core/modules-sdk/src/remote-query/remote-query.ts +++ b/packages/core/modules-sdk/src/remote-query/remote-query.ts @@ -21,6 +21,7 @@ export class RemoteQuery { private remoteJoiner: RemoteJoiner private modulesMap: Map = new Map() private customRemoteFetchData?: RemoteFetchDataCallback + private entitiesMap: Map = new Map() static traceFetchRemoteData?: ( fetcher: () => Promise, @@ -33,12 +34,15 @@ export class RemoteQuery { modulesLoaded, customRemoteFetchData, servicesConfig = [], + entitiesMap, }: { modulesLoaded?: LoadedModule[] customRemoteFetchData?: RemoteFetchDataCallback servicesConfig?: ModuleJoinerConfig[] + entitiesMap: Map }) { 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, diff --git a/packages/core/modules-sdk/src/remote-query/to-remote-query.ts b/packages/core/modules-sdk/src/remote-query/to-remote-query.ts index 2bf79b0a6e..958069f636 100644 --- a/packages/core/modules-sdk/src/remote-query/to-remote-query.ts +++ b/packages/core/modules-sdk/src/remote-query/to-remote-query.ts @@ -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(config: { - entity: TEntity | keyof RemoteQueryEntryPoints - fields: RemoteQueryObjectConfig["fields"] - filters?: RemoteQueryFilters - pagination?: { - skip?: number - take?: number - } - context?: Record -}): RemoteQueryGraph { +export function toRemoteQuery( + config: { + entity: TEntity | keyof RemoteQueryEntryPoints + fields: RemoteQueryObjectConfig["fields"] + filters?: RemoteQueryFilters + pagination?: { + skip?: number + take?: number + } + context?: Record + }, + entitiesMap: Map +): RemoteQueryGraph { const { entity, fields = [], filters = {}, context = {} } = config const joinerQuery: Record = { @@ -84,7 +88,6 @@ export function toRemoteQuery(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(config: { } } + parseAndAssignFilters( + { + entryPoint: entity, + filters: filters, + remoteQueryObject: joinerQuery, + }, + entitiesMap + ) + return joinerQuery as RemoteQueryGraph } diff --git a/packages/modules/link-modules/src/definitions/product-variant-price-set.ts b/packages/modules/link-modules/src/definitions/product-variant-price-set.ts index 490827b4ff..d2ee84799d 100644 --- a/packages/modules/link-modules/src/definitions/product-variant-price-set.ts +++ b/packages/modules/link-modules/src/definitions/product-variant-price-set.ts @@ -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", diff --git a/packages/modules/pricing/src/schema/index.ts b/packages/modules/pricing/src/schema/index.ts index 3c63f22d7c..8abfe7f6b0 100644 --- a/packages/modules/pricing/src/schema/index.ts +++ b/packages/modules/pricing/src/schema/index.ts @@ -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