3.4.3. Query

In this chapter, you’ll learn about Query and how to use it to fetch data from modules.

What is Query?#

Query fetches data across modules. It’s a set of methods registered in the Medusa container under the query key.

In all resources that can access the Medusa Container, such as API routes or workflows, you can resolve Query to fetch data across custom modules and Medusa’s Commerce Modules.


Query Example#

For example, create the route src/api/query/route.ts with the following content:

src/api/query/route.ts
7} from "@medusajs/framework/utils"8
9export const GET = async (10  req: MedusaRequest,11  res: MedusaResponse12) => {13  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)14
15  const { data: posts } = await query.graph({16    entity: "post",17    fields: ["id", "title"],18  })19
20  res.json({ posts })21}

In the above example, you resolve Query from the Medusa container using the ContainerRegistrationKeys.QUERY (query) key.

Then, you run a query using its graph method. This method accepts as a parameter an object with the following required properties:

  • entity: The data model's name, as specified in the first parameter of the model.define method used for the data model's definition.
  • fields: An array of the data model’s properties to retrieve in the result.

The method returns an object that has a data property, which holds an array of the retrieved data. For example:

Returned Data
1{2  "data": [3    {4      "id": "123",5      "title": "My Post"6    }7  ]8}

Query Usage in Workflows#

To retrieve data with Query in a workflow, use the useQueryGraphStep.

For example:

src/workflows/query.ts
1import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"2import { useQueryGraphStep } from "@medusajs/medusa/core-flows"3
4const myWorkflow = createWorkflow(5  "my-workflow",6  () => {7    const { data: posts } = useQueryGraphStep({8      entity: "post",9      fields: ["id", "title"],10    })11
12    return new WorkflowResponse({13      posts,14    })15  }16)

You can learn more about this step in the useQueryGraphStep reference.


Querying the Graph#

When you use the query.graph method, you're running a query through an internal graph that the Medusa application creates.

This graph collects data models of all modules in your application, including commerce and custom modules, and identifies relations and links between them.


Retrieve Linked Records#

Retrieve the records of a linked data model by passing in fields the data model's name suffixed with .*.

For example:

.* means that all of the data model's properties should be retrieved. You can also retrieve specific properties by replacing the * with the property name for each property.

For example:

In the example above, you retrieve only the id and title properties of the product linked to a post.

If the linked data model has isList enabled in the link definition, pass in fields the data model's plural name suffixed with .*.

For example:

In the example above, you retrieve all products linked to a post.

Apply Filters and Pagination on Linked Records#

Consider that you want to apply filters or pagination configurations on the product(s) linked to a post. To do that, you must query the module link's table instead.

As mentioned in the Module Link documentation, Medusa creates a table for your module link. So, not only can you retrieve linked records, but you can also retrieve the records in a module link's table.

A module link's definition, exported by a file under src/links, has a special entryPoint property. Use this property when specifying the entity property in Query's graph method.

For example:

Code
1import ProductPostLink from "../../../links/product-post"2
3// ...4
5const { data: productCustoms } = await query.graph({6  entity: ProductPostLink.entryPoint,7  fields: ["*", "product.*", "post.*"],8  pagination: {9    take: 5,10    skip: 0,11  },12})

In the object passed to the graph method:

  • You pass the entryPoint property of the link definition as the value for entity. So, Query will retrieve records from the module link's table.
  • You pass three items to the fields property:
    • * to retrieve the link table's fields. This is useful if the link table has custom columns.
    • product.* to retrieve the fields of a product record linked to a Post record.
    • post.* to retrieve the fields of a Post record linked to a product record.

You can then apply any filters or pagination configurations on the module link's table. For example, you can apply filters on the product_id, post_id, and any other custom columns you defined in the link table.

The returned data is similar to the following:

