1015 lines
24 KiB
Markdown
1015 lines
24 KiB
Markdown
# Querying Data in Medusa
|
|
|
|
Medusa's Query API (`query.graph()`) is the primary way to retrieve data, especially across modules. It provides a flexible, performant way to query entities with relations and filters.
|
|
|
|
## Contents
|
|
- [When to Use Query vs Module Services](#when-to-use-query-vs-module-services)
|
|
- [Basic Query Structure](#basic-query-structure)
|
|
- [In Workflows vs Outside Workflows](#in-workflows-vs-outside-workflows)
|
|
- [Field Selection](#field-selection)
|
|
- [Filtering](#filtering)
|
|
- [Important Filtering Limitation](#important-filtering-limitation)
|
|
- [Pagination](#pagination)
|
|
- [Querying Linked Data](#querying-linked-data)
|
|
- [Option 1: query.graph() - Retrieve Linked Data Without Cross-Module Filters](#option-1-querygraph---retrieve-linked-data-without-cross-module-filters)
|
|
- [Option 2: query.index() - Filter Across Linked Modules (Index Module)](#option-2-queryindex---filter-across-linked-modules-index-module)
|
|
- [Validation with throwIfKeyNotFound](#validation-with-throwifkeynotfound)
|
|
- [Performance Best Practices](#performance-best-practices)
|
|
|
|
## When to Use Query vs Module Services
|
|
|
|
**⚠️ USE QUERY FOR**:
|
|
- ✅ Retrieving data **across modules** (products with linked brands, orders with customers)
|
|
- ✅ Reading data with linked entities
|
|
- ✅ Complex queries with multiple relations
|
|
- ✅ Storefront and admin data retrieval
|
|
|
|
**⚠️ USE MODULE SERVICES FOR**:
|
|
- ✅ Retrieving data **within a single module** (products with variants - same module)
|
|
- ✅ Using `listAndCount` for pagination within one module
|
|
- ✅ Mutations (always use module services or workflows)
|
|
|
|
**Examples:**
|
|
```typescript
|
|
// ✅ GOOD: Query for cross-module data
|
|
const { data } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title", "brand.*"], // brand is in different module
|
|
})
|
|
|
|
// ✅ GOOD: Module service for single module
|
|
const [products, count] = await productService.listAndCountProducts(
|
|
{ status: "active" },
|
|
{ take: 10, skip: 0 }
|
|
)
|
|
```
|
|
|
|
## Basic Query Structure
|
|
|
|
```typescript
|
|
const query = req.scope.resolve("query")
|
|
|
|
const { data } = await query.graph({
|
|
entity: "entity_name", // The entity to query
|
|
fields: ["id", "name"], // Fields to retrieve
|
|
filters: { status: "active" }, // Filter conditions
|
|
pagination: { // Optional pagination
|
|
take: 10,
|
|
skip: 0,
|
|
},
|
|
})
|
|
```
|
|
|
|
## In Workflows vs Outside Workflows
|
|
|
|
### Outside Workflows (API Routes, Subscribers, Scheduled Jobs)
|
|
|
|
```typescript
|
|
// In API routes
|
|
const query = req.scope.resolve("query")
|
|
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title"],
|
|
})
|
|
|
|
// In subscribers/scheduled jobs
|
|
const query = container.resolve("query")
|
|
|
|
const { data: customers } = await query.graph({
|
|
entity: "customer",
|
|
fields: ["id", "email"],
|
|
})
|
|
```
|
|
|
|
### In Workflows
|
|
|
|
Use `useQueryGraphStep` within workflow composition functions:
|
|
|
|
```typescript
|
|
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
|
|
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
|
|
|
|
const myWorkflow = createWorkflow(
|
|
"my-workflow",
|
|
function (input) {
|
|
const { data: products } = useQueryGraphStep({
|
|
entity: "product",
|
|
fields: ["id", "title"],
|
|
filters: {
|
|
id: input.product_id,
|
|
},
|
|
})
|
|
|
|
return new WorkflowResponse({ products })
|
|
}
|
|
)
|
|
```
|
|
|
|
## Field Selection
|
|
|
|
### Basic Fields
|
|
|
|
```typescript
|
|
const { data } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title", "description"],
|
|
})
|
|
```
|
|
|
|
### Nested Relations
|
|
|
|
Use dot notation to include related entities:
|
|
|
|
```typescript
|
|
const { data } = await query.graph({
|
|
entity: "product",
|
|
fields: [
|
|
"id",
|
|
"title",
|
|
"variants.*", // All fields from variants
|
|
"variants.sku", // Specific variant field
|
|
"category.id",
|
|
"category.name",
|
|
],
|
|
})
|
|
```
|
|
|
|
### Performance Tip
|
|
|
|
**⚠️ IMPORTANT**: Only retrieve fields and relations you'll actually use. Avoid using `*` to select all fields or retrieving all fields of a relation unnecessarily.
|
|
|
|
```typescript
|
|
// ❌ BAD: Retrieves all fields (inefficient)
|
|
fields: ["*"]
|
|
|
|
// ❌ BAD: Retrieves all product fields (might be many)
|
|
fields: ["product.*"]
|
|
|
|
// ✅ GOOD: Only retrieves needed fields
|
|
fields: ["id", "title", "product.id", "product.title"]
|
|
```
|
|
|
|
## Filtering
|
|
|
|
### Exact Match
|
|
|
|
```typescript
|
|
filters: {
|
|
email: "user@example.com"
|
|
}
|
|
```
|
|
|
|
### Multiple Values (IN operator)
|
|
|
|
```typescript
|
|
filters: {
|
|
id: ["id1", "id2", "id3"]
|
|
}
|
|
```
|
|
|
|
### Range Queries
|
|
|
|
```typescript
|
|
filters: {
|
|
created_at: {
|
|
$gte: startDate, // Greater than or equal
|
|
$lte: endDate, // Less than or equal
|
|
}
|
|
}
|
|
```
|
|
|
|
### Text Search (LIKE)
|
|
|
|
```typescript
|
|
filters: {
|
|
name: {
|
|
$like: "%search%" // Contains "search"
|
|
}
|
|
}
|
|
|
|
// Starts with
|
|
filters: {
|
|
name: {
|
|
$like: "search%"
|
|
}
|
|
}
|
|
|
|
// Ends with
|
|
filters: {
|
|
name: {
|
|
$like: "%search"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Not Equal
|
|
|
|
```typescript
|
|
filters: {
|
|
status: {
|
|
$ne: "deleted"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Multiple Conditions
|
|
|
|
```typescript
|
|
filters: {
|
|
status: "active",
|
|
created_at: {
|
|
$gte: new Date("2024-01-01"),
|
|
},
|
|
price: {
|
|
$gte: 10,
|
|
$lte: 100,
|
|
},
|
|
}
|
|
```
|
|
|
|
### Filtering Nested Relations (Same Module)
|
|
|
|
To filter by fields in nested relations **within the same module**, use object notation:
|
|
|
|
```typescript
|
|
// Product and ProductVariant are in the same module (Product Module)
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title", "variants.*"],
|
|
filters: {
|
|
variants: {
|
|
sku: "ABC1234" // ✅ Works: variants are in same module as product
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
## Important Filtering Limitation
|
|
|
|
**⚠️ CRITICAL**: With `query.graph()`, you **CANNOT** filter by fields from linked data models in different modules. The `query.graph()` method only supports filters on data models within the same module.
|
|
|
|
### What This Means
|
|
|
|
- **Same Module** (✅ Can filter with `query.graph()`): Product and ProductVariant, Order and LineItem, Cart and CartItem
|
|
- **Different Modules** (❌ Cannot filter with `query.graph()`): Product and Brand (custom), Product and Customer, Review and Product
|
|
- **Different Modules** (✅ Can filter with `query.index()`): Any linked modules when using the Index Module
|
|
|
|
### Example: Cannot Filter Products by Linked Brand with query.graph()
|
|
|
|
```typescript
|
|
// ❌ THIS DOES NOT WORK with query.graph()
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title", "brand.*"],
|
|
filters: {
|
|
"brand.name": "Nike" // ❌ Cannot filter by linked module field
|
|
}
|
|
})
|
|
|
|
// ❌ THIS ALSO DOES NOT WORK with query.graph()
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title", "brand.*"],
|
|
filters: {
|
|
brand: {
|
|
name: "Nike" // ❌ Still doesn't work - brand is in different module
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
### Solution 1: Use query.index() with Index Module (Recommended)
|
|
|
|
**✅ BEST APPROACH**: Use the Index Module to filter across linked modules efficiently at the database level:
|
|
|
|
```typescript
|
|
// ✅ CORRECT: Use query.index() to filter products by linked brand
|
|
const { data: products } = await query.index({
|
|
entity: "product",
|
|
fields: ["*", "brand.*"],
|
|
filters: {
|
|
brand: {
|
|
name: "Nike" // ✅ Works with Index Module!
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
**Why this is best:**
|
|
- Database-level filtering (most efficient)
|
|
- Supports pagination properly
|
|
- Only retrieves the data you need
|
|
- Designed specifically for cross-module filtering
|
|
|
|
**Requirements:**
|
|
- Index Module must be installed and configured
|
|
- Link must have `filterable` properties defined
|
|
- See [Querying Linked Data](#querying-linked-data) section for setup details
|
|
|
|
### Solution 2: Query from Other Side
|
|
|
|
**✅ GOOD ALTERNATIVE**: Query the linked module and filter on it directly using `query.graph()`:
|
|
|
|
```typescript
|
|
// ✅ CORRECT: Query brands and get their products
|
|
const { data: brands } = await query.graph({
|
|
entity: "brand",
|
|
fields: ["id", "name", "products.*"],
|
|
filters: {
|
|
name: "Nike" // ✅ Filter on brand directly
|
|
}
|
|
})
|
|
|
|
// Access Nike products
|
|
const nikeProducts = brands[0]?.products || []
|
|
```
|
|
|
|
**Use this when:**
|
|
- You don't have the Index Module set up
|
|
- The "other side" of the link makes sense as the primary entity
|
|
- You need a quick solution without additional setup
|
|
|
|
### Solution 3: Filter After Query (Least Efficient)
|
|
|
|
**⚠️ LAST RESORT**: Query all data with `query.graph()`, then filter in JavaScript:
|
|
|
|
```typescript
|
|
// Get all products with brands
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title", "brand.*"],
|
|
})
|
|
|
|
// Filter in JavaScript after query
|
|
const nikeProducts = products.filter(p => p.brand?.name === "Nike")
|
|
```
|
|
|
|
**Only use this when:**
|
|
- Dataset is very small (< 100 records)
|
|
- Index Module is not available
|
|
- Querying from the other side doesn't make sense
|
|
- You need a temporary solution
|
|
|
|
**Avoid because:**
|
|
- Fetches unnecessary data from database
|
|
- Inefficient for large datasets
|
|
- No pagination support at database level
|
|
- Uses more memory and network bandwidth
|
|
|
|
### More Examples
|
|
|
|
#### Example: Approved Reviews for a Specific Product
|
|
|
|
When you need to filter linked data by its own properties, you have multiple options:
|
|
|
|
```typescript
|
|
// ❌ WRONG: Cannot filter linked reviews from product query with query.graph()
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "reviews.*"],
|
|
filters: {
|
|
id: productId,
|
|
reviews: {
|
|
status: "approved" // ❌ Doesn't work - reviews is linked module
|
|
}
|
|
}
|
|
})
|
|
|
|
// ❌ ALSO WRONG: Filtering in JavaScript is inefficient
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "reviews.*"],
|
|
filters: { id: productId }
|
|
})
|
|
const approvedReviews = products[0].reviews.filter(r => r.status === "approved") // ❌ Client-side filter
|
|
|
|
// ✅ OPTION 1 (BEST): Use Index Module to filter cross-module
|
|
const { data: products } = await query.index({
|
|
entity: "product",
|
|
fields: ["*", "reviews.*"],
|
|
filters: {
|
|
id: productId,
|
|
reviews: {
|
|
status: "approved" // ✅ Works with Index Module!
|
|
}
|
|
}
|
|
})
|
|
|
|
// ✅ OPTION 2 (GOOD): Query reviews directly with filters
|
|
const { data: reviews } = await query.graph({
|
|
entity: "review",
|
|
fields: ["id", "rating", "comment", "product.*"],
|
|
filters: {
|
|
product_id: productId, // Filter by product
|
|
status: "approved" // Filter by review status - both in same query!
|
|
}
|
|
})
|
|
```
|
|
|
|
**Why Option 1 (Index Module) is best:**
|
|
- Database-level filtering across modules
|
|
- Returns data in the structure you expect (product with reviews)
|
|
- Supports pagination properly
|
|
- Only retrieves the data you need
|
|
|
|
**Why Option 2 (query from other side) is good:**
|
|
- No Index Module setup required
|
|
- Still uses database filtering
|
|
- Works well when the "other side" is the logical primary entity
|
|
|
|
#### Example: Reviews for Active Products (Cross-Module)
|
|
|
|
```typescript
|
|
// ❌ WRONG: Cannot filter by linked module with query.graph()
|
|
const { data } = await query.graph({
|
|
entity: "review",
|
|
fields: ["id", "rating", "product.*"],
|
|
filters: {
|
|
product: {
|
|
status: "active" // Doesn't work - product is linked module
|
|
}
|
|
}
|
|
})
|
|
|
|
// ✅ OPTION 1 (BEST): Use Index Module
|
|
const { data: reviews } = await query.index({
|
|
entity: "review",
|
|
fields: ["*", "product.*"],
|
|
filters: {
|
|
product: {
|
|
status: "active" // ✅ Works with Index Module!
|
|
}
|
|
}
|
|
})
|
|
|
|
// ✅ OPTION 2 (GOOD): Query from the other side
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title", "reviews.*"],
|
|
filters: { status: "active" }
|
|
})
|
|
|
|
// Flatten reviews if needed
|
|
const reviews = products.flatMap(p => p.reviews)
|
|
```
|
|
|
|
#### Example: Products with Variants (Same Module - Works!)
|
|
|
|
```typescript
|
|
// ✅ CORRECT: Product and variants are in same module (Product Module)
|
|
// Use query.graph() - no need for Index Module
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title", "variants.*"],
|
|
filters: {
|
|
variants: {
|
|
inventory_quantity: {
|
|
$gte: 10 // ✅ Works: both in Product Module
|
|
}
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
## Pagination
|
|
|
|
### Basic Pagination
|
|
|
|
```typescript
|
|
const { data, metadata } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title"],
|
|
pagination: {
|
|
skip: 0, // Offset
|
|
take: 10, // Limit
|
|
},
|
|
})
|
|
|
|
// metadata.count contains total count
|
|
console.log(`Total: ${metadata.count}`)
|
|
```
|
|
|
|
### With Ordering
|
|
|
|
```typescript
|
|
const { data } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title", "created_at"],
|
|
pagination: {
|
|
skip: 0,
|
|
take: 10,
|
|
order: {
|
|
created_at: "DESC", // Newest first
|
|
},
|
|
},
|
|
})
|
|
```
|
|
|
|
### Multiple Order Fields
|
|
|
|
```typescript
|
|
pagination: {
|
|
order: {
|
|
status: "ASC",
|
|
created_at: "DESC",
|
|
}
|
|
}
|
|
```
|
|
|
|
## Querying Linked Data
|
|
|
|
When entities are linked via [module links](module-links.md), you have two options depending on your filtering needs:
|
|
|
|
### Option 1: query.graph() - Retrieve Linked Data Without Cross-Module Filters
|
|
|
|
**Use `query.graph()` when:**
|
|
- ✅ Retrieving linked data without filtering by linked module properties
|
|
- ✅ Filtering only by properties in the primary entity's module
|
|
- ✅ You want to include related data in the response
|
|
|
|
**Limitations:**
|
|
- ❌ **CANNOT filter by properties of linked modules** (data models in separate modules)
|
|
- ✅ **CAN filter by properties of relations in the same module** (e.g., product.variants)
|
|
|
|
```typescript
|
|
// ✅ WORKS: Get products with their linked brands (no cross-module filtering)
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
fields: [
|
|
"id",
|
|
"title",
|
|
"brand.*", // All brand fields
|
|
],
|
|
filters: {
|
|
id: "prod_123", // ✅ Filter by product property (same module)
|
|
},
|
|
})
|
|
|
|
// Access linked data
|
|
console.log(products[0].brand.name)
|
|
|
|
// ✅ WORKS: Filter by same-module relation (product and variants are in Product Module)
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title", "variants.*"],
|
|
filters: {
|
|
variants: {
|
|
sku: "ABC1234" // ✅ Works: variants are in same module as product
|
|
}
|
|
}
|
|
})
|
|
|
|
// ❌ DOES NOT WORK: Cannot filter products by linked brand name
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title", "brand.*"],
|
|
filters: {
|
|
brand: {
|
|
name: "Nike" // ❌ Fails: brand is in a different module
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
**Reverse Query (From Link to Original):**
|
|
|
|
```typescript
|
|
// Get brands with their linked products
|
|
const { data: brands } = await query.graph({
|
|
entity: "brand",
|
|
fields: [
|
|
"id",
|
|
"name",
|
|
"products.*", // All linked products
|
|
],
|
|
})
|
|
|
|
// Access linked products
|
|
brands[0].products.forEach(product => {
|
|
console.log(product.title)
|
|
})
|
|
```
|
|
|
|
### Option 2: query.index() - Filter Across Linked Modules (Index Module)
|
|
|
|
**Use `query.index()` when:**
|
|
- ✅ You need to filter data by properties of linked modules (separate modules with module links)
|
|
- ✅ Filtering by custom data model properties linked to Commerce Module entities
|
|
- ✅ Complex cross-module queries requiring efficient database-level filtering
|
|
|
|
**Key Distinction:**
|
|
- **Same module relations** (e.g., Product → ProductVariant): Use `query.graph()` ✅
|
|
- **Different module links** (e.g., Product → Brand, Product → Review): Use `query.index()` ✅
|
|
|
|
#### When to Use query.index()
|
|
|
|
The Index Module solves the fundamental limitation of `query.graph()`: **you cannot filter one module's data by another module's linked properties** using `query.graph()`.
|
|
|
|
Examples of when you need `query.index()`:
|
|
- Filter products by brand name (Product Module → Brand Module)
|
|
- Filter products by review ratings (Product Module → Review Module)
|
|
- Filter customers by custom loyalty tier (Customer Module → Loyalty Module)
|
|
- Any scenario where you need to filter by properties of a linked data model in a different module
|
|
|
|
#### Setup Requirements
|
|
|
|
Before using `query.index()`, ensure the Index Module is configured:
|
|
|
|
1. **Install the Index Module:**
|
|
```bash
|
|
npm install @medusajs/index
|
|
```
|
|
|
|
2. **Add to `medusa-config.ts`:**
|
|
```typescript
|
|
module.exports = defineConfig({
|
|
modules: [
|
|
{
|
|
resolve: "@medusajs/index",
|
|
},
|
|
],
|
|
})
|
|
```
|
|
|
|
3. **Enable the feature flag in `.env`:**
|
|
```bash
|
|
MEDUSA_FF_INDEX_ENGINE=true
|
|
```
|
|
|
|
4. **Run migrations:**
|
|
```bash
|
|
npx medusa db:migrate
|
|
```
|
|
|
|
5. **Mark linked properties as filterable** in your link definition:
|
|
```typescript
|
|
// src/links/product-brand.ts
|
|
defineLink(
|
|
{ linkable: ProductModule.linkable.product, isList: true },
|
|
{ linkable: BrandModule.linkable.brand, filterable: ["id", "name"] }
|
|
)
|
|
```
|
|
|
|
The `filterable` property marks which fields can be queried across modules.
|
|
|
|
6. **Start the application** to trigger data ingestion into the Index Module.
|
|
|
|
#### Using query.index()
|
|
|
|
```typescript
|
|
const query = req.scope.resolve("query")
|
|
|
|
// ✅ CORRECT: Filter products by linked brand name using Index Module
|
|
const { data: products } = await query.index({
|
|
entity: "product",
|
|
fields: ["*", "brand.*"],
|
|
filters: {
|
|
brand: {
|
|
name: "Nike", // ✅ Works with Index Module!
|
|
},
|
|
},
|
|
})
|
|
|
|
// ✅ CORRECT: Filter products by review ratings
|
|
const { data: products } = await query.index({
|
|
entity: "product",
|
|
fields: ["id", "title", "reviews.*"],
|
|
filters: {
|
|
reviews: {
|
|
rating: {
|
|
$gte: 4, // Products with reviews rated 4 or higher
|
|
},
|
|
},
|
|
},
|
|
})
|
|
```
|
|
|
|
#### query.index() Features
|
|
|
|
**Pagination:**
|
|
```typescript
|
|
const { data: products } = await query.index({
|
|
entity: "product",
|
|
fields: ["*", "brand.*"],
|
|
filters: {
|
|
brand: { name: "Nike" },
|
|
},
|
|
pagination: {
|
|
take: 20,
|
|
skip: 0,
|
|
},
|
|
})
|
|
```
|
|
|
|
**Advanced Filters:**
|
|
```typescript
|
|
const { data: products } = await query.index({
|
|
entity: "product",
|
|
fields: ["*", "brand.*"],
|
|
filters: {
|
|
brand: {
|
|
name: {
|
|
$like: "%Acme%", // LIKE operator
|
|
},
|
|
},
|
|
status: {
|
|
$ne: "deleted", // Not equal
|
|
},
|
|
},
|
|
})
|
|
```
|
|
|
|
#### query.graph() vs query.index() Decision Tree
|
|
|
|
```
|
|
Need to filter by linked module properties?
|
|
├─ No → Use query.graph()
|
|
│ └─ Faster, simpler, works for most queries
|
|
│
|
|
└─ Yes → Are the entities in the same module or different modules?
|
|
├─ Same module (e.g., product.variants) → Use query.graph()
|
|
│ └─ Example: Product and ProductVariant both in Product Module
|
|
│
|
|
└─ Different modules (e.g., product → brand) → Use query.index()
|
|
└─ Example: Product (Product Module) → Brand (Custom Module)
|
|
└─ Requires Index Module setup and filterable properties
|
|
```
|
|
|
|
#### Important Notes
|
|
|
|
- **Performance:** The Index Module pre-ingests data on application startup, enabling efficient cross-module filtering
|
|
- **Data Freshness:** Data is synced automatically, but there may be a brief delay after mutations
|
|
- **Fallback:** If you don't need filtering, `query.graph()` is sufficient and more straightforward
|
|
- **Module Relations:** Always use `query.graph()` for same-module relations (product → variants, order → line items)
|
|
|
|
## Validation with throwIfKeyNotFound
|
|
|
|
Use `throwIfKeyNotFound` to validate that a record exists before performing operations:
|
|
|
|
```typescript
|
|
// Outside workflows
|
|
const query = req.scope.resolve("query")
|
|
|
|
const { data } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title"],
|
|
filters: {
|
|
id: productId,
|
|
},
|
|
}, {
|
|
throwIfKeyNotFound: true, // Throws if product doesn't exist
|
|
})
|
|
|
|
// If we get here, product exists
|
|
const product = data[0]
|
|
```
|
|
|
|
```typescript
|
|
// In workflows
|
|
const { data: products } = useQueryGraphStep({
|
|
entity: "product",
|
|
fields: ["id", "title"],
|
|
filters: {
|
|
id: input.product_id,
|
|
},
|
|
options: {
|
|
throwIfKeyNotFound: true, // Throws if product doesn't exist
|
|
},
|
|
})
|
|
```
|
|
|
|
**When to use:**
|
|
- ✅ Before updating or deleting a record
|
|
- ✅ When the record MUST exist for the operation to continue
|
|
- ✅ To avoid manual existence checks
|
|
|
|
```typescript
|
|
// ❌ BAD: Manual check
|
|
const { data } = await query.graph({ /* ... */ })
|
|
if (!data || data.length === 0) {
|
|
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Product not found")
|
|
}
|
|
|
|
// ✅ GOOD: Let query handle it
|
|
const { data } = await query.graph(
|
|
{ /* ... */ },
|
|
{ throwIfKeyNotFound: true }
|
|
)
|
|
```
|
|
|
|
## Performance Best Practices
|
|
|
|
### 1. Only Query What You Need
|
|
|
|
**⚠️ CRITICAL**: Always specify only the fields you'll use. Avoid using `*` or querying unnecessary relations.
|
|
|
|
```typescript
|
|
// ❌ BAD: Retrieves everything (slow, wasteful)
|
|
fields: ["*"]
|
|
|
|
// ✅ GOOD: Only needed fields (fast)
|
|
fields: ["id", "title", "price"]
|
|
```
|
|
|
|
### 2. Limit Relation Depth
|
|
|
|
There's no hard limit on relation depth, but deeper queries are slower. Only include relations you'll actually use.
|
|
|
|
```typescript
|
|
// ❌ BAD: Unnecessary depth
|
|
fields: [
|
|
"id",
|
|
"title",
|
|
"variants.*",
|
|
"variants.product.*", // Circular, unnecessary
|
|
"variants.prices.*",
|
|
"variants.prices.currency.*", // Probably don't need all currency fields
|
|
]
|
|
|
|
// ✅ GOOD: Appropriate depth
|
|
fields: [
|
|
"id",
|
|
"title",
|
|
"variants.id",
|
|
"variants.sku",
|
|
"variants.prices.amount",
|
|
"variants.prices.currency_code",
|
|
]
|
|
```
|
|
|
|
### 3. Use Pagination for Large Result Sets
|
|
|
|
```typescript
|
|
// ✅ GOOD: Paginated query
|
|
const { data, metadata } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title"],
|
|
pagination: {
|
|
take: 50, // Don't retrieve thousands of records at once
|
|
skip: 0,
|
|
},
|
|
})
|
|
```
|
|
|
|
### 4. Filter Early
|
|
|
|
Apply filters to reduce the data set before retrieving fields and relations:
|
|
|
|
```typescript
|
|
// ✅ GOOD: Filters reduce result set first
|
|
const { data } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title", "variants.*"],
|
|
filters: {
|
|
status: "published",
|
|
created_at: {
|
|
$gte: lastWeek,
|
|
},
|
|
},
|
|
})
|
|
```
|
|
|
|
### 5. Use Specific Queries for Different Use Cases
|
|
|
|
```typescript
|
|
// ✅ For listings (minimal fields)
|
|
const { data: listings } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title", "thumbnail", "price"],
|
|
})
|
|
|
|
// ✅ For detail pages (more fields)
|
|
const { data: details } = await query.graph({
|
|
entity: "product",
|
|
fields: [
|
|
"id",
|
|
"title",
|
|
"description",
|
|
"thumbnail",
|
|
"images.*",
|
|
"variants.*",
|
|
"variants.prices.*",
|
|
],
|
|
filters: { id: productId },
|
|
})
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### Pattern: List with Search
|
|
|
|
```typescript
|
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
const query = req.scope.resolve("query")
|
|
const { q } = req.validatedQuery
|
|
|
|
const filters: any = {}
|
|
if (q) {
|
|
filters.title = { $like: `%${q}%` }
|
|
}
|
|
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title", "thumbnail"],
|
|
filters,
|
|
...req.queryConfig, // Uses request query config
|
|
})
|
|
|
|
return res.json({ products })
|
|
}
|
|
```
|
|
|
|
### Pattern: Retrieve with Validation
|
|
|
|
```typescript
|
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
const query = req.scope.resolve("query")
|
|
const { id } = req.params
|
|
|
|
// Throws 404 if product doesn't exist
|
|
const { data } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title", "description", "variants.*"],
|
|
filters: { id },
|
|
}, {
|
|
throwIfKeyNotFound: true,
|
|
})
|
|
|
|
return res.json({ product: data[0] })
|
|
}
|
|
```
|
|
|
|
### Pattern: Query with Relations and Filters
|
|
|
|
```typescript
|
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
const query = req.scope.resolve("query")
|
|
const { category_id } = req.validatedQuery
|
|
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
fields: [
|
|
"id",
|
|
"title",
|
|
"thumbnail",
|
|
"variants.id",
|
|
"variants.prices.amount",
|
|
"category.name",
|
|
],
|
|
filters: {
|
|
category_id,
|
|
status: "published",
|
|
},
|
|
pagination: {
|
|
take: 20,
|
|
skip: 0,
|
|
},
|
|
})
|
|
|
|
return res.json({ products })
|
|
}
|
|
```
|
|
|
|
### Pattern: Count Records
|
|
|
|
```typescript
|
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
const query = req.scope.resolve("query")
|
|
|
|
const { data, metadata } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id"], // Minimal fields for counting
|
|
filters: {
|
|
status: "published",
|
|
},
|
|
})
|
|
|
|
return res.json({
|
|
count: metadata.count,
|
|
})
|
|
}
|
|
```
|
|
|
|
### Pattern: Recent Items
|
|
|
|
```typescript
|
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
|
const query = req.scope.resolve("query")
|
|
|
|
const { data: recentProducts } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title", "created_at"],
|
|
pagination: {
|
|
take: 10,
|
|
skip: 0,
|
|
order: {
|
|
created_at: "DESC", // Newest first
|
|
},
|
|
},
|
|
})
|
|
|
|
return res.json({ products: recentProducts })
|
|
}
|
|
```
|