From 94376f6c751a47b3893fa6ac7128e9cf327e32e0 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Mon, 3 Feb 2025 19:43:40 +0200 Subject: [PATCH] docs: document Query Context (#11289) * docs: document Query Context * add vale exception --- .../module-links/query-context/page.mdx | 211 ++++++++++++++++++ www/apps/book/generated/edit-dates.mjs | 3 +- www/apps/book/generated/sidebar.mjs | 9 + www/apps/book/sidebar.mjs | 5 + www/vale/styles/docs/ModuleNames.yml | 3 +- 5 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 www/apps/book/app/learn/fundamentals/module-links/query-context/page.mdx diff --git a/www/apps/book/app/learn/fundamentals/module-links/query-context/page.mdx b/www/apps/book/app/learn/fundamentals/module-links/query-context/page.mdx new file mode 100644 index 0000000000..654973f794 --- /dev/null +++ b/www/apps/book/app/learn/fundamentals/module-links/query-context/page.mdx @@ -0,0 +1,211 @@ +export const metadata = { + title: `${pageNumber} Query Context`, +} + +# {metadata.title} + +In this chapter, you'll learn how to pass contexts when retrieving data with [Query](../query/page.mdx). + +## What is Query Context? + +Query context is a way to pass additional information when retrieving data with Query. This data can be useful when applying custom transformations to the retrieved data based on the current context. + +For example, consider you have a Blog Module with posts and authors. You can accept the user's language as a context and return the posts in the user's language. Another example is how Medusa uses Query Context to [retrieve product variants' prices based on the customer's currency](!resources!/commerce-modules/product/guides/price). + +--- + +## How to Use Query Context + +The `query.graph` method accepts an optional `context` parameter that can be used to pass additional context either to the data model you're retrieving (for example, `post`), or its related and linked models (for example, `author`). + +You initialize a context using `QueryContext` from the Modules SDK. It accepts an object of contexts as an argument. + +For example, to retrieve posts using Query while passing the user's language as a context: + +export const highlights1 = [ + ["4", "context", "Pass additional context to the query."], + ["4", "QueryContext", "Create a query context."] +] + +```ts +const { data } = await query.graph({ + entity: "post", + fields: ["*"], + context: QueryContext({ + lang: "es", + }) +}) +``` + +In this example, you pass in the context a `lang` property whose value is `es`. + +Then, to handle the context while retrieving records of the data model, in the associated module's service you override the generated `list` method of the data model. + +For example, continuing the example above, you can override the `listPosts` method of the Blog Module's service to handle the context: + +export const highlights2 = [ + ["11", "listPosts", "Override the generated listPosts method."], + ["16", "context", "The context is passed as part of `filters`."], + ["19", "super", "Use the parent's `listPosts` method to retrieve the posts."], + ["21", "", "If the language is set in the context, you transform the titles of the posts."] +] + +```ts highlights={highlights2} +import { MedusaContext, MedusaService } from "@medusajs/framework/utils" +import { Context, FindConfig } from "@medusajs/framework/types" +import Post from "./models/post" +import Author from "./models/author" + +class BlogModuleService extends MedusaService({ + Post, + Author +}){ + // @ts-ignore + async listPosts( + filters?: any, + config?: FindConfig | undefined, + @MedusaContext() sharedContext?: Context | undefined + ) { + const context = filters.context ?? {} + delete filters.context + + let posts = await super.listPosts(filters, config, sharedContext) + + if (context.lang === "es") { + posts = posts.map((post) => { + return { + ...post, + title: post.title + " en español", + } + }) + } + + return posts + } +} + +export default BlogModuleService +``` + +In the above example, you override the generated `listPosts` method. This method receives as a first parameter the filters passed to the query, but it also includes a `context` property that holds the context passed to the query. + +You extract the context from `filters`, then retrieve the posts using the parent's `listPosts` method. After that, if the language is set in the context, you transform the titles of the posts. + +All posts returned will now have their titles appended with "en español". + + + +Learn more about the generated `list` method in [this reference](!resources!/service-factory-reference/methods/list). + + + +--- + +## Passing Query Context to Related Data Models + +If you're retrieving a data model and you want to pass context to its associated model in the same module, you can pass them as part of `QueryContext`'s parameter, then handle them in the same `list` method. + + + +For linked data models, check out the [next section](#passing-query-context-to-linked-data-models). + + + +For example, to pass a context for the post's authors: + +export const highlights3 = [ + ["6", "author", "Pass a context for the author."] +] + +```ts highlights={highlights3} +const { data } = await query.graph({ + entity: "post", + fields: ["*"], + context: QueryContext({ + lang: "es", + author: QueryContext({ + lang: "es", + }) + }) +}) +``` + +Then, in the `listPosts` method, you can handle the context for the post's authors: + +export const highlights4 = [ + ["22", "context.author?.lang", "Access the author's context."] +] + +```ts highlights={highlights4} +import { MedusaContext, MedusaService } from "@medusajs/framework/utils" +import { Context, FindConfig } from "@medusajs/framework/types" +import Post from "./models/post" +import Author from "./models/author" + +class BlogModuleService extends MedusaService({ + Post, + Author +}){ + // @ts-ignore + async listPosts( + filters?: any, + config?: FindConfig | undefined, + @MedusaContext() sharedContext?: Context | undefined + ) { + const context = filters.context ?? {} + delete filters.context + + let posts = await super.listPosts(filters, config, sharedContext) + + const isPostLangEs = context.lang === "es" + const isAuthorLangEs = context.author?.lang === "es" + + if (isPostLangEs || isAuthorLangEs) { + posts = posts.map((post) => { + return { + ...post, + title: isPostLangEs ? post.title + " en español" : post.title, + author: { + ...post.author, + name: isAuthorLangEs ? post.author.name + " en español" : post.author.name, + } + } + }) + } + + return posts + } +} + +export default BlogModuleService +``` + +The context in `filters` will also have the context for `author`, which you can use to make transformations to the post's authors. + +--- + +## Passing Query Context to Linked Data Models + +If you're retrieving a data model and you want to pass context to a linked model in a different module, pass to the `context` property an object instead, where its keys are the linked model's name and the values are the context for that linked model. + +For example, consider the Product Module's `Product` data model is linked to the Blog Module's `Post` data model. You can pass context to the `Post` data model while retrieving products like so: + +export const highlights5 = [ + ["5", "post", "Pass a context for posts."] +] + +```ts highlights={highlights5} +const { data } = await query.graph({ + entity: "product", + fields: ["*", "post.*"], + context: { + post: QueryContext({ + lang: "es", + }) + } +}) +``` + +In this example, you retrieve products and their associated posts. You also pass a context for `post`, indicating the customer's language. + +To handle the context, you override the generated `listPosts` method of the Blog Module as explained [previously](#how-to-use-query-context). diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index fccc038bb3..8b56744cd1 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -114,5 +114,6 @@ export const generatedEditDates = { "app/learn/fundamentals/plugins/create/page.mdx": "2025-01-31T13:17:48.052Z", "app/learn/fundamentals/plugins/page.mdx": "2025-01-22T10:14:10.433Z", "app/learn/customization/reuse-customizations/page.mdx": "2025-01-22T10:01:57.665Z", - "app/learn/update/page.mdx": "2025-01-27T08:45:19.030Z" + "app/learn/update/page.mdx": "2025-01-27T08:45:19.030Z", + "app/learn/fundamentals/module-links/query-context/page.mdx": "2025-02-03T17:04:24.479Z" } \ No newline at end of file diff --git a/www/apps/book/generated/sidebar.mjs b/www/apps/book/generated/sidebar.mjs index 00056e7ca7..c7be8c2f32 100644 --- a/www/apps/book/generated/sidebar.mjs +++ b/www/apps/book/generated/sidebar.mjs @@ -368,6 +368,15 @@ export const generatedSidebar = [ "title": "Custom Columns", "children": [], "chapterTitle": "3.3.4. Custom Columns" + }, + { + "loaded": true, + "isPathHref": true, + "type": "link", + "path": "/learn/fundamentals/module-links/query-context", + "title": "Query Context", + "children": [], + "chapterTitle": "3.3.5. Query Context" } ], "chapterTitle": "3.3. Module Links" diff --git a/www/apps/book/sidebar.mjs b/www/apps/book/sidebar.mjs index bd175e8ea5..6343ef8f81 100644 --- a/www/apps/book/sidebar.mjs +++ b/www/apps/book/sidebar.mjs @@ -214,6 +214,11 @@ export const sidebar = sidebarAttachHrefCommonOptions([ path: "/learn/fundamentals/module-links/custom-columns", title: "Custom Columns", }, + { + type: "link", + path: "/learn/fundamentals/module-links/query-context", + title: "Query Context", + }, ], }, { diff --git a/www/vale/styles/docs/ModuleNames.yml b/www/vale/styles/docs/ModuleNames.yml index 750a25d20c..ce18bd459f 100644 --- a/www/vale/styles/docs/ModuleNames.yml +++ b/www/vale/styles/docs/ModuleNames.yml @@ -17,4 +17,5 @@ exceptions: - the first module - the second module - the module provider - - the specified module \ No newline at end of file + - the specified module + - the associated module \ No newline at end of file