docs: fixes to type errors in guides (#13797)

This commit is contained in:
Shahed Nasser
2025-10-21 16:12:35 +03:00
committed by GitHub
parent d9249be6e6
commit 0d83918348
19 changed files with 922 additions and 682 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -112,7 +112,7 @@ const { data: [order] } = await query.graph({
fields: ["*", "transactions.*"],
filters: {
id: "order_123",
}
},
})
const refundTransactions = order.transactions?.filter(

View File

@@ -197,7 +197,7 @@ export default class AgenticCommerceService {
// ...
async verifySignature({
signature,
payload
payload,
}: {
// base64 encoded signature
signature: string
@@ -205,18 +205,18 @@ export default class AgenticCommerceService {
}) {
try {
// Decode the base64 signature
const receivedSignature = Buffer.from(signature, 'base64')
const receivedSignature = Buffer.from(signature, "base64")
// Create HMAC-SHA256 signature using your signing key
const expectedSignature = crypto
.createHmac('sha256', this.options.signatureKey)
.update(JSON.stringify(payload), 'utf8')
.createHmac("sha256", this.options.signatureKey)
.update(JSON.stringify(payload), "utf8")
.digest()
// Compare signatures using constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(receivedSignature, expectedSignature)
} catch (error) {
console.error('Signature verification failed:', error)
console.error("Signature verification failed:", error)
return false
}
}
@@ -237,8 +237,8 @@ Add the following method to the `AgenticCommerceService` class:
export default class AgenticCommerceService {
// ...
async getSignature(data: any) {
return Buffer.from(crypto.createHmac('sha256', this.options.signatureKey)
.update(JSON.stringify(data), 'utf8').digest()).toString('base64')
return Buffer.from(crypto.createHmac("sha256", this.options.signatureKey)
.update(JSON.stringify(data), "utf8").digest()).toString("base64")
}
}
```
@@ -276,7 +276,7 @@ export default class AgenticCommerceService {
// ...
async sendWebhookEvent({
type,
data
data,
}: AgenticCommerceWebhookEvent) {
// Create signature
const signature = this.getSignature(data)
@@ -328,7 +328,7 @@ module.exports = defineConfig({
resolve: "./src/modules/agentic-commerce",
options: {
signatureKey: process.env.AGENTIC_COMMERCE_SIGNATURE_KEY || "supersecret",
}
},
},
],
})
@@ -487,7 +487,7 @@ export const getProductFeedItemsStep = createStep(
do {
const {
data: products,
metadata
metadata,
} = await query.graph({
entity: "product",
fields: [
@@ -503,7 +503,7 @@ export const getProductFeedItemsStep = createStep(
"sales_channels.*",
"sales_channels.stock_locations.*",
"sales_channels.stock_locations.address.*",
"categories.*"
"categories.*",
],
filters: {
status: "published",
@@ -513,19 +513,19 @@ export const getProductFeedItemsStep = createStep(
calculated_price: QueryContext({
currency_code: currencyCode,
}),
}
},
},
pagination: {
take: limit,
skip: offset,
}
},
})
count = metadata?.count ?? 0
offset += limit
for (const product of products) {
if (!product.variants.length) continue
if (!product.variants.length) {continue}
const salesChannel = product.sales_channels?.find((channel) => {
return channel?.stock_locations?.some((location) => {
return location?.address?.country_code.toLowerCase() === countryCode
@@ -752,11 +752,11 @@ export const sendProductFeedWorkflow = createWorkflow(
const { items: feedItems } = getProductFeedItemsStep(input)
const xml = buildProductFeedXmlStep({
items: feedItems
items: feedItems,
})
sendProductFeedStep({
productFeed: xml
productFeed: xml,
})
return new WorkflowResponse({ xml })
@@ -786,9 +786,9 @@ Create the file `src/jobs/sync-product-feed.ts` with the following content:
```ts title="src/jobs/sync-product-feed.ts"
import {
MedusaContainer
} from "@medusajs/framework/types";
import sendProductFeedWorkflow from "../workflows/send-product-feed";
MedusaContainer,
} from "@medusajs/framework/types"
import sendProductFeedWorkflow from "../workflows/send-product-feed"
export default async function syncProductFeed(container: MedusaContainer) {
const logger = container.resolve("logger")
@@ -816,7 +816,7 @@ export default async function syncProductFeed(container: MedusaContainer) {
export const config = {
name: "sync-product-feed",
schedule: "*/15 * * * *", // Every 15 minutes
};
}
```
In a scheduled job file, you must export:
@@ -890,11 +890,11 @@ To create the workflow, create the file `src/workflows/prepare-checkout-session-
import {
createWorkflow,
transform,
WorkflowResponse
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
listShippingOptionsForCartWithPricingWorkflow,
useQueryGraphStep
useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
export type PrepareCheckoutSessionDataWorkflowInput = {
@@ -960,21 +960,21 @@ const { data: carts } = useQueryGraphStep({
"original_item_total",
"shipping_total",
"metadata",
"order.id"
"order.id",
],
filters: {
id: input.cart_id,
},
options: {
throwIfKeyNotFound: true
}
throwIfKeyNotFound: true,
},
})
// Retrieve shipping options
const shippingOptions = listShippingOptionsForCartWithPricingWorkflow.runAsStep({
input: {
cart_id: carts[0].id,
}
},
})
// TODO prepare response
@@ -1022,7 +1022,7 @@ const responseData = transform({
item: {
id: item?.variant_id,
quantity: item?.quantity,
}
},
})),
fulfillment_address: data.input.fulfillment_address,
fulfillment_options: data.shippingOptions?.map((option) => ({
@@ -1080,7 +1080,7 @@ const responseData = transform({
display_name: "Total",
// @ts-ignore
amount: data.carts[0].total,
}
},
],
messages: data.input.messages || [],
links: [
@@ -1095,8 +1095,8 @@ const responseData = transform({
{
type: "seller_shop_policy",
value: "https://www.medusa-commerce.com/seller-shop-policy", // TODO: replace with actual seller shop policy
}
]
},
],
}
})
@@ -1215,7 +1215,7 @@ import {
createWorkflow,
transform,
when,
WorkflowResponse
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
addShippingMethodToCartWorkflow,
@@ -1223,10 +1223,10 @@ import {
CreateCartWorkflowInput,
createCustomersWorkflow,
listShippingOptionsForCartWithPricingWorkflow,
useQueryGraphStep
useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import {
prepareCheckoutSessionDataWorkflow
prepareCheckoutSessionDataWorkflow,
} from "./prepare-checkout-session-data"
type WorkflowInput = {
@@ -1270,7 +1270,7 @@ Replace the `TODO` in the workflow with the following:
```ts title="src/workflows/create-checkout-session.ts"
// validate item IDs
const variantIds = transform({
input
input,
}, (data) => {
return data.input.items.map((item) => item.id)
})
@@ -1280,11 +1280,11 @@ useQueryGraphStep({
entity: "variant",
fields: ["id"],
filters: {
id: variantIds
id: variantIds,
},
options: {
throwIfKeyNotFound: true
}
throwIfKeyNotFound: true,
},
})
// TODO retrieve region and sales channel
@@ -1305,9 +1305,9 @@ const { data: regions } = useQueryGraphStep({
fields: ["id"],
filters: {
countries: {
iso_2: "us"
}
}
iso_2: "us",
},
},
}).config({ name: "find-region" })
// get sales channel
@@ -1338,7 +1338,7 @@ const { data: customers } = useQueryGraphStep({
fields: ["id"],
filters: {
email: input.buyer?.email,
}
},
}).config({ name: "find-customer" })
// create customer if it does not exist
@@ -1354,9 +1354,9 @@ const createdCustomers = when ({ customers }, ({ customers }) =>
first_name: input.buyer?.first_name,
phone: input.buyer?.phone_number,
has_account: false,
}
]
}
},
],
},
})
})
@@ -1397,7 +1397,7 @@ const cartInput = transform({
return {
items: data.input.items.map((item) => ({
variant_id: item.id,
quantity: item.quantity
quantity: item.quantity,
})),
region_id: data.regions[0]?.id,
email: data.input.buyer?.email,
@@ -1416,12 +1416,12 @@ const cartInput = transform({
sales_channel_id: data.salesChannels[0]?.id,
metadata: {
is_checkout_session: true,
}
},
} as CreateCartWorkflowInput
})
const createdCart = createCartWorkflow.runAsStep({
input: cartInput
input: cartInput,
})
// TODO retrieve shipping options
@@ -1445,7 +1445,7 @@ when(input, (input) => !!input.fulfillment_address)
const shippingOptions = listShippingOptionsForCartWithPricingWorkflow.runAsStep({
input: {
cart_id: createdCart.id,
}
},
})
const shippingMethodData = transform({
@@ -1460,11 +1460,11 @@ when(input, (input) => !!input.fulfillment_address)
cart_id: data.createdCart.id,
options: [{
id: cheapestShippingOption.id,
}]
}],
}
})
addShippingMethodToCartWorkflow.runAsStep({
input: shippingMethodData
input: shippingMethodData,
})
})
@@ -1488,7 +1488,7 @@ const responseData = prepareCheckoutSessionDataWorkflow.runAsStep({
buyer: input.buyer,
fulfillment_address: input.fulfillment_address,
cart_id: createdCart.id,
}
},
})
return new WorkflowResponse(responseData)
@@ -1555,7 +1555,7 @@ export const POST = async (
input: req.validatedBody,
context: {
idempotencyKey: req.headers["idempotency-key"] as string,
}
},
})
res.set(responseHeaders).json(result)
@@ -1569,8 +1569,8 @@ export const POST = async (
code: "invalid",
content_type: "plain",
content: medusaError.message,
}
]
},
],
})
}
}
@@ -1593,8 +1593,8 @@ Next, you'll create a [middleware](!docs!/learn/fundamentals/api-routes/middlewa
Create the file `src/api/middlewares/validate-agentic-request.ts` with the following content:
```ts title="src/api/middlewares/validate-agentic-request.ts"
import { MedusaNextFunction, MedusaRequest, MedusaResponse } from "@medusajs/framework";
import { AGENTIC_COMMERCE_MODULE } from "../../modules/agentic-commerce";
import { MedusaNextFunction, MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { AGENTIC_COMMERCE_MODULE } from "../../modules/agentic-commerce"
export async function validateAgenticRequest(
req: MedusaRequest,
@@ -1609,12 +1609,12 @@ export async function validateAgenticRequest(
const isTokenValid = await apiKeyModuleService.authenticate(apiKey || "")
const isSignatureValid = !!req.body || await agenticCommerceModuleService.verifySignature({
signature,
payload: req.body
payload: req.body,
})
if (!isTokenValid || !isSignatureValid) {
return res.status(401).json({
message: "Unauthorized"
message: "Unauthorized",
})
}
@@ -1642,24 +1642,24 @@ You apply middlewares in the `src/api/middlewares.ts` file. Create this file wit
import {
defineMiddlewares,
validateAndTransformBody,
} from "@medusajs/framework/http";
import { validateAgenticRequest } from "./middlewares/validate-agentic-request";
import { PostCreateSessionSchema } from "./checkout_sessions/route";
} from "@medusajs/framework/http"
import { validateAgenticRequest } from "./middlewares/validate-agentic-request"
import { PostCreateSessionSchema } from "./checkout_sessions/route"
export default defineMiddlewares({
routes: [
{
matcher: "/checkout_sessions*",
middlewares: [
validateAgenticRequest
]
validateAgenticRequest,
],
},
{
matcher: "/checkout_sessions",
method: ["POST"],
middlewares: [validateAndTransformBody(PostCreateSessionSchema)]
middlewares: [validateAndTransformBody(PostCreateSessionSchema)],
},
]
],
})
```
@@ -1678,7 +1678,7 @@ In `src/api/middlewares.ts`, add the following import at the top of the file:
```ts title="src/api/middlewares.ts"
import {
errorHandler,
} from "@medusajs/framework/http";
} from "@medusajs/framework/http"
const originalErrorHandler = errorHandler()
```
@@ -1702,8 +1702,8 @@ export default defineMiddlewares({
code: "invalid",
content_type: "plain",
content: error.message,
}
]
},
],
})
},
})
@@ -1935,16 +1935,16 @@ import {
createWorkflow,
transform,
when,
WorkflowResponse
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
addShippingMethodToCartWorkflow,
createCustomersWorkflow,
updateCartWorkflow,
useQueryGraphStep
useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import {
prepareCheckoutSessionDataWorkflow
prepareCheckoutSessionDataWorkflow,
} from "./prepare-checkout-session-data"
type WorkflowInput = {
@@ -1980,7 +1980,7 @@ export const updateCheckoutSessionWorkflow = createWorkflow(
fields: ["id", "customer.*", "email"],
filters: {
id: input.cart_id,
}
},
})
// TODO retrieve or create customer
@@ -2003,7 +2003,7 @@ const { data: customers } = useQueryGraphStep({
fields: ["id"],
filters: {
email: input.buyer?.email,
}
},
}).config({ name: "find-customer" })
const createdCustomers = when({ customers }, ({ customers }) =>
@@ -2017,9 +2017,9 @@ const createdCustomers = when({ customers }, ({ customers }) =>
email: input.buyer?.email,
first_name: input.buyer?.first_name,
phone: input.buyer?.phone_number,
}
},
],
}
},
})
})
@@ -2057,7 +2057,7 @@ when(input, (input) => !!input.items)
},
options: {
throwIfKeyNotFound: true,
}
},
}).config({ name: "find-variant" })
})
@@ -2133,7 +2133,7 @@ const responseData = prepareCheckoutSessionDataWorkflow.runAsStep({
cart_id: updateData.id,
buyer: input.buyer,
fulfillment_address: input.fulfillment_address,
}
},
})
return new WorkflowResponse(responseData)
@@ -2199,7 +2199,7 @@ export const POST = async (
},
context: {
idempotencyKey: req.headers["idempotency-key"] as string,
}
},
})
res.set(responseHeaders).json(result)
@@ -2209,7 +2209,7 @@ export const POST = async (
await refreshPaymentCollectionForCartWorkflow(req.scope).run({
input: {
cart_id: req.params.id,
}
},
})
const { result } = await prepareCheckoutSessionDataWorkflow(req.scope)
@@ -2224,8 +2224,8 @@ export const POST = async (
"payment_declined" : "invalid",
content_type: "plain",
content: medusaError.message,
}
]
},
],
},
})
@@ -2249,7 +2249,7 @@ Finally, you'll apply the validation middleware to the `POST /checkout_sessions/
In `src/api/middlewares.ts`, add the following import at the top of the file:
```ts title="src/api/middlewares.ts"
import { PostUpdateSessionSchema } from "./checkout_sessions/[id]/route";
import { PostUpdateSessionSchema } from "./checkout_sessions/[id]/route"
```
Then, add a new route configuration in `defineMiddlewares`:
@@ -2261,7 +2261,7 @@ export default defineMiddlewares({
{
matcher: "/checkout_sessions/:id",
method: ["POST"],
middlewares: [validateAndTransformBody(PostUpdateSessionSchema)]
middlewares: [validateAndTransformBody(PostUpdateSessionSchema)],
},
],
// ...
@@ -2346,7 +2346,7 @@ export const GET = async (
},
context: {
idempotencyKey: req.headers["idempotency-key"] as string,
}
},
})
res.set(responseHeaders).status(201).json(result)
@@ -2499,7 +2499,7 @@ import {
createWorkflow,
transform,
when,
WorkflowResponse
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
completeCartWorkflow,
@@ -2507,11 +2507,11 @@ import {
createPaymentSessionsWorkflow,
refreshPaymentCollectionForCartWorkflow,
updateCartWorkflow,
useQueryGraphStep
useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import {
prepareCheckoutSessionDataWorkflow,
PrepareCheckoutSessionDataWorkflowInput
PrepareCheckoutSessionDataWorkflowInput,
} from "./prepare-checkout-session-data"
type WorkflowInput = {
@@ -2547,7 +2547,7 @@ export const completeCheckoutSessionWorkflow = createWorkflow(
"id",
"region.*",
"region.payment_providers.*",
"shipping_address.*"
"shipping_address.*",
],
filters: {
id: input.cart_id,
@@ -2589,7 +2589,7 @@ when(input, (input) => !!input.payment_data.billing_address)
postal_code: data.input.payment_data.billing_address!.postal_code,
country_code: data.input.payment_data.billing_address!.country,
phone: data.input.payment_data.billing_address!.phone_number,
}
},
}
})
return updateCartWorkflow.runAsStep({
@@ -2628,7 +2628,7 @@ const preparationInput = transform({
})
const paymentProviderId = transform({
input
input,
}, (data) => {
switch (data.input.payment_data.provider) {
case "stripe":
@@ -2640,7 +2640,7 @@ const paymentProviderId = transform({
const completeCartResponse = when({
carts,
paymentProviderId
paymentProviderId,
}, (data) => {
// @ts-ignore
return !!data.carts[0].region?.payment_providers?.find((provider) => provider?.id === data.paymentProviderId)
@@ -2649,7 +2649,7 @@ const completeCartResponse = when({
const paymentCollection = createPaymentCollectionForCartWorkflow.runAsStep({
input: {
cart_id: carts[0].id,
}
},
})
createPaymentSessionsWorkflow.runAsStep({
@@ -2658,14 +2658,14 @@ const completeCartResponse = when({
provider_id: paymentProviderId,
data: {
shared_payment_token: input.payment_data.token,
}
}
},
},
})
completeCartWorkflow.runAsStep({
input: {
id: carts[0].id,
}
},
})
return prepareCheckoutSessionDataWorkflow.runAsStep({
@@ -2695,7 +2695,7 @@ Replace the `TODO` in the workflow with the following:
```ts title="src/workflows/complete-checkout-session.ts"
const invalidPaymentResponse = when({
carts,
paymentProviderId
paymentProviderId,
}, (data) => {
return !data.carts[0].region?.payment_providers?.find((provider) => provider?.id === data.paymentProviderId)
})
@@ -2703,7 +2703,7 @@ const invalidPaymentResponse = when({
refreshPaymentCollectionForCartWorkflow.runAsStep({
input: {
cart_id: carts[0].id,
}
},
})
const prepareDataWithMessages = transform({
prepareData: preparationInput,
@@ -2716,12 +2716,12 @@ const invalidPaymentResponse = when({
code: "invalid",
content_type: "plain",
content: "Invalid payment provider",
}
]
},
],
} as PrepareCheckoutSessionDataWorkflowInput
})
return prepareCheckoutSessionDataWorkflow.runAsStep({
input: prepareDataWithMessages
input: prepareDataWithMessages,
}).config({ name: "prepare-checkout-session-data-with-messages" })
})
@@ -2739,7 +2739,7 @@ Finally, you'll return the appropriate response based on whether the cart was co
```ts title="src/workflows/complete-checkout-session.ts"
const responseData = transform({
completeCartResponse,
invalidPaymentResponse
invalidPaymentResponse,
}, (data) => {
return data.completeCartResponse || data.invalidPaymentResponse
})
@@ -2804,7 +2804,7 @@ export const POST = async (
},
context: {
idempotencyKey: req.headers["idempotency-key"] as string,
}
},
})
res.set(responseHeaders).json(result)
@@ -2814,7 +2814,7 @@ export const POST = async (
await refreshPaymentCollectionForCartWorkflow(req.scope).run({
input: {
cart_id: req.params.id,
}
},
})
const { result } = await prepareCheckoutSessionDataWorkflow(req.scope)
.run({
@@ -2828,8 +2828,8 @@ export const POST = async (
"payment_declined" : "invalid",
content_type: "plain",
content: medusaError.message,
}
]
},
],
},
})
@@ -2853,7 +2853,7 @@ Finally, you'll apply the validation middleware to the `POST /checkout_sessions/
In `src/api/middlewares.ts`, add the following import at the top of the file:
```ts title="src/api/middlewares.ts"
import { PostCompleteSessionSchema } from "./checkout_sessions/[id]/complete/route";
import { PostCompleteSessionSchema } from "./checkout_sessions/[id]/complete/route"
```
Then, add a new route configuration in `defineMiddlewares`:
@@ -2865,7 +2865,7 @@ export default defineMiddlewares({
{
matcher: "/checkout_sessions/:id/complete",
method: ["POST"],
middlewares: [validateAndTransformBody(PostCompleteSessionSchema)]
middlewares: [validateAndTransformBody(PostCompleteSessionSchema)],
},
],
// ...
@@ -3190,14 +3190,14 @@ export const cancelCheckoutSessionWorkflow = createWorkflow(
"id",
"payment_collection.*",
"payment_collection.payment_sessions.*",
"order.id"
"order.id",
],
filters: {
id: input.cart_id,
},
options: {
throwIfKeyNotFound: true,
}
},
})
validateCartCancelationStep({
@@ -3217,11 +3217,11 @@ Next, you'll cancel the payment sessions if there are any. Replace the `TODO` in
```ts title="src/workflows/cancel-checkout-session.ts"
when({
carts
carts,
}, (data) => !!data.carts[0].payment_collection?.payment_sessions?.length)
.then(() => {
const paymentSessionIds = transform({
carts
carts,
}, (data) => {
return data.carts[0].payment_collection?.payment_sessions?.map((session) => session!.id)
})
@@ -3235,8 +3235,8 @@ updateCartWorkflow.runAsStep({
id: carts[0].id,
metadata: {
checkout_session_canceled: true,
}
}
},
},
})
// TODO prepare and return response
@@ -3252,7 +3252,7 @@ Finally, you'll prepare and return the checkout session response. Replace the `T
const responseData = prepareCheckoutSessionDataWorkflow.runAsStep({
input: {
cart_id: carts[0].id,
}
},
})
return new WorkflowResponse(responseData)
@@ -3287,7 +3287,7 @@ export const POST = async (
},
context: {
idempotencyKey: req.headers["idempotency-key"] as string,
}
},
})
res.set(responseHeaders).json(result)
@@ -3300,8 +3300,8 @@ export const POST = async (
code: "invalid",
content_type: "plain",
content: medusaError.message,
}
]
},
],
})
}
}
@@ -3382,7 +3382,7 @@ export default async function orderWebhookHandler({
],
filters: {
id: orderId,
}
},
})
// only send webhook if order is associated with a checkout session
@@ -3404,7 +3404,7 @@ export default async function orderWebhookHandler({
type: "original_payment",
amount: transaction!.amount * -1,
})) || [],
}
},
}
// set status based on order, fulfillments and transactions