Example Result
1[{2  "id": "123",3  "product_id": "prod_123",4  "post_id": "123",5  "product": {6    "id": "prod_123",7    // other product fields...8  },9  "post": {10    "id": "123",11    // other post fields...12  }13}]

Apply Filters#

The query.graph function accepts a filters property. You can use this property to filter retrieved records.

In the example above, you filter the post records by the ID post_123.

You can also filter by multiple values of a property. For example:

In the example above, you filter the post records by multiple IDs.

Note: Filters don't apply on fields of linked data models from other modules. Refer to the Retrieve Linked Records section for an alternative solution.

Advanced Query Filters#

Under the hood, Query uses one of the following methods from the data model's module's service to retrieve records:

  • listX if you don't pass pagination parameters. For example, listPosts.
  • listAndCountX if you pass pagination parameters. For example, listAndCountPosts.

Both methods accept a filter object that can be used to filter records.

Those filters don't just allow you to filter by exact values. You can also filter by properties that don't match a value, match multiple values, and other filter types.

Refer to the Service Factory Reference for examples of advanced filters. The following sections provide some quick examples.

Filter by Not Matching a Value

In the example above, only posts that have a title are retrieved.

Filter by Not Matching Multiple Values

In the example above, only posts that don't have the title My Post or Another Post are retrieved.

Filter by a Range

In the example above, only posts that were published today are retrieved.

Filter Text by Like Value

Note: This filter only applies to text-like properties, including text, id, and enum properties.

In the example above, only posts that have the word My in their title are retrieved.

Filter a Relation's Property

While it's not possible to filter by a linked data model's property, you can filter by a relation's property (that is, the property of a related data model that is defined in the same module).

In the example above, only posts that have an author with the name John are retrieved.

Filter by Relation Property Not Matching Value

To filter by a relationship property whose value doesn't match a specific condition, use an $or operator that applies the following conditions:

  1. The relationship's property is not set. This is necessary to exclude posts that don't have an author.
  2. The relationship's property is not equal to the specific value.

So, in the example above, the query retrieves posts that either don't have an author or have an author whose name is not "John".


Apply Pagination#

The graph method's object parameter accepts a pagination property to configure the pagination of retrieved records.

To paginate the returned records, pass the following properties to pagination:

  • skip: (required to apply pagination) The number of records to skip before fetching the results.
  • take: The number of records to fetch.

When you provide the pagination fields, the query.graph method's returned object has a metadata property. Its value is an object having the following properties:

skipnumber
The number of records skipped.
takenumber
The number of records requested to fetch.
countnumber
The total number of records.

Sort Records#

Note: Sorting doesn't work on fields of linked data models from other modules.

To sort returned records, pass an order property to pagination.

The order property is an object whose keys are property names, and values are either:

  • ASC to sort records by that property in ascending order.
  • DESC to sort records by that property in descending order.

Retrieve Deleted Records#

By default, Query doesn't retrieve deleted records. To retrieve all records including deleted records, you can pass the withDeleted property to the query.graph method.

Note: The withDeleted property is available from Medusa v2.8.5.

For example:

In the example above, you retrieve all posts, including deleted ones.

Retrieve Only Deleted Records#

To retrieve only deleted records, you can add a deleted_at filter and set its value to not null. For example:

In the example above, you retrieve only deleted posts by enabling the withDeleted property and adding a filter to only retrieve records where the deleted_at property is not null.


Configure Query to Throw Error#

By default, if Query doesn't find records matching your query, it returns an empty array. You can configure Query to throw an error when no records are found.

The query.graph method accepts as a second parameter an object that can have a throwIfKeyNotFound property. Its value is a boolean indicating whether to throw an error if no record is found when filtering by IDs. By default, it's false.

For example:

In the example above, if no post is found with the ID post_123, Query throws an error. This is useful to stop execution when a record is expected to exist.

The throwIfKeyNotFound option can also be used to throw an error if the ID of a related data model's record (in the same module) is passed in the filters, and the related record doesn't exist.

For example:

In the example above, Query throws an error either if no post is found with the ID post_123 or if it's found but its author ID isn't author_123.

In the above example, it's assumed that a post belongs to an author, so it has an author_id property. However, this also works in the opposite case, where an author has many posts.

For example:

In the example above, Query throws an error if no author is found with the ID author_123 or if the author is found but doesn't have a post with the ID post_123.


Cache Query Results#

Note: Caching options are available from Medusa v2.11.0.

You can cache Query results to improve performance and reduce database load. To do that, you can pass a cache property in the second parameter of the query.graph method.

For example, to enable caching for a query:

In this example, you enable caching of the query's results. The next time the same query is executed, the results are returned from the cache instead of querying the database.

Tip: Refer to the Caching Module documentation for best practices on caching.

Cache Properties#

cache is an object that accepts the following properties:

enableboolean | ((args: any[]) => boolean | undefined)
Whether to enable caching of query results. If a function is passed, it receives as a parameter the query.graph parameters, and returns a boolean indicating whether caching is enabled.

Default: false

keystring | ((args: any[], cachingModule: ICachingModuleService) => string | Promise<string>)
The key to cache the query results with. If no key is provided, the Caching Module will generate the key from the query.graph parameters. If a function is passed, it receives the following properties:
  1. The parameters passed to query.graph.
  2. The Caching Module's service, which you can use to perform caching operations.
The function must return a string indicating the cache key.
tagsstring[] | ((args: any[]) => string[] | undefined)
The tags to associate with the cached results. Tags are useful to group related items. If no tag is provided, the Caching Module will generate relevant tags for the entity and its retrieved relations. If a function is passed, it receives as a parameter the query.index parameters, and returns an array of strings indicating the cache tags.
ttlnumber | ((args: any[]) => number | undefined)
The time-to-live (TTL) for the cached results, in seconds. If no TTL is provided, the Caching Module Provider will receive the configured TTL of the Caching Module, or it will use its own default value. If a function is passed, it receives as a parameter the query.graph parameters, and returns a number indicating the TTL.
autoInvalidateboolean | ((args: any[]) => boolean | undefined)
Whether to automatically invalidate the cached data when it expires. If a function is passed, it receives as a parameter the query.graph parameters, and returns a boolean indicating whether to automatically invalidate the cache.

Default: `true`

providersstring[] | ((args: any[]) => string[] | undefined)
The IDs of the providers to use for caching. If not provided, the default Caching Module Provider is used. If multiple providers are passed, the cache is stored and retrieved in those providers in order. If a function is passed, it receives as a parameter the query.graph parameters, and return an array of strings indicating the providers to use.

Set Cache Key#

By default, the Caching Module generates a cache key for a query based on the arguments passed to query.graph. The cache key is a unique key that the cached result is stored with.

Alternatively, you can set a custom cache key for a query. This is useful if you want to manage invalidating the cache manually.

To set the cache key of a query, pass the cache.key option:

In the example above, you cache the query results with the products-123456 key.

Note: You should generate cache keys with the Caching Module service's computeKey method to ensure that the key is unique and follows best practices.

You can also pass a function as the value of cache.key:

Note: Passing a function to cache.key is only supported in query.graph, not in useQueryGraphStep. This is due to variable-related restrictions in workflows, as explained in the Data Manipulation in Workflows guide. You can alternatively create a step that uses Query directly, and use it in the workflow.
Code
1const { data: products } = await query.graph({2  entity: "product",3  fields: ["id", "title"],4}, {5  cache: {6    enable: true,7    key: async (args, cachingModuleService) => {8      return await cachingModuleService.computeKey({9        ...args,10        prefix: "products"11      })12    }13  }14})

In the example above, you pass a function to key. It accepts two parameters:

  1. The arguments of query.graph passed as an array.
  2. The Caching Module's service.

You generate the key using the computeKey method of the Caching Module's service. The query results will be cached with that key.

Set Cache Tags#

By default, the Caching Module generates relevant tags for a query based on the entity and its retrieved relations. Cache tags are useful to group related items together, allowing you to retrieve or invalidate items by common tags.

Alternatively, you can set the cache tags of a query manually. This is useful if you want to manage invalidating the cache manually, or you want to group related cached items with custom tags.

To set the cache tags of a query, pass the cache.tags option:

In the example above, you cache the query results with the Product:list:* tag.

Note: The cache tag must follow the Caching Tags Convention to be automatically invalidated.

You can also pass a function as the value of cache.tags:

Note: Passing a function to cache.tags is only supported in query.graph, not in useQueryGraphStep. This is due to variable-related restrictions in workflows, as explained in the Data Manipulation in Workflows guide. You can alternatively create a step that uses Query directly, and use it in the workflow.
Code
1const { data: products } = await query.graph({2  entity: "product",3  fields: ["id", "title"],4}, {5  cache: {6    enable: true,7    tags: (args) => {8      const collectionId = args[0].filter?.collection_id9      return [10        ...args,11        collectionId ? `ProductCollection:${collectionId}` : undefined,12      ]13    },14  }15})

In the example above, you use a function to determine the cache tags. The function accepts the arguments passed to query.graph as an array.

Then, you add the ProductCollection:id tag if collection_id is passed in the query filters.

Set TTL#

By default, the Caching Module will pass the configured time-to-live (TTL) to the Caching Module Provider when caching data. The Caching Module Provider may also have its own default TTL. The cache isn't invalidated until the configured TTL passes.

Alternatively, you can set a custom TTL for a query. This is useful if you want the cached data to be invalidated sooner or later than the default TTL.

To set the TTL of the cached query results to a custom value, use the cache.ttl option:

In the example above, you set the TTL of the cached query result to 100 seconds. It will be invalidated after that time.

You can also pass a function as the value of cache.ttl:

Note: Passing a function to cache.ttl is only supported in query.graph, not in useQueryGraphStep. This is due to variable-related restrictions in workflows, as explained in the Data Manipulation in Workflows guide. You can alternatively create a step that uses Query directly, and use it in the workflow.
Code
1const { data: products } = await query.graph({2  entity: "product",3  fields: ["id", "title"],4  filters: {5    id: "prod_123"6  }7}, {8  cache: {9    enable: true,10    ttl: (args) => {11      return args[0].filters.id === "test" ? 10 : 10012    }13  }14})

In the example above, you use a function to determine the TTL. The function accepts the arguments passed to query.graph as an array.

Then, you set the TTL based on the ID of the product passed in the filters.

Set Auto Invalidation#

By default, the Caching Module automatically invalidates cached query results when the data changes.

Alternatively, you can disable auto invalidation of cached query results. This is useful if you want to manage invalidating the cache manually.

To configure invalidation behavior, use the cache.autoInvalidate option:

In this example, you disable auto invalidation of the query result. You must invalidate the cached data manually.

You can also pass a function as the value of cache.autoInvalidate:

Note: Passing a function to cache.autoInvalidate is only supported in query.graph, not in useQueryGraphStep. This is due to variable-related restrictions in workflows, as explained in the Data Manipulation in Workflows guide. You can alternatively create a step that uses Query directly, and use it in the workflow.
Code
1const { data: products } = await query.graph({2  entity: "product",3  fields: ["id", "title"],4}, {5  cache: {6    enable: true,7    autoInvalidate: (args) => {8      return !args[0].fields.includes("custom_field")9    }10  }11})

In the example above, you use a function to determine whether to invalidate the cached query result automatically. The function accepts the arguments passed to query.graph as an array.

Then, you enable auto-invalidation only if the fields passed to query.graph don't include custom_fields. If this disables auto-invalidation, you must invalidate the cached data manually.

Tip: Learn more about automatic invalidation in the Caching Module documentation.

Set Caching Provider#

By default, the Caching Module uses the default Caching Module Provider to cache a query.

Alternatively, you can set the caching provider to use for a query. This is useful if you have multiple caching providers configured, and you want to use a specific one for a query, or you want to specify a fallback provider.

To configure the caching providers, use the cache.providers option:

In the example above, you specify the providers with ID caching-redis and caching-memcached to cache the query results. These IDs must match the IDs of the providers in medusa-config.ts.

When you pass multiple providers, the cache is stored and retrieved in those providers in order.

You can also pass a function as the value of cache.providers:

Note: Passing a function to cache.providers is only supported in query.graph, not in useQueryGraphStep. This is due to variable-related restrictions in workflows, as explained in the Data Manipulation in Workflows guide. You can alternatively create a step that uses Query directly, and use it in the workflow.
Code
1const { data: products } = await query.graph({2  entity: "product",3  fields: ["id", "title"],4  filters: {5    id: "prod_123"6  }7}, {8  cache: {9    enable: true,10    providers: (args) => {11      return args[0].filters.id === "test" ? ["caching-redis"] : ["caching-memcached"]12    }13  }14})

In the example above, you use a function to determine the caching providers. The function accepts the arguments passed to query.graph as an array.

Then, you set the providers based on the ID of the product passed in the filters.


Request Query Configurations#

For API routes that retrieve a single or list of resources, Medusa provides a validateAndTransformQuery middleware that:

  • Validates accepted query parameters, as explained in this documentation.
  • Parses configurations that are received as query parameters to be passed to Query.

Using this middleware allows you to have default configurations for retrieved fields and relations or pagination, while allowing clients to customize them per request.

Step 1: Add Middleware#

The first step is to use the validateAndTransformQuery middleware on the GET route. You add the middleware in src/api/middlewares.ts:

src/api/middlewares.ts
1import { 2  validateAndTransformQuery,3  defineMiddlewares,4} from "@medusajs/framework/http"5import { createFindParams } from "@medusajs/medusa/api/utils/validators"6
7export const GetCustomSchema = createFindParams()8
9export default defineMiddlewares({10  routes: [11    {12      matcher: "/customs",13      method: "GET",14      middlewares: [15        validateAndTransformQuery(16          GetCustomSchema,17          {18            defaults: [19              "id",20              "title",21              "products.*",22            ],23            isList: true,24          }25        ),26      ],27    },28  ],29})

The validateAndTransformQuery accepts two parameters:

  1. A Zod validation schema for the query parameters, which you can learn more about in the API Route Validation documentation. Medusa has a createFindParams utility that generates a Zod schema that accepts four query parameters:
    1. fields: The fields and relations to retrieve in the returned resources.
    2. offset: The number of items to skip before retrieving the returned items.
    3. limit: The maximum number of items to return.
    4. order: The fields to order the returned items by in ascending or descending order.
  2. A Query configuration object. It accepts the following properties:
    1. defaults: An array of default fields and relations to retrieve in each resource.
    2. isList: A boolean indicating whether a list of items is returned in the response.
    3. allowed: An array of fields and relations allowed to be passed in the fields query parameter.
    4. defaultLimit: A number indicating the default limit to use if no limit is provided. By default, it's 50.

Step 2: Use Configurations in API Route#

After applying this middleware, your API route now accepts the fields, offset, limit, and order query parameters mentioned above.

The middleware transforms these parameters to configurations that you can pass to Query in your API route handler. These configurations are stored in the queryConfig parameter of the MedusaRequest object.

Note: As of Medusa v2.2.0, remoteQueryConfig has been deprecated in favor of queryConfig. Their usage is still the same, only the property name has changed.

For example, create the file src/api/customs/route.ts with the following content:

src/api/customs/route.ts
1import {2  MedusaRequest,3  MedusaResponse,4} from "@medusajs/framework/http"5import {6  ContainerRegistrationKeys,7} from "@medusajs/framework/utils"8
9export const GET = async (10  req: MedusaRequest,11  res: MedusaResponse12) => {13  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)14
15  const { data: posts } = await query.graph({16    entity: "post",17    ...req.queryConfig,18  })19
20  res.json({ posts: posts })21}

This adds a GET API route at /customs, which is the API route you added the middleware for.

In the API route, you pass req.queryConfig to query.graph. queryConfig has properties like fields and pagination to configure the query based on the default values you specified in the middleware, and the query parameters passed in the request.

Test it Out#

To test it out, start your Medusa application and send a GET request to the /customs API route. A list of records is retrieved with the specified fields in the middleware.

Returned Data
1{2  "posts": [3    {4      "id": "123",5      "title": "test"6    }7  ]8}

Try passing one of the Query configuration parameters, like fields or limit, and you'll see its impact on the returned result.

Note: Learn more about specifying fields and relations and pagination in the API reference.
Was this chapter helpful?
Ask Anything
FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break