feat(modules-sdk): Parse filters based on loaded modules graph (#9158)
This commit is contained in:
committed by
GitHub
parent
812b80b6a3
commit
c6795dfc47
@@ -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,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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: {
|
||||
|
||||
239
packages/core/modules-sdk/src/remote-query/parse-filters.ts
Normal file
239
packages/core/modules-sdk/src/remote-query/parse-filters.ts
Normal 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
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user