View File

@@ -22,7 +22,7 @@ const key = await cachingModuleService.computeKey(data)
await cachingModuleService.set({
key,
tags: ["Product:prod_123", "Product:list:*"],
data
data,
})
```
@@ -79,7 +79,7 @@ const key = await cachingModuleService.computeKey(data)
await cachingModuleService.set({
key,
tags: ["Product:list:*", "Product:prod_123"],
data
data,
})
```
@@ -170,7 +170,7 @@ const key = await cachingModuleService.computeKey(data)
await cachingModuleService.set({
key,
tags: ["Product:prod_123", "Product:list:*"],
data
data,
})
```

View File

@@ -108,14 +108,14 @@ export default async function connection({
serverUrls = ["127.0.0.1:11211"],
username,
password,
options: clientOptions
options: clientOptions,
} = options || {}
try {
logger.info("Connecting to Memcached...")
// Create Memcached client
const client = memjs.Client.create(serverUrls.join(','), {
const client = memjs.Client.create(serverUrls.join(","), {
username,
password,
...clientOptions,
@@ -271,9 +271,9 @@ class MemcachedCachingProviderService implements ICachingProviderService {
return { data, compressed: false }
}
const buffer = Buffer.from(data, 'utf8')
const buffer = Buffer.from(data, "utf8")
const compressed = await deflateAsync(buffer)
const compressedData = compressed.toString('base64')
const compressedData = compressed.toString("base64")
// Only use compression if it actually reduces size
if (compressedData.length < data.length) {
@@ -291,9 +291,9 @@ class MemcachedCachingProviderService implements ICachingProviderService {
return data
}
const buffer = Buffer.from(data, 'base64')
const buffer = Buffer.from(data, "base64")
const decompressed = await inflateAsync(buffer)
return decompressed.toString('utf8')
return decompressed.toString("utf8")
}
}
```
@@ -411,12 +411,12 @@ class MemcachedCachingProviderService implements ICachingProviderService {
// Compress data if enabled
const {
data: finalData,
compressed
compressed,
} = await this.compressData(serializedData)
// Batch operations for better performance
const operations: Promise<any>[] = [
this.client.set(prefixedKey, finalData, setOptions)
this.client.set(prefixedKey, finalData, setOptions),
]
// Always store options (including compression flag) to allow checking them later
@@ -484,9 +484,9 @@ class MemcachedCachingProviderService implements ICachingProviderService {
const storedTags = JSON.parse(keyTagsResult.value.toString())
// Batch all namespace checks for better performance
const tagKeys = Object.keys(storedTags).map(tag => this.TAG_PREFIX + tag)
const tagKeys = Object.keys(storedTags).map((tag) => this.TAG_PREFIX + tag)
const tagResults = await Promise.all(
tagKeys.map(tagKey => this.client.get(tagKey))
tagKeys.map((tagKey) => this.client.get(tagKey))
)
// Check if any tag namespace is missing or changed
@@ -536,7 +536,7 @@ class MemcachedCachingProviderService implements ICachingProviderService {
}
// Get all keys associated with each tag
const tagKeysOperations = tags.map(tag => {
const tagKeysOperations = tags.map((tag) => {
const tagKeysKey = `${this.TAG_KEYS_PREFIX}${tag}`
return this.client.get(tagKeysKey)
})
@@ -548,7 +548,7 @@ class MemcachedCachingProviderService implements ICachingProviderService {
for (const result of tagKeysResults) {
if (result.value) {
const keys = JSON.parse(result.value.toString()) as string[]
keys.forEach(key => allKeys.add(key))
keys.forEach((key) => allKeys.add(key))
}
}
@@ -557,7 +557,7 @@ class MemcachedCachingProviderService implements ICachingProviderService {
}
// Get all cached data for the collected keys
const dataOperations = Array.from(allKeys).map(async key => {
const dataOperations = Array.from(allKeys).map(async (key) => {
const prefixedKey = this.CACHE_PREFIX + key
const result = await this.client.get(prefixedKey)
@@ -717,7 +717,7 @@ class MemcachedCachingProviderService implements ICachingProviderService {
let keys: string[] = JSON.parse(tagKeysResult.value.toString()) as string[]
// Remove the specified keys
keys = keys.filter(key => !keysToRemove.includes(key))
keys = keys.filter((key) => !keysToRemove.includes(key))
if (keys.length === 0) {
// If no keys left, delete the tag keys entry
@@ -755,7 +755,7 @@ class MemcachedCachingProviderService implements ICachingProviderService {
const operations: Promise<any>[] = [
this.client.delete(this.CACHE_PREFIX + key),
this.client.delete(this.OPTIONS_PREFIX + key),
this.client.delete(keyTagsKey)
this.client.delete(keyTagsKey),
]
// If the key has tags, remove it from tag key lists
@@ -764,7 +764,7 @@ class MemcachedCachingProviderService implements ICachingProviderService {
const tagNames = Object.keys(storedTags)
// Batch tag cleanup operations
const tagCleanupOperations = tagNames.map(async tag => {
const tagCleanupOperations = tagNames.map(async (tag) => {
await this.removeKeysFromTag(tag, [key])
})
operations.push(...tagCleanupOperations)
@@ -794,7 +794,7 @@ Add the following method to the `MemcachedCachingProviderService` class:
class MemcachedCachingProviderService implements ICachingProviderService {
// ...
private async clearByTags(tags: string[]): Promise<void> {
const operations = tags.map(async tag => {
const operations = tags.map(async (tag) => {
const tagKey = this.TAG_PREFIX + tag
const result = await this.client.increment(tagKey, 1)
if (result === null) {
@@ -972,7 +972,7 @@ module.exports = defineConfig({
resolve: "@medusajs/medusa/caching",
options: {
in_memory: {
enable: true
enable: true,
},
providers: [
{
@@ -981,16 +981,16 @@ module.exports = defineConfig({
// Optional, makes this the default caching provider
is_default: true,
options: {
serverUrls: process.env.MEMCACHED_SERVERS?.split(',') ||
serverUrls: process.env.MEMCACHED_SERVERS?.split(",") ||
["127.0.0.1:11211"],
// add other optional options here...
},
},
// other caching providers...
],
}
}
]
},
},
],
})
```
@@ -1051,7 +1051,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
key: "test-cache-products",
// If you didn't set is_default to true, uncomment the following line
// providers: ["caching-memcached"],
}
},
})
res.status(200).json({

View File

@@ -74,10 +74,10 @@ module.exports = defineConfig({
// more options...
},
},
]
}
}
]
],
},
},
],
})
```
@@ -143,7 +143,7 @@ For example:
```ts highlights={[["14"], ["15"], ["16"]]}
import {
createWorkflow,
WorkflowResponse
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
@@ -201,7 +201,7 @@ export const getBrandStep = createStep(
const cacheKey = await cachingModuleService.computeKey(input.filters)
const cachedValue = await cachingModuleService.get({
tags: ["brand"]
tags: ["brand"],
})
if (cachedValue) {
@@ -213,7 +213,7 @@ export const getBrandStep = createStep(
await cachingModuleService.set({
key: cacheKey,
tags: [`Brand:${brand.id}`],
data: brand
data: brand,
})
return new StepResponse(brand)

View File

@@ -102,11 +102,11 @@ module.exports = defineConfig({
options: {
// Memcached options...
},
}
]
}
}
]
},
],
},
},
],
})
```
@@ -130,7 +130,7 @@ const { data: products } = useQueryGraphStep({
options: {
cache: {
enable: true,
providers: ["caching-memcached"] // Specify Memcached provider
providers: ["caching-memcached"], // Specify Memcached provider
},
},
})
@@ -145,7 +145,7 @@ const cachingModuleService = container.resolve(Modules.CACHING)
await cachingModuleService.set({
key: "product-list",
data: products,
providers: ["caching-memcached"] // Specify Memcached provider
providers: ["caching-memcached"], // Specify Memcached provider
})
```

View File

@@ -146,7 +146,7 @@ For example, you can create a workflow in `src/workflows/cache-products.ts` that
```ts title="src/workflows/cache-products.ts" highlights={[["14"], ["15"], ["16"], ["17"]]}
import {
createWorkflow,
WorkflowResponse
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

View File

@@ -406,6 +406,7 @@ If you get a type error on resolving the Meilisearch Module, run the Medusa appl
import { ProductDTO } from "@medusajs/framework/types"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { MEILISEARCH_MODULE } from "../../modules/meilisearch"
import MeilisearchModuleService from "../../modules/meilisearch/service"
export type SyncProductsStepInput = {
products: ProductDTO[]
@@ -414,7 +415,7 @@ export type SyncProductsStepInput = {
export const syncProductsStep = createStep(
"sync-products",
async ({ products }: SyncProductsStepInput, { container }) => {
const meilisearchModuleService = container.resolve(
const meilisearchModuleService = container.resolve<MeilisearchModuleService>(
MEILISEARCH_MODULE
)
const existingProducts = await meilisearchModuleService.retrieveFromIndex(
@@ -470,7 +471,7 @@ export const syncProductsStep = createStep(
return
}
const meilisearchModuleService = container.resolve(
const meilisearchModuleService = container.resolve<MeilisearchModuleService>(
MEILISEARCH_MODULE
)

View File

@@ -1200,13 +1200,14 @@ Create the file `src/workflows/send-order-confirmation.ts` with the following co
export const workflowHighlights = [
["13", "sendOrderConfirmationWorkflow", "Create the workflow that sends an order confirmation email."],
["15", "useQueryGraphStep", "Retrieve the order's details."],
["43", "sendNotificationStep", "Send the order confirmation email."]
["16", "useQueryGraphStep", "Retrieve the order's details."],
["48", "sendNotificationStep", "Send the order confirmation email."]
]
```ts title="src/workflows/send-order-confirmation.ts" highlights={workflowHighlights}
import {
createWorkflow,
when,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
@@ -1244,18 +1245,26 @@ export const sendOrderConfirmationWorkflow = createWorkflow(
filters: {
id,
},
options: {
throwIfKeyNotFound: true,
},
})
const notification = sendNotificationStep([{
to: orders[0].email,
channel: "email",
template: "order-placed",
data: {
order: orders[0],
},
}])
const notification = when({ orders }, (data) => !!data.orders[0].email)
.then(() => {
return sendNotificationStep([{
to: orders[0].email!,
channel: "email",
template: "order-placed",
data: {
order: orders[0],
},
}])
})
return new WorkflowResponse(notification)
return new WorkflowResponse({
notification,
})
}
)
```
@@ -1265,7 +1274,7 @@ You create a workflow using `createWorkflow` from the Workflows SDK. It accepts
It accepts as a second parameter a constructor function, which is the workflow's implementation. The workflow has the following steps:
1. `useQueryGraphStep`, which is a step implemented by Medusa that uses [Query](!docs!/learn/fundamentals/module-links/query), a tool that allows you to retrieve data across modules. You use it to retrieve the order's details.
2. `sendNotificationStep` which is the step you implemented. You pass it an array with one object, which is the notification's details having following properties:
2. Ensure that the order has an email address by using `when-then`. If so, you send the notification using the `sendNotificationStep` you implemented earlier. You pass it an object with the following properties:
- `to`: The address to send the email to. You pass the customer's email that is stored in the order.
- `channel`: The channel to send the notification through, which is `email`. Since you specified `email` in the Resend Module Provider's `channel` option, the Notification Module will delegate the sending to the Resend Module Provider's service.
- `template`: The email's template type. You retrieve the template content in the `ResendNotificationProviderService`'s `send` method based on the template specified here.
@@ -1273,7 +1282,7 @@ It accepts as a second parameter a constructor function, which is the workflow's
<Note title="Tip">
A workflow's constructor function has some constraints in implementation. Learn more about them in [this documentation](!docs!/learn/fundamentals/workflows/constructor-constraints).
`when` allows you to perform steps based on a condition during execution. Learn more in the [Conditions in Workflows](!docs!/learn/fundamentals/workflows/conditions) documentation.
</Note>

View File

@@ -215,7 +215,7 @@ export const createReturnRequest = async (
return_shipping: {
option_id: returnShippingOptionId,
},
location_id: locationId
location_id: locationId,
},
headers,
})
@@ -671,7 +671,7 @@ export const returnShippingSelectorHighlights = [
import React, {
useEffect,
useState
useState,
} from "react"
import { RadioGroup } from "@headlessui/react"
import { HttpTypes } from "@medusajs/types"
@@ -911,7 +911,7 @@ const ReturnRequestTemplate: React.FC<ReturnRequestTemplateProps> = ({
formData.append("items", JSON.stringify(selectedItems))
formData.append("return_shipping_option_id", selectedShippingOption)
// @ts-expect-error issue in HTTP types
const locationId = shippingOptions.find(opt => opt.id === selectedShippingOption)?.service_zone.fulfillment_set.location.id
const locationId = shippingOptions.find((opt) => opt.id === selectedShippingOption)?.service_zone.fulfillment_set.location.id
formData.append("location_id", locationId)
formAction(formData)
}

View File

@@ -450,7 +450,7 @@ export const validateVariantOutOfStockStep = createStep(
sales_channel_id,
})
if (availability[variant_id].availability > 0) {
if ((availability[variant_id].availability || 0) > 0) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Variant isn't out of stock."
@@ -1212,7 +1212,7 @@ export const getRestockedStep = createStep(
sales_channel_id: restockSubscription.sales_channel_id,
})
if (variantAvailability[restockSubscription.variant_id].availability > 0) {
if ((variantAvailability[restockSubscription.variant_id].availability || 0) > 0) {
restocked.push(restockSubscription)
}
})

View File

@@ -461,7 +461,7 @@ Create the file `src/workflows/create-digital-product/steps/create-digital-produ
export const createDpHighlights = [
["19", "createDigitalProducts", "Create the digital product."],
["24", "digital_product", "Pass the digital product to the compensation function."],
["31", "deleteDigitalProducts", "Delete the digital product if an error occurs in the workflow."]
["34", "deleteDigitalProducts", "Delete the digital product if an error occurs in the workflow."]
]
```ts title="src/workflows/create-digital-product/steps/create-digital-product.ts" highlights={createDpHighlights} collapsibleLines="1-7" expandMoreLabel="Show Imports"
@@ -491,12 +491,15 @@ const createDigitalProductStep = createStep(
digital_product: digitalProduct,
})
},
async ({ digital_product }, { container }) => {
async (data, { container }) => {
if (!data) {
return
}
const digitalProductModuleService: DigitalProductModuleService =
container.resolve(DIGITAL_PRODUCT_MODULE)
await digitalProductModuleService.deleteDigitalProducts(
digital_product.id
data.digital_product.id
)
}
)
@@ -515,7 +518,7 @@ Create the file `src/workflows/create-digital-product/steps/create-digital-produ
export const createDigitalProductMediaHighlights = [
["29", "createDigitalProductMedias", "Create the digital product medias."],
["34", "digital_product_medias", "Pass the digital product medias to the compensation function."],
["41", "deleteDigitalProductMedias", "Delete the digital product medias if an error occurs in the workflow."]
["44", "deleteDigitalProductMedias", "Delete the digital product medias if an error occurs in the workflow."]
]
```ts title="src/workflows/create-digital-product/steps/create-digital-product-medias.ts" highlights={createDigitalProductMediaHighlights} collapsibleLines="1-8" expandMoreLabel="Show Imports"
@@ -555,12 +558,15 @@ const createDigitalProductMediasStep = createStep(
digital_product_medias: digitalProductMedias,
})
},
async ({ digital_product_medias }, { container }) => {
async (data, { container }) => {
if (!data) {
return
}
const digitalProductModuleService: DigitalProductModuleService =
container.resolve(DIGITAL_PRODUCT_MODULE)
await digitalProductModuleService.deleteDigitalProductMedias(
digital_product_medias.map((media) => media.id)
data.digital_product_medias.map((media) => media.id)
)
}
)
@@ -1883,7 +1889,7 @@ export const createDpoHighlights = [
["18", "InferTypeOf", "Infer the type of the `DigitalProduct` data model since it's a variable."],
["33", "createDigitalProductOrders", "Create the digital product order."],
["41", "digital_product_order", "Pass the created digital product order to the compensation function."],
["48", "deleteDigitalProductOrders", "Delete the digital product order if an error occurs in the workflow."]
["51", "deleteDigitalProductOrders", "Delete the digital product order if an error occurs in the workflow."]
]
```ts title="src/workflows/create-digital-product-order/steps/create-digital-product-order.ts" highlights={createDpoHighlights} collapsibleLines="1-14" expandMoreLabel="Show Imports"
@@ -1901,7 +1907,7 @@ import DigitalProductModuleService from "../../../modules/digital-product/servic
import { DIGITAL_PRODUCT_MODULE } from "../../../modules/digital-product"
import DigitalProduct from "../../../modules/digital-product/models/digital-product"
type StepInput = {
export type CreateDigitalProductOrderStepInput = {
items: (OrderLineItemDTO & {
variant: ProductVariantDTO & {
digital_product: InferTypeOf<typeof DigitalProduct>
@@ -1911,7 +1917,7 @@ type StepInput = {
const createDigitalProductOrderStep = createStep(
"create-digital-product-order",
async ({ items }: StepInput, { container }) => {
async ({ items }: CreateDigitalProductOrderStepInput, { container }) => {
const digitalProductModuleService: DigitalProductModuleService =
container.resolve(DIGITAL_PRODUCT_MODULE)
@@ -1929,12 +1935,15 @@ const createDigitalProductOrderStep = createStep(
digital_product_order: digitalProductOrder,
})
},
async ({ digital_product_order }, { container }) => {
async (data, { container }) => {
if (!data) {
return
}
const digitalProductModuleService: DigitalProductModuleService =
container.resolve(DIGITAL_PRODUCT_MODULE)
await digitalProductModuleService.deleteDigitalProductOrders(
digital_product_order.id
data.digital_product_order.id
)
}
)
@@ -1951,14 +1960,14 @@ In the compensation function, you delete the digital product order.
Create the file `src/workflows/create-digital-product-order/index.ts` with the following content:
export const createDpoWorkflowHighlights = [
["27", "completeCartWorkflow", "Create an order for the cart."],
["33", "useQueryGraphStep", "Retrieve the order's items and their associated variants and linked digital products."],
["58", "when", "Check whether the order has any digital products."],
["64", "then", "Perform the callback function if an order has digital products."],
["67", "createDigitalProductOrderStep", "Create the digital product order."],
["71", "createRemoteLinkStep", "Link the digital product order to the Medusa order."],
["80", "createOrderFulfillmentWorkflow", "Create a fulfillment for the digital products in the order."],
["94", "emitEventStep", "Emit the `digital_product_order.created` event."]
["29", "completeCartWorkflow", "Create an order for the cart."],
["35", "useQueryGraphStep", "Retrieve the order's items and their associated variants and linked digital products."],
["60", "when", "Check whether the order has any digital products."],
["66", "then", "Perform the callback function if an order has digital products."],
["69", "createDigitalProductOrderStep", "Create the digital product order."],
["73", "createRemoteLinkStep", "Link the digital product order to the Medusa order."],
["82", "createOrderFulfillmentWorkflow", "Create a fulfillment for the digital products in the order."],
["96", "emitEventStep", "Emit the `digital_product_order.created` event."]
]
```ts title="src/workflows/create-digital-product-order/index.ts" highlights={createDpoWorkflowHighlights} collapsibleLines="1-17" expandMoreLabel="Show Imports"
@@ -1978,7 +1987,9 @@ import {
import {
Modules,
} from "@medusajs/framework/utils"
import createDigitalProductOrderStep from "./steps/create-digital-product-order"
import createDigitalProductOrderStep, {
CreateDigitalProductOrderStepInput,
} from "./steps/create-digital-product-order"
import { DIGITAL_PRODUCT_MODULE } from "../../modules/digital-product"
type WorkflowInput = {
@@ -2015,7 +2026,7 @@ const createDigitalProductOrderWorkflow = createWorkflow(
orders,
},
(data) => {
return data.orders[0].items.filter((item) => item.variant.digital_product !== undefined)
return data.orders[0].items?.filter((item) => item?.variant?.digital_product !== undefined)
}
)
@@ -2023,14 +2034,14 @@ const createDigitalProductOrderWorkflow = createWorkflow(
"create-digital-product-order-condition",
itemsWithDigitalProducts,
(itemsWithDigitalProducts) => {
return itemsWithDigitalProducts.length
return !!itemsWithDigitalProducts?.length
}
).then(() => {
const {
digital_product_order,
} = createDigitalProductOrderStep({
items: orders[0].items,
})
} as unknown as CreateDigitalProductOrderStepInput)
createRemoteLinkStep([{
[DIGITAL_PRODUCT_MODULE]: {
@@ -2047,9 +2058,9 @@ const createDigitalProductOrderWorkflow = createWorkflow(
items: transform({
itemsWithDigitalProducts,
}, (data) => {
return data.itemsWithDigitalProducts.map((item) => ({
id: item.id,
quantity: item.quantity,
return data.itemsWithDigitalProducts!.map((item) => ({
id: item!.id,
quantity: item!.quantity,
}))
}),
},
@@ -2225,10 +2236,14 @@ import {
INotificationModuleService,
IFileModuleService,
} from "@medusajs/framework/types"
import { ModuleRegistrationName } from "@medusajs/framework/utils"
import {
MedusaError,
ModuleRegistrationName,
promiseAll,
} from "@medusajs/framework/utils"
import { DigitalProductOrder, MediaType } from "../../../modules/digital-product/types"
type SendDigitalOrderNotificationStepInput = {
export type SendDigitalOrderNotificationStepInput = {
digital_product_order: DigitalProductOrder
}
@@ -2244,6 +2259,20 @@ export const sendDigitalOrderNotificationStep = createStep(
ModuleRegistrationName.FILE
)
if (!digitalProductOrder.order) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Digital product order is missing associated order."
)
}
if (!digitalProductOrder.order.email) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Order is missing email."
)
}
// TODO assemble notification
}
)
@@ -2251,16 +2280,16 @@ export const sendDigitalOrderNotificationStep = createStep(
This creates the `sendDigitalOrderNotificationStep` step that receives a digital product order as an input.
In the step, so far you resolve the main services of the Notification and File Modules.
In the step, so far you resolve the main services of the Notification and File Modules. You also check that the digital product order has an associated order and that the order has an email.
Replace the `TODO` with the following:
Next, you'll prepare the data to pass in the notification. Replace the `TODO` with the following:
```ts title="src/workflows/fulfill-digital-order/steps/send-digital-order-notification.ts"
const notificationData = await Promise.all(
const notificationData = await promiseAll(
digitalProductOrder.products.map(async (product) => {
const medias = []
const medias: string[] = []
await Promise.all(
await promiseAll(
product.medias
.filter((media) => media.type === MediaType.MAIN)
.map(async (media) => {
@@ -2304,12 +2333,12 @@ You use the `createNotifications` method of the Notification Module's main servi
Create the workflow in the file `src/workflows/fulfill-digital-order/index.ts`:
export const fulfillWorkflowHighlights = [
["18", "useQueryGraphStep", "Retrieve the digital product order's details."],
["35", "sendDigitalOrderNotificationStep", "Send a notification to the customer."],
["39", "markOrderFulfillmentAsDeliveredWorkflow", "Mark the order's fulfillment as delivered."],
["21", "useQueryGraphStep", "Retrieve the digital product order's details."],
["38", "sendDigitalOrderNotificationStep", "Send a notification to the customer."],
["44", "markOrderFulfillmentAsDeliveredWorkflow", "Mark the order's fulfillment as delivered."],
]
```ts title="src/workflows/fulfill-digital-order/index.ts" highlights={fulfillWorkflowHighlights} collapsibleLines="1-10" expandMoreLabel="Show Imports"
```ts title="src/workflows/fulfill-digital-order/index.ts" highlights={fulfillWorkflowHighlights} collapsibleLines="1-13" expandMoreLabel="Show Imports"
import {
createWorkflow,
WorkflowResponse,
@@ -2318,7 +2347,10 @@ import {
markOrderFulfillmentAsDeliveredWorkflow,
useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { sendDigitalOrderNotificationStep } from "./steps/send-digital-order-notification"
import {
sendDigitalOrderNotificationStep,
SendDigitalOrderNotificationStepInput,
} from "./steps/send-digital-order-notification"
type FulfillDigitalOrderWorkflowInput = {
id: string
@@ -2346,14 +2378,17 @@ export const fulfillDigitalOrderWorkflow = createWorkflow(
sendDigitalOrderNotificationStep({
digital_product_order: digitalProductOrders[0],
})
} as unknown as SendDigitalOrderNotificationStepInput)
markOrderFulfillmentAsDeliveredWorkflow.runAsStep({
input: {
orderId: digitalProductOrders[0].order.id,
fulfillmentId: digitalProductOrders[0].order.fulfillments[0].id,
},
})
when({ digitalProductOrders }, (data) => !!data.digitalProductOrders[0].order?.fulfillments?.length)
.then(() => {
markOrderFulfillmentAsDeliveredWorkflow.runAsStep({
input: {
orderId: digitalProductOrders[0].order!.id,
fulfillmentId: digitalProductOrders[0].order!.fulfillments![0]!.id,
},
})
})
return new WorkflowResponse(
digitalProductOrders[0]
@@ -2366,7 +2401,13 @@ In the workflow, you:
1. Retrieve the digital product order's details using `useQueryGraphStep` from Medusa's core workflows.
2. Send a notification to the customer with the digital product download links using the `sendDigitalOrderNotificationStep`.
3. Mark the order's fulfillment as delivered using `markOrderFulfillmentAsDeliveredWorkflow` from Medusa's core workflows.
3. Check whether the Medusa order has fulfillments. If so, mark the order's fulfillment as delivered using `markOrderFulfillmentAsDeliveredWorkflow` from Medusa's core workflows.
<Note title="Tip">
`when` allows you to perform steps based on a condition during execution. Learn more in the [Conditions in Workflows](!docs!/learn/fundamentals/workflows/conditions) documentation.
</Note>
### Configure Notification Module Provider
@@ -2555,8 +2596,11 @@ export const GET = async (
const digitalProducts = {}
customer.orders.forEach((order) => {
order.digital_product_order.products.forEach((product) => {
customer.orders?.forEach((order) => {
order?.digital_product_order?.products.forEach((product) => {
if (!product) {
return
}
digitalProducts[product.id] = product
})
})
@@ -2613,9 +2657,9 @@ export const POST = async (
},
})
const customerDigitalOrderIds = customer.orders
.filter((order) => order.digital_product_order !== undefined)
.map((order) => order.digital_product_order.id)
const customerDigitalOrderIds = customer.orders?.filter(
(order) => order?.digital_product_order !== undefined
).map((order) => order!.digital_product_order!.id)
const { data: dpoResult } = await query.graph({
entity: "digital_product_order",
@@ -2634,11 +2678,11 @@ export const POST = async (
)
}
let foundMedia = undefined
let foundMedia: any | undefined = undefined
dpoResult[0].products.some((product) => {
return product.medias.some((media) => {
foundMedia = media.id === req.params.mediaId ? media : undefined
return product?.medias.some((media) => {
foundMedia = media?.id === req.params.mediaId ? media : undefined
return foundMedia !== undefined
})

View File

@@ -231,15 +231,9 @@ export enum DeliveryStatus {
IN_TRANSIT = "in_transit",
DELIVERED = "delivered",
}
declare module "@medusajs/framework/types" {
export interface ModuleImplementations {
deliveryModuleService: DeliveryModuleService;
}
}
```
This adds an enum that is used by the data models. It also adds a type for `deliveryModuleService` in `ModuleImplementations` so that when you resolve it from the Medusa container, it has the correct typing.
This adds an enum that is used by the data models.
### Create Delivery Data Models
@@ -489,15 +483,9 @@ import { Restaurant } from "../models/restaurant"
export type CreateRestaurant = Omit<
InferTypeOf<typeof Restaurant>, "id" | "admins"
>
declare module "@medusajs/framework/types" {
export interface ModuleImplementations {
restaurantModuleService: RestaurantModuleService;
}
}
```
This adds a type used for inputs in creating a restaurant. It also adds a type for `restaurantModuleService` in `ModuleImplementations` so that when you resolve it from the Medusa container, it has the correct typing.
This adds a type used for inputs in creating a restaurant.
<Note title="Tip">
@@ -920,14 +908,16 @@ const { user, authUserInput } = transform({ input, restaurantUser, driverUser },
authUserInput: {
authIdentityId: data.input.auth_identity_id,
actorType: data.input.user.actor_type,
value: user.id,
value: user?.id || "",
},
}
})
setAuthAppMetadataStep(authUserInput)
return new WorkflowResponse(user)
return new WorkflowResponse({
user,
})
```
In the workflow, you:
@@ -987,7 +977,7 @@ export const POST = async (
} as CreateUserWorkflowInput,
})
res.status(201).json({ user: result })
res.status(201).json({ user: result.user })
}
```
@@ -1133,12 +1123,15 @@ export const deleteRestaurantAdminStep = createStep(
return new StepResponse(undefined, { admin })
},
async ({ admin }, { container }) => {
async (data, { container }) => {
if (!data) {
return
}
const restaurantModuleService: RestaurantModuleService = container.resolve(
RESTAURANT_MODULE
)
const { restaurant: _, ...adminData } = admin
const { restaurant: _, ...adminData } = data.admin
await restaurantModuleService.createRestaurantAdmins(adminData)
}
@@ -1190,6 +1183,7 @@ const { data: authIdentities } = useQueryGraphStep({
entity: "auth_identity",
fields: ["id"],
filters: {
// @ts-ignore
app_metadata: {
restaurant_id: input.id,
},
@@ -1498,7 +1492,7 @@ Next, create the file `src/workflows/delivery/steps/create-delivery.ts` with the
export const createDeliveryStepHighlights = [
["11", "createDeliveries", "Create the delivery."],
["21", "softDeleteDeliveries", "Delete the delivery if an error occurs in the workflow."]
["24", "softDeleteDeliveries", "Delete the delivery if an error occurs in the workflow."]
]
```ts title="src/workflows/delivery/steps/create-delivery.ts" highlights={createDeliveryStepHighlights}
@@ -1518,11 +1512,14 @@ export const createDeliveryStep = createStep(
delivery_id: delivery.id,
})
},
async function ({ delivery_id }, { container }) {
async function (data, { container }) {
if (!data) {
return
}
const deliverModuleService: DeliveryModuleService =
container.resolve(DELIVERY_MODULE)
deliverModuleService.softDeleteDeliveries(delivery_id)
deliverModuleService.softDeleteDeliveries(data.delivery_id)
}
)
@@ -1535,10 +1532,10 @@ This step creates a delivery and returns it. In the compensation function, it de
Finally, create the workflow in `src/workflows/delivery/workflows/create-delivery.ts`:
export const createDeliveryWorkflowHighlights = [
["23", "validateRestaurantStep", "Ensure that the specified restaurant exists before creating the delivery."],
["26", "createDeliveryStep", "Create the delivery."],
["28", "transform", "Define an array of link objects to create."],
["50", "createRemoteLinkStep", "Create the links between the delivery and cart and restaurant."]
["24", "validateRestaurantStep", "Ensure that the specified restaurant exists before creating the delivery."],
["27", "createDeliveryStep", "Create the delivery."],
["29", "transform", "Define an array of link objects to create."],
["51", "createRemoteLinkStep", "Create the links between the delivery and cart and restaurant."]
]
```ts title="src/workflows/delivery/workflows/create-delivery.ts" highlights={createDeliveryWorkflowHighlights} collapsibleLines="1-13" expandButtonLabel="Show Imports"
@@ -1548,6 +1545,7 @@ import {
createWorkflow,
transform,
} from "@medusajs/framework/workflows-sdk"
import { LinkDefinition } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
import { DELIVERY_MODULE } from "../../../modules/delivery"
@@ -1589,7 +1587,7 @@ export const createDeliveryWorkflow = createWorkflow(
delivery_id: data.delivery.id,
},
},
]))
] as LinkDefinition[]))
createRemoteLinkStep(links)
@@ -1787,7 +1785,7 @@ export const notifyRestaurantStep = createStep(
await eventBus.emit({
name: "notify.restaurant",
data: {
restaurant_id: delivery.restaurant.id,
restaurant_id: delivery.restaurant?.id,
delivery_id: delivery.id,
},
})
@@ -1837,7 +1835,7 @@ This step is async and its only purpose is to wait until its marked as succes
Create the file `src/workflows/delivery/steps/create-order.ts` with the following content:
export const createOrderStepHighlights1 = [
["15", "deliveryQuery", "Retrieve the delivery with its linked cart's details."]
["16", "deliveryQuery", "Retrieve the delivery with its linked cart's details."]
]
```ts title="src/workflows/delivery/steps/create-order.ts" highlights={createOrderStepHighlights1} collapsibleLines="1-9" expandButtonLabel="Show Imports"
@@ -1845,6 +1843,7 @@ import { CreateOrderShippingMethodDTO } from "@medusajs/framework/types"
import {
Modules,
ContainerRegistrationKeys,
MedusaError,
} from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { DELIVERY_MODULE } from "../../../modules/delivery"
@@ -1871,7 +1870,7 @@ export const createOrderStep = createStep(
// TODO create order
},
async ({ orderId }, { container }) => {
async (data, { container }) => {
// TODO add compensation
}
)
@@ -1883,26 +1882,66 @@ This creates the `createOrderStep`, which so far only retrieves the delivery wit
Replace the `TODO` with the following to create the order:
export const createOrderStepHighlights2 = [
["5", "createOrders", "Create the order."],
["18", "linkDef", "Define an array of links to be created."]
["12", "createOrders", "Create the order."],
["58", "linkDef", "Define an array of links to be created."]
]
```ts title="src/workflows/delivery/steps/create-order.ts" highlights={createOrderStepHighlights2}
const { cart } = delivery
if (!cart) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Cart for delivery with id: ${deliveryId} was not found`
)
}
const orderModuleService = container.resolve(Modules.ORDER)
const order = await orderModuleService.createOrders({
currency_code: cart.currency_code,
email: cart.email,
shipping_address: cart.shipping_address,
billing_address: cart.billing_address,
items: cart.items,
region_id: cart.region_id,
customer_id: cart.customer_id,
sales_channel_id: cart.sales_channel_id,
shipping_address: {
first_name: cart.shipping_address?.first_name || "",
last_name: cart.shipping_address?.last_name || "",
address_1: cart.shipping_address?.address_1 || "",
address_2: cart.shipping_address?.address_2 || "",
city: cart.shipping_address?.city || "",
province: cart.shipping_address?.province || "",
postal_code: cart.shipping_address?.postal_code || "",
country_code: cart.shipping_address?.country_code || "",
phone: cart.shipping_address?.phone || "",
},
billing_address: {
first_name: cart.billing_address?.first_name || "",
last_name: cart.billing_address?.last_name || "",
address_1: cart.billing_address?.address_1 || "",
address_2: cart.billing_address?.address_2 || "",
city: cart.billing_address?.city || "",
province: cart.billing_address?.province || "",
postal_code: cart.billing_address?.postal_code || "",
country_code: cart.billing_address?.country_code || "",
phone: cart.billing_address?.phone || "",
},
items: cart.items.map((item) => ({
title: item!.title,
quantity: item!.quantity,
variant_id: item!.variant_id || "",
unit_price: item!.unit_price,
metadata: item!.metadata || undefined,
product_id: item!.product_id || "",
})),
region_id: cart.region_id || "",
customer_id: cart.customer_id || "",
sales_channel_id: cart.sales_channel_id || "",
shipping_methods:
cart.shipping_methods as unknown as CreateOrderShippingMethodDTO[],
cart.shipping_methods?.map((sm): CreateOrderShippingMethodDTO => ({
shipping_option_id: sm?.shipping_option_id || "",
data: sm?.data || {},
name: sm?.name || "",
amount: sm?.amount || 0,
order_id: "", // will be set internally
})) || [],
})
const linkDef = [{
@@ -1927,9 +1966,12 @@ You create the order using the Order Modules main service. Then, you create a
Then, replace the `TODO` in the compensation function with the following:
```ts title="src/workflows/delivery/steps/create-order.ts"
if (!data) {
return
}
const orderService = container.resolve(Modules.ORDER)
await orderService.softDeleteOrders([orderId])
await orderService.softDeleteOrders([data.orderId])
```
You delete the order in the compensation function.
@@ -2374,7 +2416,7 @@ So, start by creating the first step at `src/workflows/delivery/steps/update-del
export const updateDeliveryStepHighlights = [
["16", "prevDeliveryData", "Retrieve the previous data of the delivery for the compensation."],
["19", "updateDeliveries", "Update the delivery."],
["35", "updateDeliveries", "Update the delivery with the old data if an error occurs in the workflow."]
["38", "updateDeliveries", "Update the delivery with the old data if an error occurs in the workflow."]
]
```ts title="src/workflows/delivery/steps/update-delivery.ts" highlights={updateDeliveryStepHighlights}
@@ -2403,14 +2445,17 @@ export const updateDeliveryStep = createStep(
prevDeliveryData,
})
},
async ({ prevDeliveryData }, { container }) => {
async (data, { container }) => {
if (!data) {
return
}
const deliverModuleService: DeliveryModuleService =
container.resolve(DELIVERY_MODULE)
const {
driver,
...prevDeliveryDataWithoutDriver
} = prevDeliveryData
} = data.prevDeliveryData
await deliverModuleService.updateDeliveries(prevDeliveryDataWithoutDriver)
}
@@ -2452,7 +2497,7 @@ export const setStepSuccessStep = createStep(
await engineService.setStepSuccess({
idempotencyKey: {
action: TransactionHandlerType.INVOKE,
transactionId: updatedDelivery.transaction_id,
transactionId: updatedDelivery.transaction_id || "",
stepId,
workflowId: handleDeliveryWorkflowId,
},
@@ -2500,7 +2545,7 @@ export const setStepFailedStep = createStep(
await engineService.setStepFailure({
idempotencyKey: {
action: TransactionHandlerType.INVOKE,
transactionId: updatedDelivery.transaction_id,
transactionId: updatedDelivery.transaction_id || "",
stepId,
workflowId: handleDeliveryWorkflowId,
},
@@ -2551,7 +2596,7 @@ export const updateDeliveryWorkflow = createWorkflow(
when(input, ({ stepIdToSucceed }) => stepIdToSucceed !== undefined)
.then(() => {
setStepSuccessStep({
stepId: input.stepIdToSucceed,
stepId: input.stepIdToSucceed!,
updatedDelivery,
})
})
@@ -2560,7 +2605,7 @@ export const updateDeliveryWorkflow = createWorkflow(
when(input, ({ stepIdToFail }) => stepIdToFail !== undefined)
.then(() => {
setStepFailedStep({
stepId: input.stepIdToFail,
stepId: input.stepIdToFail!,
updatedDelivery,
})
})
@@ -2688,7 +2733,7 @@ export const isDeliveryRestaurant = async (
},
})
if (delivery.restaurant.id !== restaurantAdmin.restaurant.id) {
if (delivery.restaurant?.id !== restaurantAdmin.restaurant.id) {
return res.status(403).json({
message: "unauthorized",
})
@@ -2738,7 +2783,7 @@ import deliveriesMiddlewares from "./deliveries/[id]/middlewares"
export default defineMiddlewares({
routes: [
// ...
...deliveriesMiddlewares.routes,
...deliveriesMiddlewares.routes!,
],
})
```
@@ -3387,7 +3432,7 @@ res.write(
"data: " +
JSON.stringify({
message: "Subscribed to workflow",
transactionId: delivery.transaction_id,
transactionId: delivery.transaction_id || undefined,
}) +
"\n\n"
)

View File

@@ -975,6 +975,7 @@ import {
} from "@medusajs/framework/workflows-sdk"
import {
createProductsWorkflow,
CreateProductsWorkflowInput,
createRemoteLinkStep,
useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
@@ -1014,7 +1015,7 @@ const createVendorProductWorkflow = createWorkflow(
})
const createdProducts = createProductsWorkflow.runAsStep({
input: productData,
input: productData as CreateProductsWorkflowInput,
})
// TODO link vendor and products
@@ -1333,26 +1334,28 @@ import {
createStep,
StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { CartDTO, CartLineItemDTO } from "@medusajs/framework/types"
import { CartLineItemDTO } from "@medusajs/framework/types"
import { ContainerRegistrationKeys, promiseAll } from "@medusajs/framework/utils"
type StepInput = {
cart: CartDTO
export type GroupVendorItemsStepInput = {
cart: {
items?: CartLineItemDTO[]
}
}
const groupVendorItemsStep = createStep(
"group-vendor-items",
async ({ cart }: StepInput, { container }) => {
async ({ cart }: GroupVendorItemsStepInput, { container }) => {
const query = container.resolve(ContainerRegistrationKeys.QUERY)
const vendorsItems: Record<string, CartLineItemDTO[]> = {}
await promiseAll(cart.items?.map(async (item) => {
await promiseAll((cart.items || []).map(async (item) => {
const { data: [product] } = await query.graph({
entity: "product",
fields: ["vendor.*"],
filters: {
id: [item.product_id],
id: item.product_id || "",
},
})
@@ -1451,7 +1454,7 @@ const createVendorOrdersStep = createStep(
created_orders: createdOrders,
})
},
async ({ created_orders }, { container, context }) => {
async (data, { container, context }) => {
// TODO add compensation function
}
)
@@ -1586,19 +1589,19 @@ function prepareOrderData(
// item/vendor. This requires changes in the storefront to commodate that
// and passing the item/vendor ID in the `data` property, for example.
// For simplicity here we just use the same shipping method.
shipping_methods: parentOrder.shipping_methods.map((shippingMethod) => ({
shipping_methods: parentOrder.shipping_methods?.map((shippingMethod) => ({
name: shippingMethod.name,
amount: shippingMethod.amount,
shipping_option_id: shippingMethod.shipping_option_id,
data: shippingMethod.data,
tax_lines: shippingMethod.tax_lines.map((taxLine) => ({
tax_lines: shippingMethod.tax_lines?.map((taxLine) => ({
code: taxLine.code,
rate: taxLine.rate,
provider_id: taxLine.provider_id,
tax_rate_id: taxLine.tax_rate_id,
description: taxLine.description,
})),
adjustments: shippingMethod.adjustments.map((adjustment) => ({
adjustments: shippingMethod.adjustments?.map((adjustment) => ({
code: adjustment.code,
amount: adjustment.amount,
description: adjustment.description,
@@ -1621,7 +1624,10 @@ When creating the child orders, the shipping method of the parent is used as-is
Finally, replace the `TODO` in the compensation function with the following:
```ts title="src/workflows/marketplace/create-vendor-orders/steps/create-vendor-orders.ts"
await Promise.all(created_orders.map((createdOrder) => {
if (!data) {
return
}
await promiseAll(data.created_orders.map((createdOrder) => {
return cancelOrderWorkflow(container).run({
input: {
order_id: createdOrder.id,
@@ -1687,7 +1693,7 @@ const createVendorOrdersWorkflow = createWorkflow(
const { vendorsItems } = groupVendorItemsStep({
cart: carts[0],
})
} as unknown as GroupVendorItemsStepInput)
const order = getOrderDetailWorkflow.runAsStep({
input: {
@@ -1869,7 +1875,7 @@ export const GET = async (
],
variables: {
filters: {
id: vendorAdmin.vendor.orders.map((order) => order.id),
id: vendorAdmin.vendor.orders?.map((order) => order?.id),
},
},
},

View File

@@ -646,7 +646,7 @@ const createSubscriptionStep = createStep(
}, {
subscription: subscription[0],
})
}, async ({ subscription }, { container }) => {
}, async (data, { container }) => {
// TODO implement compensation
}
)
@@ -706,10 +706,13 @@ This adds links between:
The step also has a compensation function to undo the steps changes if an error occurs. So, replace the second `TODO` with the following:
```ts title="src/workflows/create-subscription/steps/create-subscription.ts"
if (!data) {
return
}
const subscriptionModuleService: SubscriptionModuleService =
container.resolve(SUBSCRIPTION_MODULE)
container.resolve(SUBSCRIPTION_MODULE)
await subscriptionModuleService.cancelSubscriptions(subscription.id)
await subscriptionModuleService.cancelSubscriptions(data.subscription.id)
```
The compensation function receives the subscription as a parameter. It cancels the subscription.
@@ -815,7 +818,7 @@ const createSubscriptionWorkflow = createWorkflow(
const { subscription, linkDefs } = createSubscriptionStep({
cart_id: input.cart_id,
order_id: orders[0].id,
customer_id: orders[0].customer_id,
customer_id: orders[0].customer_id!,
subscription_data: input.subscription_data,
})
@@ -859,9 +862,9 @@ In this step, youll create a custom API route similar to the [Complete Cart A
Create the file `src/api/store/carts/[id]/subscribe/route.ts` with the following content:
export const completeCartHighlights = [
["17", "graph", "Retrieve the cart to retrieve the subscription details from the `metadata`."],
["29", "", "If the subscription data isn't set in the cart's `metadata`, throw an error"],
["36", "createSubscriptionWorkflow", "Execute the workflow created in the previous step."]
["18", "graph", "Retrieve the cart to retrieve the subscription details from the `metadata`."],
["30", "", "If the subscription data isn't set in the cart's `metadata`, throw an error"],
["37", "createSubscriptionWorkflow", "Execute the workflow created in the previous step."]
]
```ts title="src/api/store/carts/[id]/subscribe/route.ts" highlights={completeCartHighlights} collapsibleLines="1-10" expandMoreLabel="Show Imports"
@@ -874,6 +877,7 @@ import {
MedusaError,
} from "@medusajs/framework/utils"
import createSubscriptionWorkflow from "../../../../../workflows/create-subscription"
import { SubscriptionInterval } from "../../../../../modules/subscription/types"
export const POST = async (
req: MedusaRequest,
@@ -906,8 +910,8 @@ export const POST = async (
input: {
cart_id: req.params.id,
subscription_data: {
interval: metadata.subscription_interval,
period: metadata.subscription_period,
interval: metadata.subscription_interval as SubscriptionInterval,
period: metadata.subscription_period as number,
},
},
})
@@ -1247,7 +1251,7 @@ export const GET = async (
const {
data: subscriptions,
metadata: { count, take, skip },
metadata: { count, take, skip } = {},
} = await query.graph({
entity: "subscription",
...req.queryConfig,
@@ -1743,7 +1747,7 @@ import { AccountHolderDTO, CustomerDTO, PaymentMethodDTO } from "@medusajs/frame
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
export interface GetPaymentMethodStepInput {
customer: CustomerDTO & {
customer?: CustomerDTO & {
account_holder: AccountHolderDTO
}
}
@@ -1776,7 +1780,7 @@ export const getPaymentMethodStep = createStep(
async ({ customer }: GetPaymentMethodStepInput, { container }) => {
const paymentModuleService = container.resolve(Modules.PAYMENT)
if (!customer.account_holder) {
if (!customer?.account_holder) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"No account holder found for the customer while retrieving payment method"
@@ -1840,7 +1844,7 @@ import { createOrderWorkflow } from "@medusajs/medusa/core-flows"
import { SubscriptionData } from "../../../modules/subscription/types"
import { SUBSCRIPTION_MODULE } from "../../../modules/subscription"
type StepInput = {
export type CreateSubscriptionOrderStepInput = {
subscription: SubscriptionData
cart: CartWorkflowDTO
payment_collection: PaymentCollectionDTO
@@ -1854,7 +1858,7 @@ const createSubscriptionOrderStep = createStep(
"create-subscription-order",
async ({
subscription, cart, payment_collection,
}: StepInput,
}: CreateSubscriptionOrderStepInput,
{ container, context }) => {
const linkDefs: LinkDefinition[] = []
@@ -1873,7 +1877,7 @@ const createSubscriptionOrderStep = createStep(
order,
})
},
async ({ order }, { container }) => {
async (data, { container }) => {
// TODO add compensation function
}
)
@@ -1904,20 +1908,20 @@ function getOrderData(cart: CartWorkflowDTO) {
id: null,
},
items: cart.items,
shipping_methods: cart.shipping_methods.map((method) => ({
shipping_methods: cart.shipping_methods?.map((method) => ({
name: method.name,
amount: method.amount,
is_tax_inclusive: method.is_tax_inclusive,
shipping_option_id: method.shipping_option_id,
data: method.data,
tax_lines: method.tax_lines.map((taxLine) => ({
tax_lines: method.tax_lines?.map((taxLine) => ({
description: taxLine.description,
tax_rate_id: taxLine.tax_rate_id,
code: taxLine.code,
rate: taxLine.rate,
provider_id: taxLine.provider_id,
})),
adjustments: method.adjustments.map((adjustment) => ({
adjustments: method.adjustments?.map((adjustment) => ({
code: adjustment.code,
amount: adjustment.amount,
description: adjustment.description,
@@ -1962,11 +1966,14 @@ This adds links to be created into the `linkDefs` array between the new order an
Finally, replace the `TODO` in the compensation function to cancel the order in case of an error:
```ts title="src/workflows/create-subscription-order/steps/create-subscription-order.ts"
if (!data) {
return
}
const orderModuleService: IOrderModuleService = container.resolve(
Modules.ORDER
)
await orderModuleService.cancel(order.id)
await orderModuleService.cancel(data.order.id)
```
### Create updateSubscriptionStep (Ninth Step)
@@ -2046,7 +2053,7 @@ const updateSubscriptionStep = createStep(
})
},
async ({
prev_data,
data,
}, { container }) => {
// TODO add compensation
}
@@ -2062,15 +2069,18 @@ Before updating the subscription, the step retrieves the old data and passes it
So, replace the `TODO` in the compensation function with the following:
```ts title="src/workflows/create-subscription-order/steps/update-subscription.ts"
if (!data) {
return
}
const subscriptionModuleService: SubscriptionModuleService =
container.resolve(
SUBSCRIPTION_MODULE
)
await subscriptionModuleService.updateSubscriptions({
id: prev_data.id,
last_order_date: prev_data.last_order_date,
next_order_date: prev_data.next_order_date,
id: data.prev_data.id,
last_order_date: data.prev_data.last_order_date,
next_order_date: data.prev_data.next_order_date,
})
```
@@ -2081,19 +2091,19 @@ This updates the subscriptions `last_order_date` and `next_order_date` proper
Finally, create the file `src/workflows/create-subscription-order/index.ts` with the following content:
export const createSubscriptionOrderWorkflowHighlights = [
["26", "useQueryGraphStep", "Retrieve the cart linked to the subscription."],
["63", "createPaymentCollectionsStep", "Create a payment collection using the same information in the cart."],
["67", "getPaymentMethodStep", "Get the customer's saved payment method."],
["80", "data", "Pass data required by Stripe to capture the payment."],
["89", "createPaymentSessionsWorkflow", "Create a payment session in the payment collection from the previous step."],
["93", "authorizePaymentSessionStep", "Authorize the payment session created from the first step."],
["98", "createSubscriptionOrderStep", "Create the new order for the subscription."],
["104", "createRemoteLinkStep", "Create links returned by the previous step."],
["106", "capturePaymentStep", "Capture the orders payment."],
["111", "updateSubscriptionStep", "Update the subscriptions `last_order_date` and `next_order_date`."]
["31", "useQueryGraphStep", "Retrieve the cart linked to the subscription."],
["68", "createPaymentCollectionsStep", "Create a payment collection using the same information in the cart."],
["72", "getPaymentMethodStep", "Get the customer's saved payment method."],
["85", "data", "Pass data required by Stripe to capture the payment."],
["94", "createPaymentSessionsWorkflow", "Create a payment session in the payment collection from the previous step."],
["98", "authorizePaymentSessionStep", "Authorize the payment session created from the first step."],
["103", "createSubscriptionOrderStep", "Create the new order for the subscription."],
["109", "createRemoteLinkStep", "Create links returned by the previous step."],
["111", "capturePaymentStep", "Capture the orders payment."],
["116", "updateSubscriptionStep", "Update the subscriptions `last_order_date` and `next_order_date`."]
]
```ts title="src/workflows/create-subscription-order/index.ts" highlights={createSubscriptionOrderWorkflowHighlights} collapsibleLines="1-18" expandMoreLabel="Show Imports"
```ts title="src/workflows/create-subscription-order/index.ts" highlights={createSubscriptionOrderWorkflowHighlights} collapsibleLines="1-23" expandMoreLabel="Show Imports"
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import {
useQueryGraphStep,
@@ -2108,9 +2118,14 @@ import {
authorizePaymentSessionStep,
createPaymentCollectionsStep,
} from "@medusajs/medusa/core-flows"
import createSubscriptionOrderStep from "./steps/create-subscription-order"
import createSubscriptionOrderStep, {
CreateSubscriptionOrderStepInput,
} from "./steps/create-subscription-order"
import updateSubscriptionStep from "./steps/update-subscription"
import { getPaymentMethodStep } from "./steps/get-payment-method"
import {
getPaymentMethodStep,
GetPaymentMethodStepInput,
} from "./steps/get-payment-method"
type WorkflowInput = {
subscription: SubscriptionData
@@ -2150,9 +2165,9 @@ const createSubscriptionOrderWorkflow = createWorkflow(
}, (data) => {
const cart = data.subscriptions[0].cart
return {
currency_code: cart.currency_code,
amount: cart.payment_collection.amount,
metadata: cart.payment_collection.metadata,
currency_code: cart?.currency_code || "",
amount: cart?.payment_collection?.amount || 0,
metadata: cart?.payment_collection?.metadata || undefined,
}
})
@@ -2172,7 +2187,7 @@ const createSubscriptionOrderWorkflow = createWorkflow(
return {
payment_collection_id: data.payment_collection.id,
provider_id: "pp_stripe_stripe",
customer_id: data.subscriptions[0].cart.customer.id,
customer_id: data.subscriptions[0].cart?.customer?.id,
data: {
payment_method: data.defaultPaymentMethod.id,
off_session: true,
@@ -2195,7 +2210,7 @@ const createSubscriptionOrderWorkflow = createWorkflow(
subscription: input.subscription,
cart: carts[0],
payment_collection,
})
} as unknown as CreateSubscriptionOrderStepInput)
createRemoteLinkStep(linkDefs)

View File

@@ -1542,7 +1542,7 @@ return (
placeholder="Quantity"
type="number"
min="1"
max={selectedVariant?.inventory_quantity}
max={selectedVariant?.inventory_quantity || undefined}
value={quantity}
onChange={(e) => setQuantity(parseInt(e.target.value))}
/>

View File

@@ -138,9 +138,9 @@ const queryClient = new QueryClient({
staleTime: 1000 * 60 * 5, // 5 minutes
},
},
});
})
export default queryClient;
export default queryClient
// product.ts
// Used in components that are children of QueryClientProvider
@@ -149,10 +149,10 @@ import { sdk } from "../lib/sdk"
export const useProduct = (id: string) => {
return useQuery(["product", id], async () => {
const response = await sdk.store.products.retrieve(id);
return response.data;
});
};
const response = await sdk.store.products.retrieve(id)
return response.data
})
}
export const useProductPrice = (id: string) => {
return useQuery(
@@ -160,14 +160,14 @@ export const useProductPrice = (id: string) => {
async () => {
const response = await sdk.store.products.retrieve(id, {
fields: "*variants.calculated_price",
});
return response.data.variants[0].price;
})
return response.data.variants[0].price
},
{
staleTime: 0, // Always fetch fresh data
}
);
};
)
}
```
### Invalidate Queries
@@ -181,19 +181,19 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
export const useAddToCart = () => {
const queryClient = useQueryClient();
const queryClient = useQueryClient()
return useMutation(
async (item) => {
await sdk.store.cart.createLineItem("cart_id", item);
await sdk.store.cart.createLineItem("cart_id", item)
},
{
onSuccess: () => {
queryClient.invalidateQueries(["cart"]);
queryClient.invalidateQueries(["cart"])
},
}
);
};
)
}
```
---
@@ -209,7 +209,7 @@ For example, if you only need the product's `id`, `title`, and `variants.calcula
```ts
const response = await sdk.store.products.retrieve("product_id", {
fields: "*variants.calculated_price, id, title",
});
})
```
This query will return only the specified fields, reducing the response size and improving performance.
@@ -221,11 +221,11 @@ const useProduct = (id: string, fields?: string) => {
return useQuery({
queryKey: ["product", id, fields],
queryFn: async () => {
const response = await sdk.store.products.retrieve(id, { fields });
return response.data;
const response = await sdk.store.products.retrieve(id, { fields })
return response.data
},
});
};
})
}
```
Learn more about the `fields` parameter in the [Store API reference](!api!/store#select-fields-and-relations).
@@ -366,7 +366,7 @@ export const addItemOptimistically = (
// Check if item already exists in cart
const existingItemIndex = currentCart.items?.findIndex(
item => item.variant_id === newItem.variant_id
(item) => item.variant_id === newItem.variant_id
)
let updatedItems: HttpTypes.StoreCartLineItem[]
@@ -459,7 +459,7 @@ export const updateLineItemOptimistically = (
return null
}
const updatedItems = (currentCart.items || []).map(item => {
const updatedItems = (currentCart.items || []).map((item) => {
if (item.id === lineId) {
return {
...item,
@@ -513,7 +513,7 @@ export const removeLineItemOptimistically = (
return null
}
const updatedItems = (currentCart.items || []).filter(item => item.id !== lineId)
const updatedItems = (currentCart.items || []).filter((item) => item.id !== lineId)
const optimisticCart: OptimisticCart = {
...currentCart,
@@ -633,7 +633,7 @@ export const createOptimisticCart = (region: HttpTypes.StoreRegion): OptimisticC
export const getCurrentCart = (queryClient: QueryClient, fields?: string): HttpTypes.StoreCart | null => {
return queryClient.getQueryData<HttpTypes.StoreCart | null>(queryKeys.cart.current(fields)) ||
queryClient.getQueriesData<HttpTypes.StoreCart | null>({
predicate: queryKeys.cart.predicate
predicate: queryKeys.cart.predicate,
})[0]?.[1] || null
}
```
@@ -702,7 +702,7 @@ export const useCart = ({ fields }: { fields?: string } = {}) => {
return useQuery({
queryKey: queryKeys.cart.current(fields),
queryFn: () => retrieveCart({ fields }),
staleTime: 0
staleTime: 0,
})
}
@@ -751,7 +751,7 @@ export const useUpdateCart = () => {
mutationFn: updateCart,
onSuccess: () => {
queryClient.invalidateQueries({ predicate: queryKeys.cart.predicate })
}
},
})
}
@@ -906,7 +906,7 @@ export const useAddToCart = ({ fields }: { fields?: string } = {}) => {
onSettled: (data) => {
// Always refetch after error or success to ensure we have the latest data
queryClient.invalidateQueries({ predicate:
(query) => queryKeys.cart.predicate(query, fields && data ? [fields] : undefined)
(query) => queryKeys.cart.predicate(query, fields && data ? [fields] : undefined),
})
if (data) {
queryClient.setQueryData(queryKeys.cart.current(fields), data)
@@ -970,7 +970,7 @@ export const useUpdateLineItem = ({ fields }: { fields?: string } = {}) => {
onMutate: async (variables) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({
predicate: (query) => queryKeys.cart.predicate(query, fields ? [fields] : undefined)
predicate: (query) => queryKeys.cart.predicate(query, fields ? [fields] : undefined),
})
// Snapshot the previous value
@@ -993,7 +993,7 @@ export const useUpdateLineItem = ({ fields }: { fields?: string } = {}) => {
onSettled: (data) => {
// Always refetch after error or success to ensure we have the latest data
queryClient.invalidateQueries({ predicate:
(query) => queryKeys.cart.predicate(query, fields && data ? [fields] : undefined)
(query) => queryKeys.cart.predicate(query, fields && data ? [fields] : undefined),
})
if (data) {
queryClient.setQueryData(queryKeys.cart.current(fields), data)
@@ -1057,7 +1057,7 @@ export const useDeleteLineItem = ({ fields }: { fields?: string } = {}) => {
onMutate: async (variables) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ predicate:
(query) => queryKeys.cart.predicate(query, fields ? [fields] : undefined)
(query) => queryKeys.cart.predicate(query, fields ? [fields] : undefined),
})
// Snapshot the previous value
@@ -1080,7 +1080,7 @@ export const useDeleteLineItem = ({ fields }: { fields?: string } = {}) => {
onSettled: (data) => {
// Always refetch after error or success to ensure we have the latest data
queryClient.invalidateQueries({ predicate:
(query) => queryKeys.cart.predicate(query, fields && data ? [fields] : undefined)
(query) => queryKeys.cart.predicate(query, fields && data ? [fields] : undefined),
})
if (data) {
queryClient.setQueryData(queryKeys.cart.current(fields), data)

View File

@@ -110,17 +110,17 @@ export const generatedEditDates = {
"app/nextjs-starter/page.mdx": "2025-02-26T11:37:47.137Z",
"app/recipes/b2b/page.mdx": "2025-05-20T07:51:40.718Z",
"app/recipes/commerce-automation/page.mdx": "2025-05-20T07:51:40.719Z",
"app/recipes/digital-products/examples/standard/page.mdx": "2025-04-24T15:41:05.364Z",
"app/recipes/digital-products/examples/standard/page.mdx": "2025-10-21T10:31:35.296Z",
"app/recipes/digital-products/page.mdx": "2025-05-20T07:51:40.719Z",
"app/recipes/ecommerce/page.mdx": "2025-06-24T08:50:10.116Z",
"app/recipes/marketplace/examples/vendors/page.mdx": "2025-07-16T09:58:59.793Z",
"app/recipes/marketplace/examples/vendors/page.mdx": "2025-10-21T10:47:23.415Z",
"app/recipes/marketplace/page.mdx": "2025-05-20T07:51:40.721Z",
"app/recipes/multi-region-store/page.mdx": "2025-05-20T07:51:40.721Z",
"app/recipes/omnichannel/page.mdx": "2025-05-20T07:51:40.722Z",
"app/recipes/oms/page.mdx": "2025-05-20T07:51:40.722Z",
"app/recipes/personalized-products/page.mdx": "2025-07-22T06:52:29.419Z",
"app/recipes/pos/page.mdx": "2025-05-20T07:51:40.723Z",
"app/recipes/subscriptions/examples/standard/page.mdx": "2025-05-20T07:51:40.723Z",
"app/recipes/subscriptions/examples/standard/page.mdx": "2025-10-21T12:36:28.745Z",
"app/recipes/subscriptions/page.mdx": "2025-05-20T07:51:40.723Z",
"app/recipes/page.mdx": "2025-05-20T07:51:40.722Z",
"app/service-factory-reference/methods/create/page.mdx": "2025-09-01T15:54:53.385Z",
@@ -568,7 +568,7 @@ export const generatedEditDates = {
"app/medusa-cli/commands/start/page.mdx": "2025-10-13T10:27:25.345Z",
"app/medusa-cli/commands/telemtry/page.mdx": "2025-01-16T09:51:24.323Z",
"app/medusa-cli/commands/user/page.mdx": "2025-09-01T15:36:38.978Z",
"app/recipes/marketplace/examples/restaurant-delivery/page.mdx": "2025-08-25T07:30:37.299Z",
"app/recipes/marketplace/examples/restaurant-delivery/page.mdx": "2025-10-21T12:04:46.051Z",
"references/types/HttpTypes/interfaces/types.HttpTypes.AdminCreateCustomerGroup/page.mdx": "2024-12-09T13:21:33.569Z",
"references/types/HttpTypes/interfaces/types.HttpTypes.AdminCreateReservation/page.mdx": "2025-04-11T09:04:47.498Z",
"references/types/HttpTypes/interfaces/types.HttpTypes.AdminCustomerGroup/page.mdx": "2025-05-20T07:51:41.059Z",
@@ -5520,7 +5520,7 @@ export const generatedEditDates = {
"references/workflows/classes/workflows.WorkflowResponse/page.mdx": "2025-04-11T09:04:53.140Z",
"references/workflows/interfaces/workflows.ApplyStepOptions/page.mdx": "2025-09-12T14:10:46.875Z",
"references/workflows/types/workflows.WorkflowData/page.mdx": "2024-12-23T13:57:08.059Z",
"app/integrations/guides/resend/page.mdx": "2025-08-01T12:09:08.483Z",
"app/integrations/guides/resend/page.mdx": "2025-10-21T11:08:19.352Z",
"references/api_key_models/variables/api_key_models.ApiKey/page.mdx": "2025-10-21T08:10:52.876Z",
"references/cart/ICartModuleService/methods/cart.ICartModuleService.updateShippingMethods/page.mdx": "2025-10-21T08:10:43.389Z",
"references/cart/interfaces/cart.UpdateShippingMethodDTO/page.mdx": "2025-06-25T10:11:35.052Z",
@@ -5560,7 +5560,7 @@ export const generatedEditDates = {
"references/modules/sales_channel_models/page.mdx": "2024-12-10T14:55:13.205Z",
"references/types/DmlTypes/types/types.DmlTypes.KnownDataTypes/page.mdx": "2024-12-17T16:57:19.922Z",
"references/types/DmlTypes/types/types.DmlTypes.RelationshipTypes/page.mdx": "2024-12-10T14:54:55.435Z",
"app/recipes/commerce-automation/restock-notification/page.mdx": "2025-09-29T15:35:49.320Z",
"app/recipes/commerce-automation/restock-notification/page.mdx": "2025-10-21T12:06:58.024Z",
"app/integrations/guides/shipstation/page.mdx": "2025-05-20T07:51:40.717Z",
"app/nextjs-starter/guides/customize-stripe/page.mdx": "2025-07-15T08:50:51.997Z",
"references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.listShippingOptionsForCartWithPricingWorkflow/page.mdx": "2025-09-18T17:04:38.644Z",
@@ -5722,7 +5722,7 @@ export const generatedEditDates = {
"references/types/StockLocationTypes/interfaces/types.StockLocationTypes.FilterableStockLocationAddressProps/page.mdx": "2025-01-13T18:05:55.410Z",
"references/types/StockLocationTypes/types/types.StockLocationTypes.UpdateStockLocationAddressInput/page.mdx": "2025-01-07T12:54:23.057Z",
"references/types/StockLocationTypes/types/types.StockLocationTypes.UpsertStockLocationAddressInput/page.mdx": "2025-01-07T12:54:23.058Z",
"app/storefront-development/guides/express-checkout/page.mdx": "2025-05-20T07:51:40.724Z",
"app/storefront-development/guides/express-checkout/page.mdx": "2025-10-21T10:35:36.273Z",
"app/commerce-modules/inventory/inventory-kit/page.mdx": "2025-05-20T07:51:40.708Z",
"app/commerce-modules/api-key/workflows/page.mdx": "2025-01-09T13:41:46.573Z",
"app/commerce-modules/api-key/js-sdk/page.mdx": "2025-01-09T12:04:39.787Z",
@@ -6604,7 +6604,7 @@ export const generatedEditDates = {
"references/core_flows/Locking/Steps_Locking/variables/core_flows.Locking.Steps_Locking.acquireLockStepId/page.mdx": "2025-09-15T09:52:14.218Z",
"references/core_flows/Locking/Steps_Locking/variables/core_flows.Locking.Steps_Locking.releaseLockStepId/page.mdx": "2025-09-15T09:52:14.219Z",
"references/core_flows/Locking/core_flows.Locking.Steps_Locking/page.mdx": "2025-09-15T09:52:14.217Z",
"app/integrations/guides/meilisearch/page.mdx": "2025-10-09T11:30:25.084Z",
"app/integrations/guides/meilisearch/page.mdx": "2025-10-21T10:51:27.358Z",
"app/nextjs-starter/guides/storefront-returns/page.mdx": "2025-09-22T06:02:00.580Z",
"references/js_sdk/admin/Admin/properties/js_sdk.admin.Admin.views/page.mdx": "2025-10-21T08:10:55.384Z",
"app/data-model-repository-reference/methods/create/page.mdx": "2025-10-09T11:42:23.826Z",