docs: added troubleshooting guides + improvements (#11927)

* docs: added troubleshooting guides + improvements

* build fixes
This commit is contained in:
Shahed Nasser
2025-03-21 11:47:03 +02:00
committed by GitHub
parent c4f75ecbb2
commit 4c33586946
35 changed files with 17258 additions and 15864 deletions
@@ -11,6 +11,8 @@ You can customize the admin dashboard by:
- Adding new sections to existing pages using Widgets.
- Adding new pages using UI Routes.
However, you can't customize the admin dashboard's layout, design, or the content of the existing pages (aside from injecting widgets).
---
## Medusa UI Package
@@ -42,12 +42,12 @@ export const GET = async (
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { data: myCustoms } = await query.graph({
entity: "my_custom",
fields: ["id", "name"],
const { data: posts } = await query.graph({
entity: "post",
fields: ["id", "title"],
})
res.json({ my_customs: myCustoms })
res.json({ posts })
}
```
@@ -65,7 +65,7 @@ The method returns an object that has a `data` property, which holds an array of
"data": [
{
"id": "123",
"name": "test"
"title": "My Post"
}
]
}
@@ -88,21 +88,33 @@ Retrieve the records of a linked data model by passing in `fields` the data mode
For example:
```ts highlights={[["6"]]}
const { data: myCustoms } = await query.graph({
entity: "my_custom",
const { data: posts } = await query.graph({
entity: "post",
fields: [
"id",
"name",
"title",
"product.*",
],
})
```
<Note title="Tip">
`.*` means that all of data model's properties should be retrieved. You can also retrieve specific properties by replacing the `*` with the property name, for each property.
`.*` means that all of data model's properties should be retrieved. To retrieve a specific property, replace the `*` with the property's name. For example, `product.title`.
For example:
</Note>
```ts
const { data: posts } = await query.graph({
entity: "post",
fields: [
"id",
"title",
"product.id",
"product.title",
],
})
```
In the example above, you retrieve only the `id` and `title` properties of the `product` linked to a `post`.
### Retrieve List Link Records
@@ -111,19 +123,21 @@ If the linked data model has `isList` enabled in the link definition, pass in `f
For example:
```ts highlights={[["6"]]}
const { data: myCustoms } = await query.graph({
entity: "my_custom",
const { data: posts } = await query.graph({
entity: "post",
fields: [
"id",
"name",
"title",
"products.*",
],
})
```
In the example above, you retrieve all products linked to a post.
### Apply Filters and Pagination on Linked Records
Consider you want to apply filters or pagination configurations on the product(s) linked to `my_custom`. To do that, you must query the module link's table instead.
Consider you want to apply filters or pagination configurations on the product(s) linked to `post`. To do that, you must query the module link's table instead.
As mentioned in the [Module Link](../page.mdx) 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.
@@ -133,19 +147,19 @@ For example:
export const queryLinkTableHighlights = [
["1", "", "Import the module link."],
["6", "productBrandLink.entryPoint", "Pass the `entryPoint` property of the link to Query"],
["7", `"product.*"`, "Retrieve the fields of a product record linked to a `MyCustom` record."],
["7", `"brand.*"`, "Retrieve the fields of a `MyCustom` record linked to a product record."]
["6", "ProductPostLink.entryPoint", "Pass the `entryPoint` property of the link to Query"],
["7", `"product.*"`, "Retrieve the fields of a product record linked to a `Post` record."],
["7", `"post.*"`, "Retrieve the fields of a `Post` record linked to a product record."]
]
```ts highlights={queryLinkTableHighlights}
import productCustomLink from "../../../links/product-custom"
import ProductPostLink from "../../../links/product-post"
// ...
const { data: productCustoms } = await query.graph({
entity: productCustomLink.entryPoint,
fields: ["*", "product.*", "my_custom.*"],
entity: ProductPostLink.entryPoint,
fields: ["*", "product.*", "post.*"],
pagination: {
take: 5,
skip: 0,
@@ -158,8 +172,8 @@ 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 `field` property:
- `*` to retrieve the link table's fields. This is useful if the link table has [custom columns](../custom-columns/page.mdx).
- `product.*` to retrieve the fields of a product record linked to a `MyCustom` record.
- `my_custom.*` to retrieve the fields of a `MyCustom` record linked to a product record.
- `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](#apply-filters) or [pagination configurations](#apply-pagination).
@@ -169,58 +183,173 @@ The returned `data` is similar to the following:
[{
"id": "123",
"product_id": "prod_123",
"my_custom_id": "123",
"post_id": "123",
"product": {
"id": "prod_123",
// other product fields...
},
"my_custom": {
"post": {
"id": "123",
// other my_custom fields...
// other post fields...
}
}]
```
---
## Apply Filters
```ts highlights={[["6"], ["7"], ["8"], ["9"]]}
const { data: myCustoms } = await query.graph({
entity: "my_custom",
fields: ["id", "name"],
```ts highlights={[["4"], ["5"], ["6"]]}
const { data: posts } = await query.graph({
entity: "post",
fields: ["id", "title"],
filters: {
id: [
"mc_01HWSVWR4D2XVPQ06DQ8X9K7AX",
"mc_01HWSVWK3KYHKQEE6QGS2JC3FX",
],
id: "post_123",
},
})
```
The `query.graph` function accepts a `filters` property. You can use this property to filter retrieved records.
In the example above, you filter the `my_custom` records by multiple IDs.
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:
```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"], ["9"]]}
const { data: posts } = await query.graph({
entity: "post",
fields: ["id", "title"],
filters: {
id: [
"post_123",
"post_321",
],
},
})
```
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.
Filters don't apply on fields of linked data models from other modules. Refer to the [Retrieve Linked Records](#retrieve-linked-records) section for an alternative solution.
</Note>
### Advanced Query Filters
Under the hood, Query uses the `listX` (`listPosts`) method of the data model's module's service to retrieve records. This method accepts 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](!resources!/service-factory-reference/tips/filtering) for examples of advanced filters. The following sections provide some quick examples.
#### Filter by Not Matching a Value
```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
const { data: posts } = await query.graph({
entity: "post",
fields: ["id", "title"],
filters: {
title: {
$ne: null,
},
},
})
```
In the example above, only posts that have a title are retrieved.
#### Filter by Not Matching Multiple Values
```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
const { data: posts } = await query.graph({
entity: "post",
fields: ["id", "title"],
filters: {
title: {
$nin: ["My Post", "Another Post"],
},
},
})
```
In the example above, only posts that don't have the title `My Post` or `Another Post` are retrieved.
#### Filter by a Range
```ts highlights={[["10"], ["11"], ["12"], ["13"], ["14"], ["15"]]}
const startToday = new Date()
startToday.setHours(0, 0, 0, 0)
const endToday = new Date()
endToday.setHours(23, 59, 59, 59)
const { data: posts } = await query.graph({
entity: "post",
fields: ["id", "title"],
filters: {
published_at: {
$gt: startToday,
$lt: endToday,
},
},
})
```
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.
</Note>
```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
const { data: posts } = await query.graph({
entity: "post",
fields: ["id", "title"],
filters: {
title: {
$like: "%My%",
},
},
})
```
In the example above, only posts that have the word `My` in their title are retrieved.
#### Filter a Relation's Property
```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
const { data: posts } = await query.graph({
entity: "post",
fields: ["id", "title"],
filters: {
author: {
name: "John",
},
},
})
```
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.
---
## Apply Pagination
```ts highlights={[["8", "skip", "The number of records to skip before fetching the results."], ["9", "take", "The number of records to fetch."]]}
const {
data: myCustoms,
data: posts,
metadata: { count, take, skip } = {},
} = await query.graph({
entity: "my_custom",
fields: ["id", "name"],
entity: "post",
fields: ["id", "title"],
pagination: {
skip: 0,
take: 10,
@@ -258,9 +387,9 @@ When you provide the pagination fields, the `query.graph` method's returned obje
### Sort Records
```ts highlights={[["5"], ["6"], ["7"]]}
const { data: myCustoms } = await query.graph({
entity: "my_custom",
fields: ["id", "name"],
const { data: posts } = await query.graph({
entity: "post",
fields: ["id", "title"],
pagination: {
order: {
name: "DESC",
@@ -284,6 +413,72 @@ The `order` property is an object whose keys are property names, and values are
---
## Configure Query to Throw Errors
By default, if Query doesn't find records matching your query, it returns an empty array. You can add option to 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:
```ts
const { data: posts } = await query.graph({
entity: "post",
fields: ["id", "title"],
filters: {
id: "post_123",
},
}, {
throwIfKeyNotFound: true,
})
```
In the example above, if no post is found with the ID `post_123`, Query will throw an error. This is useful to stop execution when a record is expected to exist.
### Throw Error on Related Data Model
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:
```ts
const { data: posts } = await query.graph({
entity: "post",
fields: ["id", "title", "author.*"],
filters: {
id: "post_123",
author_id: "author_123",
},
}, {
throwIfKeyNotFound: true,
})
```
In the example above, Query throws an error either if no post is found with the ID `post_123` or if its 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:
```ts
const { data: posts } = await query.graph({
entity: "author",
fields: ["id", "name", "posts.*"],
filters: {
id: "author_123",
posts: {
id: "post_123",
},
},
}, {
throwIfKeyNotFound: true,
})
```
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`.
---
## Request Query Configurations
For API routes that retrieve a single or list of resources, Medusa provides a `validateAndTransformQuery` middleware that:
@@ -317,7 +512,7 @@ export default defineMiddlewares({
{
defaults: [
"id",
"name",
"title",
"products.*",
],
isList: true,
@@ -375,12 +570,12 @@ export const GET = async (
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { data: myCustoms } = await query.graph({
entity: "my_custom",
const { data: posts } = await query.graph({
entity: "post",
...req.queryConfig,
})
res.json({ my_customs: myCustoms })
res.json({ posts: posts })
}
```
@@ -394,10 +589,10 @@ To test it out, start your Medusa application and send a `GET` request to the `/
```json title="Returned Data"
{
"my_customs": [
"posts": [
{
"id": "123",
"name": "test"
"title": "test"
}
]
}
@@ -51,7 +51,7 @@ export default defineLink(
},
ProductModule.linkable.product,
{
readOnly: true
readOnly: true,
}
)
```
@@ -76,8 +76,8 @@ const { result } = await query.graph({
entity: "post",
fields: ["id", "product.*"],
filters: {
id: "post_123"
}
id: "post_123",
},
})
```
@@ -114,7 +114,7 @@ export default defineLink(
primaryKey: "product_id",
},
{
readOnly: true
readOnly: true,
}
)
```
@@ -133,8 +133,8 @@ const { result } = await query.graph({
entity: "product",
fields: ["id", "post.*"],
filters: {
id: "prod_123"
}
id: "prod_123",
},
})
```
@@ -194,7 +194,7 @@ export default defineLink(
},
ProductModule.linkable.product,
{
readOnly: true
readOnly: true,
}
)
```
@@ -229,7 +229,7 @@ export default defineLink(
},
ProductModule.linkable.product,
{
readOnly: true
readOnly: true,
}
)
```
@@ -276,7 +276,7 @@ export default defineLink(
primaryKey: "product_id",
},
{
readOnly: true
readOnly: true,
}
)
```
@@ -343,14 +343,14 @@ export default defineLink(
{
linkable: ProductModule.linkable.product,
field: "id",
isList: true
isList: true,
},
{
...BlogModule.linkable.post.id,
primaryKey: "product_id"
primaryKey: "product_id",
},
{
readOnly: true
readOnly: true,
}
)
```
@@ -444,14 +444,14 @@ import { CMS_MODULE } from "../modules/cms"
export default defineLink(
{
linkable: ProductModule.linkable.product,
field: "id"
field: "id",
},
{
linkable: {
serviceName: CMS_MODULE,
alias: "cms_post",
primaryKey: "product_id",
}
},
},
{
readOnly: true,
@@ -28,8 +28,8 @@ So, to run database queries in a service:
For example, in your service, add the following methods:
export const methodsHighlight = [
["12", "getCount", "Retrieves the number of records in `my_custom` using the `count` method."],
["19", "getCountSql", "Retrieves the number of records in `my_custom` using the `execute` method."]
["13", "getCount", "Retrieves the number of records in `my_custom` using the `count` method."],
["20", "getCountSql", "Retrieves the number of records in `my_custom` using the `execute` method."]
]
```ts highlights={methodsHighlight}
@@ -38,7 +38,8 @@ import {
InjectManager,
MedusaContext,
} from "@medusajs/framework/utils"
import { SqlEntityManager } from "@mikro-orm/knex"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
class BlogModuleService {
// ...
@@ -46,19 +47,19 @@ class BlogModuleService {
@InjectManager()
async getCount(
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<number> {
return await sharedContext.manager.count("my_custom")
): Promise<number | undefined> {
return await sharedContext?.manager?.count("my_custom")
}
@InjectManager()
async getCountSql(
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<number> {
const data = await sharedContext.manager.execute(
const data = await sharedContext?.manager?.execute(
"SELECT COUNT(*) as num FROM my_custom"
)
return parseInt(data[0].num)
return parseInt(data?.[0].num || 0)
}
}
```
@@ -115,8 +116,8 @@ class BlogModuleService {
},
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<any> {
const transactionManager = sharedContext.transactionManager
await transactionManager.nativeUpdate(
const transactionManager = sharedContext?.transactionManager
await transactionManager?.nativeUpdate(
"my_custom",
{
id: input.id,
@@ -127,7 +128,7 @@ class BlogModuleService {
)
// retrieve again
const updatedRecord = await transactionManager.execute(
const updatedRecord = await transactionManager?.execute(
`SELECT * FROM my_custom WHERE id = '${input.id}'`
)
@@ -178,10 +179,22 @@ For example, the `update` method could be changed to the following:
```ts
// other imports...
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
class BlogModuleService {
// ...
@InjectTransactionManager()
protected async update_(
// ...
): Promise<any> {
// ...
}
@InjectManager()
async update(
input: {
@@ -192,12 +205,14 @@ class BlogModuleService {
) {
const newData = await this.update_(input, sharedContext)
await sendNewDataToSystem(newData)
// example method that sends data to another system
await this.sendNewDataToSystem(newData)
return newData
}
}
```
In this case, only the `update_` method is wrapped in a transaction. The returned value `newData` holds the committed result, which can be used for other operations, such as passed to a `sendNewDataToSystem` method.
### Using Methods in Transactional Methods
@@ -208,6 +223,11 @@ For example:
```ts
// other imports...
import {
InjectTransactionManager,
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
class BlogModuleService {
@@ -343,7 +363,7 @@ class BlogModuleService {
return updatedRecord
},
{
transaction: sharedContext.transactionManager,
transaction: sharedContext?.transactionManager,
}
)
}
@@ -382,6 +402,12 @@ The second parameter of the `baseRepository_.transaction` method is an object of
```ts highlights={[["16"]]}
// other imports...
import { EntityManager } from "@mikro-orm/knex"
import {
InjectTransactionManager,
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
class BlogModuleService {
// ...
@@ -398,7 +424,7 @@ class BlogModuleService {
// ...
},
{
transaction: sharedContext.transactionManager,
transaction: sharedContext?.transactionManager,
}
)
}
@@ -414,6 +440,12 @@ class BlogModuleService {
```ts highlights={[["19"]]}
// other imports...
import {
InjectTransactionManager,
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { IsolationLevel } from "@mikro-orm/core"
class BlogModuleService {
@@ -442,6 +474,14 @@ class BlogModuleService {
- If `transaction` is provided and this is disabled, the manager in `transaction` is re-used.
```ts highlights={[["16"]]}
// other imports...
import {
InjectTransactionManager,
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
class BlogModuleService {
// ...
@InjectTransactionManager()