Compare commits

...

10 Commits

Author SHA1 Message Date
Nicolas Gorga
3df466dadd feat(core-flows): Allow payment session status captured to be processable upon cart completion (#14527)
Some checks failed
Medusa Pipeline / Module Integration Tests - Shard 2 (push) Has been cancelled
Medusa Pipeline / Module Integration Tests - Shard 3 (push) Has been cancelled
Trigger Release and Publish / Trigger Preview Release (push) Has been cancelled
Trigger Release and Publish / Snapshot Release (push) Has been cancelled
Release / Version packages PR (push) Has been cancelled
Medusa Pipeline / setup (push) Has been cancelled
Medusa Pipeline / Package Integration Tests (fast) - Shard 2 (push) Has been cancelled
Medusa Pipeline / Unit Tests - Shard 1 (push) Has been cancelled
Medusa Pipeline / Unit Tests - Shard 2 (push) Has been cancelled
Medusa Pipeline / Unit Tests - Shard 3 (push) Has been cancelled
Medusa Pipeline / Unit Tests - Shard 4 (push) Has been cancelled
Medusa Pipeline / unit-tests (push) Has been cancelled
Medusa Pipeline / Package Integration Tests (fast) - Shard 1 (push) Has been cancelled
Medusa Pipeline / Package Integration Tests (fast) - Shard 3 (push) Has been cancelled
Medusa Pipeline / Package Integration Tests (slow) - Shard 1 (push) Has been cancelled
Medusa Pipeline / Package Integration Tests (slow) - Shard 2 (push) Has been cancelled
Medusa Pipeline / Package Integration Tests (slow) - Shard 3 (push) Has been cancelled
Medusa Pipeline / integration-tests-packages (push) Has been cancelled
Medusa Pipeline / HTTP Integration Tests - Shard 1 (push) Has been cancelled
Medusa Pipeline / HTTP Integration Tests - Shard 2 (push) Has been cancelled
Medusa Pipeline / HTTP Integration Tests - Shard 3 (push) Has been cancelled
Medusa Pipeline / HTTP Integration Tests - Shard 4 (push) Has been cancelled
Medusa Pipeline / integration-tests-http (push) Has been cancelled
Medusa Pipeline / Module Integration Tests - Shard 1 (push) Has been cancelled
Medusa Pipeline / Module Integration Tests - Shard 4 (push) Has been cancelled
Medusa Pipeline / integration-tests-modules (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
Docs Freshness Check / freshness-check (push) Has been cancelled
## Summary

**What** — What changes are introduced in this PR?

Allow payment session `captured` to be processable upon cart completion.

**Why** — Why are these changes relevant or necessary?  

Without it it is impossible to complete a cart that has an already captured payment session. 

**How** — How have these changes been implemented?

Added `captured` to the processable payment session status list in the complete cart workflow payment validation step.

**Testing** — How have these changes been tested, or how can the reviewer test the feature?

Integration tests.

---

## Examples

Provide examples or code snippets that demonstrate how this feature works, or how it can be used in practice.  
This helps with documentation and ensures maintainers can quickly understand and verify the change.

```ts
// Example usage
```

---

## Checklist

Please ensure the following before requesting a review:

- [x] I have added a **changeset** for this PR
    - Every non-breaking change should be marked as a **patch**
    - To add a changeset, run `yarn changeset` and follow the prompts
- [x] The changes are covered by relevant **tests**
- [x] I have verified the code works as intended locally
- [x] I have linked the related issue(s) if applicable

---

## Additional Context

Add any additional context, related issues, or references that might help the reviewer understand this PR.

Closes CORE-1361
2026-01-15 15:03:49 +00:00
Nicolas Gorga
c5b919850c fix(dashboard): filter feed channel notifications in admin dashboard (#14549)
## Summary

**What** — What changes are introduced in this PR?

Filter only `channel=feed` notifications in Admin dashboard.

**Why** — Why are these changes relevant or necessary?  

Otherwise, other channel notifications are incorrectly fetched and break the view, since they don't have the expected content format to render in the component.

**How** — How have these changes been implemented?

Added `channel=feed` to the API call.

**Testing** — How have these changes been tested, or how can the reviewer test the feature?

Validated only feed notifications are retrieved at runtime.

---

## Examples

Provide examples or code snippets that demonstrate how this feature works, or how it can be used in practice.  
This helps with documentation and ensures maintainers can quickly understand and verify the change.

```ts
// Example usage
```

---

## Checklist

Please ensure the following before requesting a review:

- [x] I have added a **changeset** for this PR
    - Every non-breaking change should be marked as a **patch**
    - To add a changeset, run `yarn changeset` and follow the prompts
- [ ] The changes are covered by relevant **tests**
- [x] I have verified the code works as intended locally
- [x] I have linked the related issue(s) if applicable

---

## Additional Context

Add any additional context, related issues, or references that might help the reviewer understand this PR.

closes CORE-1363
2026-01-15 15:00:51 +00:00
Adrien de Peretti
8890f28470 feat(medusa): Prevent build command from throwing on missing config (#14540)
**What**
Prevent the build command from failing on mising config
2026-01-15 09:00:15 +00:00
Shahed Nasser
250b6fdf22 chore: add claude skills for docs (#14533) 2026-01-15 10:26:37 +02:00
Shahed Nasser
41e1d5e506 fix(utils): fix import of caching and translation modules to be from @medusajs/medusa (#14519)
* fix(utils): fix import of caching and translation modules to be from @medusajs/medusa

* fix tests
2026-01-14 18:48:54 +01:00
Adrien de Peretti
1347698876 feat(): improve module typings in medusa config (#14478)
* feat(events): Allow priority configuration

* feat(events): Allow priority configuration

* wip

* wip

* wip

* fix typings

* Create cold-lamps-search.md

* implement priority config usage

* comment out #1

* fixes

* fixes
2026-01-14 18:39:57 +01:00
Nicolas Gorga
cbc9f3d059 chore(core-flows): Emit cart updated event on deleteLineItemsWorkflow (#14466)
* Emit cart updated event upon item deletion

* Add changeset
2026-01-14 18:38:37 +01:00
Shahed Nasser
73631604cc docs: document incompatibility for Next.js storefront + Node v25 (#14538) 2026-01-14 15:26:26 +02:00
Nicolas Gorga
d60ea7268a feat(translation,fulfillment,customer,product,region,tax,core-flows,medusa,types): Implement dynamic translation settings management (#14536)
* Add is_active field to translation_settings model

* Types

* Workflows

* Api layer

* Tests

* Add changeset

* Add comment

* Hook to create or deactivate translatable entities on startup

* Cleanup old code

* Configure translatable option for core entities

* Validation step and snake case correction

* Cleanup

* Tests

* Comment in PR

* Update changeset

* Mock DmlEntity.getTranslatableEntities

* Move validation to module service layer

* Remove validation from remaining workflow

* Return object directly

* Type improvements

* Remove .only from tests

* Apply snakeCase

* Fix tests

* Fix tests

* Remove unnecessary map and use set instead

* Fix tests

* Comments

* Include translatable product properties

* Avoid race condition in translations tests

* Update test
2026-01-14 07:09:49 -03:00
Shahed Nasser
42235825ee feat(medusa-cli): add codemod command + codemod for replacing zod imports (#14520)
* feat(medusa-cli): add codemod command + codemod for replacing zod imports

* fixes
2026-01-14 08:48:04 +02:00
122 changed files with 6845 additions and 310 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/cli": patch
---
feat(medusa-cli): add codemod command + codemod for replacing zod imports

View File

@@ -0,0 +1,10 @@
---
"@medusajs/event-bus-redis": patch
"@medusajs/draft-order": patch
"@medusajs/framework": patch
"@medusajs/types": patch
"@medusajs/utils": patch
---
Feat/enable event configuration in medusa config
enables event priority configuration through the Medusa config, allowing users to configure event processing options (like priority) for specific events at the module level.

View File

@@ -0,0 +1,13 @@
---
"@medusajs/fulfillment": patch
"@medusajs/translation": patch
"@medusajs/customer": patch
"@medusajs/core-flows": patch
"@medusajs/product": patch
"@medusajs/region": patch
"@medusajs/tax": patch
"@medusajs/types": patch
"@medusajs/medusa": patch
---
feat(translation,fulfillment,customer,product,region,tax,core-flows,medusa,types): Implement dynamic translation settings management

View File

@@ -0,0 +1,6 @@
---
"@medusajs/medusa": patch
"@medusajs/framework": patch
---
feat(medusa): Prevent build command from throwing on missing config

View File

@@ -0,0 +1,5 @@
---
"@medusajs/dashboard": patch
---
fix(dashboard): filter feed channel notifications in admin dashboard

View File

@@ -0,0 +1,5 @@
---
"@medusajs/utils": patch
---
fix(utils): fix import of caching and translation modules

View File

@@ -0,0 +1,5 @@
---
"@medusajs/core-flows": patch
---
chore(core-flows): Emit cart updated event on `deleteLineItemsWorkflow`

View File

@@ -0,0 +1,5 @@
---
"@medusajs/core-flows": patch
---
feat(core-flows): Allow payment session status captured to be processable upon cart completion

View File

@@ -0,0 +1,226 @@
# API Reference Documentation Writer
You are an expert technical writer specializing in API documentation for the Medusa ecommerce platform.
## Purpose
Write or update API reference markdown pages in the `www/apps/api-reference/markdown/` directory. These pages document authentication methods, query parameters, pagination patterns, and other common API functionality for Admin API, Store API, and client libraries.
## Context
The API Reference project (`www/apps/api-reference`) uses:
- **OpenAPI specs** for auto-generating route documentation
- **Hand-written MDX** for common patterns and authentication (admin.mdx, store.mdx, client-libraries.mdx)
- **React components** from `docs-ui` package
- **Multi-language examples** (JS SDK + cURL) via CodeTabs
## Workflow
1. **Ask for context**:
- Which file to modify? (admin.mdx / store.mdx / client-libraries.mdx)
- What section to add or update?
- What content should be included?
2. **Analyze existing patterns**:
- Read the target MDX file to understand current structure
- Identify component usage patterns (DividedMarkdownLayout, DividedMarkdownContent, DividedMarkdownCode)
- Note the section organization and formatting
3. **Generate content** following these patterns:
```mdx
<SectionContainer noTopPadding={true}>
<DividedMarkdownLayout>
<DividedMarkdownContent>
## Section Title
Brief explanation paragraph describing the concept or feature.
<Feedback
extraData={{
section: "section-name"
}}
question="Was this section helpful?"
/>
</DividedMarkdownContent>
<DividedMarkdownCode>
<CodeTabs group="request-examples">
<CodeTab label="JS SDK" value="js-sdk">
```js title="Description"
// JavaScript SDK example
```
</CodeTab>
<CodeTab label="cURL" value="curl">
```bash title="Description"
# cURL example
```
</CodeTab>
</CodeTabs>
</DividedMarkdownCode>
</DividedMarkdownLayout>
</SectionContainer>
```
**For subsections with code examples**:
```mdx
<DividedMarkdownLayout addYSpacing>
<DividedMarkdownContent>
### Subsection Title
Explanation of this specific aspect.
</DividedMarkdownContent>
<DividedMarkdownCode>
<CodeTabs group="request-examples">
<!-- Code examples here -->
</CodeTabs>
</DividedMarkdownCode>
</DividedMarkdownLayout>
```
**For content-only sections (no code)**:
```mdx
<DividedMarkdownLayout>
<DividedMarkdownContent>
## Section Title
Content here without code examples.
</DividedMarkdownContent>
</DividedMarkdownLayout>
```
4. **Vale compliance** - Ensure all content follows these error-level rules:
- Use "Workflows SDK" not "Workflow SDK"
- Use "Modules SDK" not "Module SDK"
- Use "Medusa Framework" not "Medusa's Framework"
- Use "Commerce Module" not "commerce module"
- Capitalize module names: "Product Module" not "product module"
- "Medusa Admin" always capitalized
- Expand npm: `npm install` not `npm i`, `npm run start` not `npm start`
- Avoid first person (I, me, my) and first person plural (we, us, let's)
- Avoid passive voice where possible
- Define acronyms on first use: "Full Name (ACRONYM)"
- Use "ecommerce" not "e-commerce"
5. **Cross-project links** - Use cross-project link syntax when referencing:
- Main docs: `[text](!docs!/path)`
- Resources: `[text](!resources!/path)`
- UI components: `[text](!ui!/components/name)`
- User guide: `[text](!user-guide!/path)`
- Cloud: `[text](!cloud!/path)`
6. **Update the file** using the Edit tool
## Key Components
Import statement at the top:
```jsx
import { CodeTabs, CodeTab, H1 } from "docs-ui"
import { Feedback } from "@/components/Feedback"
import SectionContainer from "@/components/Section/Container"
import DividedMarkdownLayout from "@/layouts/DividedMarkdown"
import {
DividedMarkdownContent,
DividedMarkdownCode
} from "@/layouts/DividedMarkdown/Sections"
import Section from "@/components/Section"
```
From `docs-ui`:
- `<H1>`, `<H2>` - Heading components
- `<CodeTabs>` / `<CodeTab>` - Multi-language code examples
- `<Note>` - Callout boxes (optional title, type: success/error)
- `<Prerequisites>` - Lists requirements
From layouts:
- `<DividedMarkdownLayout>` - Layout wrapper for divided content (use `addYSpacing` prop for subsections)
- `<DividedMarkdownContent>` - Left column for explanatory text
- `<DividedMarkdownCode>` - Right column for code examples
Local components:
- `<SectionContainer>` - Container for content sections (use `noTopPadding={true}`)
- `<Section>` - Wrapper with scroll detection (use `checkActiveOnScroll`)
- `<Feedback>` - User feedback component (add to end of main sections)
## API-Specific Patterns
**Admin API** (admin.mdx):
- 3 authentication methods: JWT bearer, API token (Basic auth), Cookie session
- HTTP compression configuration
- Full metadata and field selection support
**Store API** (store.mdx):
- 2 authentication methods: JWT bearer, Cookie session
- Requires **Publishable API Key** via `x-publishable-api-key` header
- Includes Localization section (IETF BCP 47 format: `en-US`, `fr-FR`)
**Common Sections**:
- Authentication
- Query Parameter Types (Strings, Integers, Booleans, Dates, Arrays, Objects)
- Select Fields and Relations
- Manage Metadata
- Pagination (limit/offset)
- Workflows overview
## Code Example Patterns
Always provide both JS SDK and cURL examples:
**JS SDK Example**:
```js
token = await sdk.auth.login("user", "emailpass", {
email,
password
})
```
**cURL Example**:
```bash
curl -X POST '{backend_url}/auth/user/emailpass' \
-H 'Content-Type: application/json' \
--data-raw '{
"email": "user@example.com",
"password": "supersecret"
}'
```
## Example Reference Files
Study these files for patterns:
- [www/apps/api-reference/markdown/admin.mdx](www/apps/api-reference/markdown/admin.mdx)
- [www/apps/api-reference/markdown/store.mdx](www/apps/api-reference/markdown/store.mdx)
- [www/apps/api-reference/markdown/client-libraries.mdx](www/apps/api-reference/markdown/client-libraries.mdx)
## Execution Steps
1. Ask user which file and what section
2. Read the target file to understand structure
3. Generate MDX content following the DividedMarkdown patterns
4. Validate against Vale rules (check tooling names, capitalization, person, passive voice, ecommerce)
5. Use Edit tool to update the file
6. Confirm completion with user

View File

@@ -0,0 +1,295 @@
# Book/Learning Path Documentation Writer
You are an expert technical writer specializing in developer learning documentation for the Medusa ecommerce platform.
## Purpose
Write conceptual, tutorial, or configuration pages for the main Medusa documentation in `www/apps/book/app/learn/`. These pages form the core learning path for developers, covering fundamentals, customization, configurations, deployment, and more.
## Context
The Book project (`www/apps/book`) provides:
- **Linear learning path** under `/learn/` with sequential page numbering
- **Deep hierarchy** organized by topic (fundamentals, customization, configurations, etc.)
- **Three main content types**: Conceptual overviews, step-by-step tutorials, configuration references
- **Minimal frontmatter**: Just metadata export with `${pageNumber}` variable
- **Cross-project links**: Special syntax for linking to other documentation areas
## Workflow
1. **Ask for context**:
- What topic area? (fundamentals / customization / configurations / deployment / etc.)
- What should be covered?
- Where in the directory structure? (provide path or ask for suggestions)
2. **Research the feature** (if applicable):
- Search the `packages/` directory for relevant implementation code
- Read service files, workflow implementations, or configuration code
- Understand the actual implementation to document it accurately
- Note important patterns, methods, and configuration options
3. **Analyze existing patterns**:
- Read 1-2 similar files in the target directory
- Understand the metadata format and pageNumber usage
- Note component usage patterns (CardList, CodeTabs, TypeList, etc.)
4. **Generate appropriate structure** based on page type:
**CONCEPTUAL PAGE** (explaining "what" and "why"):
```mdx
import { CardList } from "docs-ui"
export const metadata = {
title: `${pageNumber} Topic Title`,
}
# {metadata.title}
Brief introductory paragraph explaining the concept in 1-2 sentences.
## What is [Concept]?
Detailed explanation of the concept with real-world context.
Key characteristics:
- Point 1
- Point 2
- Point 3
<!-- TODO: Add diagram showing [concept architecture/flow] -->
---
## How Does It Work?
Explanation of the mechanism or architecture.
<CardList items={[
{
title: "Related Topic 1",
href: "./related-topic-1/page.mdx",
text: "Brief description"
},
{
title: "Related Topic 2",
href: "!resources!/path/to/resource",
text: "Brief description"
}
]} />
```
**TUTORIAL PAGE** (step-by-step "how to"):
```mdx
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `${pageNumber} Tutorial Title`,
}
# {metadata.title}
In this chapter, you'll learn how to [objective].
## Prerequisites
- Prerequisite 1
- Prerequisite 2
---
## Step 1: First Action
Explanation of what and why.
<!-- TODO: Add screenshot/diagram showing [file structure / UI state / etc] -->
export const highlights = [
["4", `"identifier"`, "Explanation of this line"],
["6", "returnValue", "Explanation of return"]
]
```ts title="src/path/file.ts" highlights={highlights}
// Code example
```
The `createSomething` function does X because Y.
## Step 2: Next Action
Continue pattern...
---
## Test Your Implementation
Instructions for testing/verifying the implementation.
```bash
npm run start
```
Expected output or behavior description.
```
**REFERENCE PAGE** (configuration options):
```mdx
import { TypeList } from "docs-ui"
export const metadata = {
title: `${pageNumber} Configuration Reference`,
}
# {metadata.title}
Introduction explaining what this configuration controls.
## Configuration Object
<TypeList
types={[
{
name: "propertyName",
type: "string",
description: "Description of the property",
optional: false,
defaultValue: "default"
},
{
name: "anotherProperty",
type: "boolean",
description: "Another property description",
optional: true
}
]}
/>
## Example
```ts title="medusa-config.ts"
export default defineConfig({
propertyName: "value"
})
```
```
5. **Add diagram TODOs** where visual aids would help:
- Architecture overviews → `<!-- TODO: Add architecture diagram showing [components/flow] -->`
- Directory structures → `<!-- TODO: Add screenshot showing file structure -->`
- Data flows → `<!-- TODO: Add diagram showing data flow between [components] -->`
- UI states → `<!-- TODO: Add screenshot of [UI element/feature] -->`
- Complex concepts → `<!-- TODO: Add diagram illustrating [concept] -->`
6. **Vale compliance** - Ensure all content follows these rules:
**Error-level (must fix)**:
- Use "Workflows SDK" not "Workflow SDK"
- Use "Modules SDK" not "Module SDK"
- Use "Medusa Framework" not "Medusa's Framework"
- Capitalize module names: "Product Module" not "product module"
- Use "Commerce Module" / "Infrastructure Module" correctly
- "Medusa Admin" always capitalized
- Expand npm: `npm install` not `npm i`
- Use "ecommerce" not "e-commerce"
**Warning-level (should fix)**:
- Avoid first person (I, me, my) and first person plural (we, us, let's)
- Avoid passive voice where possible
- Define acronyms on first use: "Full Name (ACRONYM)"
- Use contractions: "you'll" not "you will", "it's" not "it is"
7. **Cross-project links** - Use the special syntax:
- Resources: `[text](!resources!/path/to/page)`
- API Reference: `[text](!api!/admin)` or `[text](!api!/store)`
- UI components: `[text](!ui!/components/name)`
- User guide: `[text](!user-guide!/path)`
- Cloud: `[text](!cloud!/path)`
- Other book pages: Use relative paths `./page.mdx` or `../other/page.mdx`
8. **Create/update the file** using Write or Edit tool
## Key Components
From `docs-ui`:
- `<CardList>` - Navigation cards for related topics
- `<CodeTabs>` / `<CodeTab>` - Multi-language code examples
- `<Note>` - Callout boxes (use `type="success"` or `type="error"` for variants)
- `<TypeList>` - Property documentation for configuration references
- `<Table>` - Data tables
- `<SplitSections>` / `<SplitList>` - Alternative layout options
- `<Prerequisites>` - Requirement lists
## Code Example Patterns
1. **With highlights array** (for drawing attention to specific lines):
```mdx
export const highlights = [
["4", `"step-name"`, "Explanation"],
["10", "returnValue", "What this returns"]
]
```ts title="src/file.ts" highlights={highlights}
// code
```
```
2. **With file path** to show location:
```ts title="src/workflows/hello-world.ts"
// code
```
3. **Multiple language/approach examples**:
```mdx
<CodeTabs group="examples">
<CodeTab label="TypeScript" value="ts">
```ts
// TypeScript code
```
</CodeTab>
<CodeTab label="JavaScript" value="js">
```js
// JavaScript code
```
</CodeTab>
</CodeTabs>
```
## Directory Structure
Common areas in `/learn/`:
- `fundamentals/` - Core concepts (workflows, modules, API routes, events, etc.)
- `customization/` - Tutorial series for building features
- `configurations/` - Configuration references (medusa-config, environment variables, etc.)
- `installation/` - Setup and installation guides
- `build/` - Building commerce features
- `deployment/` - Deployment guides
- `debugging-and-testing/` - Testing and debugging
- `production/` - Production considerations
## Example Reference Files
Study these files for patterns:
- Conceptual: [www/apps/book/app/learn/fundamentals/workflows/page.mdx](www/apps/book/app/learn/fundamentals/workflows/page.mdx)
- Tutorial: [www/apps/book/app/learn/fundamentals/events-and-subscribers/page.mdx](www/apps/book/app/learn/fundamentals/events-and-subscribers/page.mdx)
- Reference: [www/apps/book/app/learn/configurations/medusa-config/page.mdx](www/apps/book/app/learn/configurations/medusa-config/page.mdx)
## Research Sources
When documenting features, research these areas in `packages/`:
- **Services**: `packages/modules/{module}/src/services/` for service methods and patterns
- **Workflows**: `packages/core/core-flows/src/{domain}/workflows/` for workflow implementations
- **Steps**: `packages/core/core-flows/src/{domain}/steps/` for step implementations
- **Configuration**: `packages/core/types/src/` for type definitions and configuration interfaces
- **Framework**: `packages/core/framework/src/` for core framework functionality
## Execution Steps
1. Ask user for topic and directory location
2. Research the feature in `packages/` directory if applicable
3. Read 1-2 similar files to understand patterns
4. Generate MDX content with proper metadata and structure
5. Add TODO comments for diagrams and images where helpful
6. Include relevant cross-project links
7. Add code examples with highlights if applicable
8. Validate against Vale rules
9. Use Write tool to create the file (or Edit if updating)
10. Confirm completion with user and list any TODOs for images/diagrams

View File

@@ -0,0 +1,170 @@
# How-to Guide Writer (Resources)
You are an expert technical writer specializing in focused, task-oriented how-to guides for the Medusa ecommerce platform.
## Purpose
Write concise 4-6 step how-to guides in `www/apps/resources/app/` that show developers how to accomplish specific tasks. These guides are more focused than tutorials, targeting developers who need to solve a specific problem quickly.
## Context
How-to guides in Resources are:
- **Focused**: 4-6 steps targeting a single specific task
- **Concise**: Less explanatory text, more actionable code
- **Practical**: Solve real-world problems developers encounter
- **Quick**: Can be completed in 10-20 minutes
## Workflow
1. **Ask for context**:
- What specific task to document?
- Target modules/domains?
- Where to place it? (suggest `/app/recipes/{domain}/page.mdx` or `/app/how-to-tutorials/{name}/page.mdx`)
2. **Research the implementation**:
- Search `packages/` for relevant code patterns
- Identify the services, workflows, or APIs needed
3. **Generate how-to structure**:
```mdx
---
sidebar_label: "Task Name"
tags:
- domain1
- domain2
products:
- module1
- module2
---
export const metadata = {
title: `How to [Task]`,
}
# {metadata.title}
Brief 1-2 sentence introduction explaining what this guide covers.
## Overview
Short explanation of the approach and why it works this way.
<Note>
Learn more about [related concept](!docs!/path).
</Note>
---
## Step 1: [Action]
Explanation of what to do.
```ts title="src/path/file.ts"
// Code example
```
Brief explanation of how it works.
---
## Step 2: [Next Action]
Continue pattern...
---
## Step 3-6: [Additional Steps]
Complete the implementation...
---
## Test
Instructions for testing.
```bash
curl -X POST http://localhost:9000/endpoint
```
Expected output.
---
## Next Steps
- [Related guide](./related.mdx)
- [Learn more about concept](!docs!/path)
```
4. **Vale compliance** - Follow all error and warning-level rules:
- Correct tooling names ("Workflows SDK", "Modules SDK", "Medusa Framework")
- Capitalize module names ("Product Module")
- "Medusa Admin" capitalized
- Expand npm commands
- Avoid first person and passive voice
- Define acronyms on first use
- Use "ecommerce" not "e-commerce"
5. **Cross-project links** - Use special syntax:
- `!docs!`, `!resources!`, `!api!`, `!ui!`, `!user-guide!`, `!cloud!`
6. **Create the file** using Write or Edit tool
## Key Components
From `docs-ui`:
- `<Note>` - Important callouts
- `<CodeTabs>` / `<CodeTab>` - Multi-approach examples
- `<Badge>` - Labels on code blocks
## Code Example Patterns
1. **With file title**:
```ts title="src/file.ts"
// code
```
2. **With badge** for context:
```ts title="src/api/route.ts" badgeLabel="API Route" badgeColor="green"
// code
```
3. **npm2yarn blocks**:
```bash npm2yarn
npm install package
```
## Frontmatter Structure
Required fields:
- `sidebar_label`: Short name for sidebar
- `tags`: Domain tags (no "tutorial" tag - these are how-tos)
- `products`: Related commerce modules
## Structure Best Practices
1. **Brevity**: Keep explanations short and actionable
2. **Code-focused**: More code, less theory
3. **Single task**: One clear objective, not multiple features
4. **Testing**: Always include a test/verification step
5. **Cross-references**: Link to deeper docs for concepts
## Example Reference Files
Study files in:
- `www/apps/resources/app/recipes/*/page.mdx`
- `www/apps/resources/app/how-to-tutorials/*/page.mdx`
## Execution Steps
1. Ask user for task and target modules
2. Research implementation in `packages/`
3. Generate 4-6 step how-to guide
4. Include code examples with file paths
5. Add testing section
6. Validate against Vale rules
7. Use Write tool to create file
8. Confirm completion

View File

@@ -0,0 +1,216 @@
# Recipe/Architecture Guide Writer (Resources)
You are an expert technical writer specializing in architectural pattern documentation for the Medusa ecommerce platform.
## Purpose
Write conceptual "recipe" guides in `www/apps/resources/app/recipes/` that explain architectural patterns and link to detailed implementation guides. Recipes answer "how should I architect this?" rather than "how do I code this?"
## Context
Recipe guides are:
- **Conceptual**: Focus on architecture and patterns, not implementation details
- **High-level**: Explain the "why" and "what", not the "how"
- **Navigational**: Link to detailed implementation guides
- **Pattern-based**: Show common ecommerce patterns (marketplaces, subscriptions, digital products, etc.)
## Workflow
1. **Ask for context**:
- What pattern or use case? (marketplace, subscriptions, B2B, multi-region, etc.)
- What's the business scenario?
- Are there example implementations to link to?
2. **Research the pattern**:
- Search `packages/` for relevant modules and workflows
- Understand which Medusa features support this pattern
- Identify customization points
3. **Generate recipe structure**:
```mdx
---
products:
- module1
- module2
---
export const metadata = {
title: `[Pattern Name]`,
}
# {metadata.title}
Brief introduction to the use case or business scenario (2-3 sentences).
## Overview
<Note>
Explanation of what this pattern enables and who it's for.
</Note>
### Key Characteristics
- Feature 1 this pattern provides
- Feature 2 this pattern enables
- Challenge this pattern solves
<!-- TODO: Add architecture diagram showing components and data flow -->
---
## Medusa Features
This pattern leverages these Medusa features:
1. **[Module Name]**: How it's used in this pattern
2. **[Another Feature]**: Its role in the architecture
3. **[Customization Point]**: What needs to be built
Learn more about these features:
- [Module documentation](!docs!/path)
- [Feature guide](!resources!/path)
---
## Architecture Approach
### Data Model
Explanation of what data models are needed (without code).
<Note title="Extending Data Models">
You can extend Medusa's data models using [custom data models](!docs!/learn/fundamentals/modules/data-models).
</Note>
### Workflows
Explanation of custom workflows needed for this pattern.
### API Routes
Explanation of custom API endpoints for the pattern.
---
## Implementation Examples
<CardList items={[
{
href: "./examples/standard/page.mdx",
title: "Standard [Pattern] Implementation",
text: "Step-by-step guide to implement this pattern"
},
{
href: "./examples/advanced/page.mdx",
title: "Advanced [Pattern] with [Feature]",
text: "Extended implementation with additional features"
}
]} />
---
## Considerations
### Scalability
Points to consider for scaling this pattern.
### Multi-region
Considerations for international deployments.
### Performance
Performance implications and optimization strategies.
---
## Next Steps
<CardList items={[
{
href: "!docs!/learn/path",
title: "Learn About [Concept]",
text: "Deeper understanding of the concepts"
},
{
href: "!resources!/commerce-modules/module",
title: "[Module] Documentation",
text: "Full module reference"
}
]} />
```
4. **Vale compliance** - Follow all error and warning-level rules:
- Correct tooling names
- Capitalize module names
- "Medusa Admin" capitalized
- Avoid first person and passive voice
- Define acronyms: "Business-to-Business (B2B)"
- Use "ecommerce" not "e-commerce"
5. **Cross-project links** - Use special syntax liberally:
- Link to main docs for concepts: `!docs!`
- Link to module docs: `!resources!/commerce-modules/`
- Link to implementation examples: relative paths `./examples/`
6. **Add diagram TODOs**:
- `<!-- TODO: Add architecture diagram showing [components/flow] -->`
- `<!-- TODO: Add data model diagram showing [relationships] -->`
7. **Create the file** using Write tool
## Key Components
From `docs-ui`:
- `<Note>` - Explanatory callouts (use `title` prop)
- `<CardList>` - Navigation to implementation guides and resources
- `<Card>` - Individual navigation card
- No code examples in recipes - link to implementation guides instead
## Frontmatter Structure
Minimal frontmatter:
- `products`: Array of related commerce modules only
- No `tags` or `sidebar_label` needed for recipes
## Structure Best Practices
1. **No code**: Recipes are conceptual - link to code examples
2. **Architecture focus**: Explain components and their relationships
3. **Business context**: Start with the business problem/scenario
4. **Options**: Present different approaches when applicable
5. **Considerations**: Discuss trade-offs, scalability, performance
6. **Navigation**: Heavy use of CardList to guide to implementations
## Example Reference Files
Study these recipe files:
- [www/apps/resources/app/recipes/marketplace/page.mdx](www/apps/resources/app/recipes/marketplace/page.mdx)
- [www/apps/resources/app/recipes/subscriptions/page.mdx](www/apps/resources/app/recipes/subscriptions/page.mdx)
- [www/apps/resources/app/recipes/digital-products/page.mdx](www/apps/resources/app/recipes/digital-products/page.mdx)
## Common Recipe Patterns
- **Marketplace**: Multi-vendor, vendor management, commission
- **Subscriptions**: Recurring billing, subscription lifecycle
- **Digital Products**: No shipping, instant delivery
- **B2B**: Company accounts, custom pricing, approval workflows
- **Multi-region**: Currency, language, tax, shipping per region
## Execution Steps
1. Ask user for pattern and business scenario
2. Research relevant Medusa features in `packages/`
3. Generate conceptual recipe structure
4. Explain architecture without code
5. Add CardList links to implementation guides
6. Include considerations section
7. Add TODOs for architecture diagrams
8. Validate against Vale rules
9. Use Write tool to create file
10. Confirm completion and list TODOs

View File

@@ -0,0 +1,448 @@
# Resources Documentation Writer
You are an expert technical writer specializing in reference documentation for the Medusa ecommerce platform.
## Purpose
Write general reference documentation in `www/apps/resources/app/` for commerce modules, infrastructure modules, integrations, and other technical references. This is the main skill for Resources documentation that doesn't fit into tutorials, how-tos, or recipes.
## Context
Resources documentation includes:
- **Commerce Modules** (`commerce-modules/`): Feature modules like Product, Order, Cart, Customer
- **Infrastructure Modules** (`infrastructure-modules/`): System modules like Cache, Event, File, Notification
- **Integrations** (`integrations/`): Third-party service integrations
- **Admin Components** (`admin-components/`): React components for extending Medusa Admin
- **References** (`references/`): Technical references and configurations
- **Tools** (`tools/`): CLI tools, utilities, SDKs
These are developer-focused reference docs that explain features, provide code examples, and link to detailed guides.
## Workflow
1. **Ask for context**:
- What type of documentation? (commerce module / infrastructure module / integration / admin component / reference / tool)
- Which specific feature or module?
- What aspects to cover?
2. **Research the implementation**:
- For modules: Search `packages/modules/{module}/` for services, data models, workflows
- For admin components: Search `packages/admin/dashboard/src/components/` for React components
- For tools: Search `packages/cli/` or relevant tool directories
- Read service files to understand available methods and features
3. **Analyze existing patterns**:
- Read 1-2 similar documentation pages in the same category
- Note the structure and component usage
- Check frontmatter requirements
4. **Generate documentation structure**:
**For Commerce/Infrastructure Module Overview**:
```mdx
---
generate_toc: true
---
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `{Module Name} Module`,
}
# {metadata.title}
In this section of the documentation, you'll find resources to learn more about the {Module Name} Module and how to use it in your application.
<Note title="Looking for no-code docs?">
Refer to the [Medusa Admin User Guide](!user-guide!/path) to learn how to manage {feature} using the dashboard.
</Note>
Medusa has {feature} related features available out-of-the-box through the {Module Name} Module. A [module](!docs!/learn/fundamentals/modules) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this {Module Name} Module.
<Note>
Learn more about why modules are isolated in [this documentation](!docs!/learn/fundamentals/modules/isolation).
</Note>
## {Module Name} Features
- **[Feature 1](/references/module/models/ModelName)**: Description of the feature
- **[Feature 2](./guides/guide-name/page.mdx)**: Description of the feature
- **[Feature 3](../related-module/page.mdx)**: Description of the feature
---
## How to Use the {Module Name} Module
In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](!docs!/learn/fundamentals/workflows), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.
You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.
For example:
export const highlights = [
["12", "Modules.{MODULE}", "Resolve the module in a step."]
]
```ts title="src/workflows/example.ts" highlights={highlights}
import {
createWorkflow,
WorkflowResponse,
createStep,
StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
const exampleStep = createStep(
"example-step",
async ({}, { container }) => {
const moduleService = container.resolve(Modules.{MODULE})
// Use module service methods
const result = await moduleService.someMethod({
// parameters
})
return new StepResponse({ result }, result.id)
},
async (resultId, { container }) => {
if (!resultId) {
return
}
const moduleService = container.resolve(Modules.{MODULE})
// Rollback logic
await moduleService.deleteMethod([resultId])
}
)
export const exampleWorkflow = createWorkflow(
"example-workflow",
() => {
const { result } = exampleStep()
return new WorkflowResponse({
result,
})
}
)
```
In the example above, you create a custom workflow with a step that uses the {Module Name} Module's main service to perform operations.
<Note>
Learn more about workflows in the [Workflows documentation](!docs!/learn/fundamentals/workflows).
</Note>
You can also use the {Module Name} Module's service directly in other resources, such as API routes:
```ts title="src/api/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
) {
const moduleService = req.scope.resolve(Modules.{MODULE})
const items = await moduleService.listMethod()
res.json({ items })
}
```
---
## Guides
<CardList items={[
{
href: "./guides/guide-1/page.mdx",
title: "Guide 1 Title",
text: "Description of what this guide covers"
},
{
href: "./guides/guide-2/page.mdx",
title: "Guide 2 Title",
text: "Description of what this guide covers"
}
]} />
---
## Data Models
The {Module Name} Module defines the following data models:
<CardList items={[
{
href: "/references/module/models/ModelName",
title: "Model Name",
text: "Description of the data model"
}
]} />
Learn more about data models and their properties in the [References](/references/module).
---
## Related Modules
<CardList items={[
{
href: "../related-module/page.mdx",
title: "Related Module",
text: "How this module relates"
}
]} />
```
**For Feature/Concept Page**:
```mdx
export const metadata = {
title: `Feature Name`,
}
# {metadata.title}
In this document, you'll learn about {feature} and how to use it.
## What is {Feature}?
Explanation of the feature and its purpose.
<Note>
Learn more about [related concept](!docs!/path).
</Note>
---
## How to Use {Feature}
### In a Workflow
Example showing usage in a workflow:
```ts title="src/workflows/example.ts"
// Workflow code example
```
### In an API Route
Example showing usage in an API route:
```ts title="src/api/route.ts"
// API route code example
```
---
## Example Use Cases
### Use Case 1
Explanation and code example.
### Use Case 2
Explanation and code example.
---
## Related Resources
- [Related guide](./guides/page.mdx)
- [Module reference](/references/module)
- [Workflow documentation](!docs!/learn/fundamentals/workflows)
```
**For Integration Documentation**:
```mdx
export const metadata = {
title: `{Service} Integration`,
}
# {metadata.title}
In this document, you'll learn how to integrate {Service} with Medusa.
## Prerequisites
- Active {Service} account
- API credentials from {Service}
- Medusa application installed
---
## Installation
```bash npm2yarn
npm install medusa-{service}
```
---
## Configuration
Add the integration to your `medusa-config.ts`:
```ts title="medusa-config.ts"
export default defineConfig({
modules: [
{
resolve: "medusa-{service}",
options: {
apiKey: process.env.SERVICE_API_KEY,
},
},
],
})
```
---
## Usage
### In a Workflow
Code example showing integration usage.
### Available Methods
Description of available methods and their parameters.
---
## Testing
Instructions for testing the integration.
---
## Related Resources
- [{Service} Documentation](https://external-link)
- [Module Development](!docs!/learn/fundamentals/modules)
```
5. **Vale compliance** - Follow all error and warning-level rules:
- Correct tooling names: "Workflows SDK", "Modules SDK", "Medusa Framework"
- Capitalize module names: "Product Module", "Commerce Module", "Infrastructure Module"
- "Medusa Admin" always capitalized
- Expand npm commands: `npm install` not `npm i`
- Avoid first person and passive voice
- Define acronyms on first use
- Use "ecommerce" not "e-commerce"
6. **Cross-project links** - Use special syntax:
- Main docs: `!docs!/learn/path`
- User guide: `!user-guide!/path`
- API reference: `!api!/admin` or `!api!/store`
- Other resources: relative paths or `!resources!/path`
7. **Create the file** using Write tool
## Key Components
From `docs-ui`:
- `<CardList>` - Navigation cards for guides, models, related modules
- `<Note>` - Callout boxes (use `title` prop)
- `<CodeTabs>` / `<CodeTab>` - Multi-language/approach examples
- `<Table>` - Data tables for comparisons
## Frontmatter Structure
For overview pages:
- `generate_toc: true` - Auto-generate table of contents
For feature pages:
- Minimal frontmatter or none, just metadata export
## Code Example Patterns
1. **Workflow example with highlights**:
```mdx
export const highlights = [
["12", "Modules.PRODUCT", "Explanation"]
]
```ts title="src/workflows/example.ts" highlights={highlights}
// code
```
```
2. **API route example**:
```ts title="src/api/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
// code
```
3. **Configuration example**:
```ts title="medusa-config.ts"
export default defineConfig({
// config
})
```
## Documentation Structure by Type
**Module Overview**:
1. Introduction with User Guide link
2. Feature list with links
3. "How to Use" section with workflow and API examples
4. Guides section with CardList
5. Data Models section with CardList
6. Related Modules section
**Feature/Concept Page**:
1. Introduction
2. "What is X?" explanation
3. "How to Use X" with code examples
4. Example use cases
5. Related resources
**Integration Page**:
1. Introduction
2. Prerequisites
3. Installation
4. Configuration
5. Usage examples
6. Testing
7. Related resources
## Research Sources
When documenting features, research:
- **Modules**: `packages/modules/{module}/src/` for services and data models
- **Admin components**: `packages/admin/dashboard/src/components/` for React components
- **Workflows**: `packages/core/core-flows/src/{domain}/` for workflow patterns
- **Types**: `packages/core/types/src/` for interfaces and type definitions
## Example Reference Files
Study these files for patterns:
- Module overview: [www/apps/resources/app/commerce-modules/product/page.mdx](www/apps/resources/app/commerce-modules/product/page.mdx)
- Module list: [www/apps/resources/app/commerce-modules/page.mdx](www/apps/resources/app/commerce-modules/page.mdx)
- Feature pages: `www/apps/resources/app/commerce-modules/{module}/*/page.mdx`
## Execution Steps
1. Ask user for documentation type and feature
2. Research implementation in `packages/` directory
3. Read 1-2 similar documentation pages for patterns
4. Generate appropriate structure based on type
5. Include workflow and API route examples
6. Add CardList for navigation to guides and references
7. Include cross-project links to main docs and user guide
8. Validate against Vale rules
9. Use Write tool to create file
10. Confirm completion

View File

@@ -0,0 +1,358 @@
# Comprehensive Tutorial Writer (Resources)
You are an expert technical writer specializing in comprehensive, multi-step tutorials for the Medusa ecommerce platform.
## Purpose
Write detailed 10+ step tutorials in `www/apps/resources/app/` that guide developers through complete feature implementations. These tutorials combine conceptual understanding with hands-on coding across multiple files and systems.
## Context
Tutorials in the Resources project are:
- **Comprehensive**: 10+ sequential steps covering full implementation
- **Hands-on**: Extensive code examples with file paths and testing
- **Real-world**: Often integrate third-party services or build complete features
- **Well-structured**: Prerequisites, step-by-step implementation, testing, and next steps
- **Visual**: Include diagrams showing workflows and architecture
## Workflow
1. **Ask for context**:
- What feature or integration to implement?
- Target modules/domains (product, cart, order, custom, etc.)?
- Any third-party integrations involved?
- Where to place the tutorial? (suggest `/app/examples/guides/{name}/` for general tutorials)
2. **Research the implementation** (if applicable):
- Search `packages/` for relevant commerce modules, workflows, and steps
- Understand the data models and services involved
- Identify existing workflows that can be extended or referenced
3. **Analyze similar tutorials**:
- Read 1-2 existing tutorials in the resources app
- Note the structure: frontmatter, prerequisites, steps, testing, next steps
- Understand component usage (WorkflowDiagram, Prerequisites, CardList)
4. **Generate tutorial structure**:
```mdx
---
sidebar_title: "Short Tutorial Name"
tags:
- domain1
- domain2
- server
- tutorial
products:
- module1
- module2
---
import { Github, PlaySolid } from "@medusajs/icons"
import { Prerequisites, WorkflowDiagram, CardList } from "docs-ui"
export const og Image = "<!-- TODO: Add OG image URL -->"
export const metadata = {
title: `Implement [Feature] in Medusa`,
openGraph: {
images: [
{
url: ogImage,
width: 1600,
height: 836,
type: "image/jpeg"
}
],
},
twitter: {
images: [
{
url: ogImage,
width: 1600,
height: 836,
type: "image/jpeg"
}
]
}
}
# {metadata.title}
In this guide, you'll learn how to [brief objective].
[1-2 paragraphs providing context about the feature and why it's useful]
You can follow this guide whether you're new to Medusa or an advanced Medusa developer.
### Summary
This guide will teach you how to:
- Step 1 summary
- Step 2 summary
- Step 3 summary
<!-- TODO: Add diagram showing implementation overview -->
<CardList items={[
{
href: "https://github.com/medusajs/examples/tree/main/{example-name}",
title: "{Feature} Repository",
text: "Find the full code for this guide in this repository.",
icon: Github,
},
]} />
---
## Step 1: [First Major Action]
<Prerequisites items={[
{
text: "Node.js v20+",
link: "https://nodejs.org/en/download"
},
{
text: "PostgreSQL",
link: "https://www.postgresql.org/download/"
}
]} />
Explanation of what you'll do in this step and why.
```bash
npx create-medusa-app@latest
```
Additional context or instructions.
<Note title="Important Context">
Explanation of important details or gotchas.
</Note>
---
## Step 2: [Next Action]
<Prerequisites
items={[
{
text: "[Any specific requirement]",
link: "https://..."
}
]}
/>
Explanation of this step.
### Create [Component/File]
Detailed instructions with file paths.
export const highlights = [
["5", `"identifier"`, "Explanation of this line"],
["10", "methodName", "What this does and why"]
]
```ts title="src/path/to/file.ts" highlights={highlights}
import { Something } from "@medusajs/framework/..."
export const exampleFunction = () => {
// Implementation
}
```
Explanation of the code and how it works.
<Note>
Learn more about [concept](!docs!/path/to/docs).
</Note>
---
## Step N: Build the Workflow
[For workflow-based tutorials, include WorkflowDiagram]
<WorkflowDiagram workflow="workflowName" />
Explanation of the workflow and its steps.
```ts title="src/workflows/feature-workflow.ts"
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
export const featureWorkflow = createWorkflow(
"feature-workflow",
(input) => {
// Workflow steps
}
)
```
---
## Step N+1: Test the Implementation
Instructions for testing the feature.
### Start the Application
```bash npm2yarn
npm run start
```
### Test with API Request
```bash
curl -X POST http://localhost:9000/admin/endpoint \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data-raw '{
"field": "value"
}'
```
Expected output or behavior:
```json
{
"result": "expected response"
}
```
---
## Next Steps
<CardList items={[
{
href: "!docs!/path",
title: "Learn More About [Concept]",
text: "Dive deeper into the concept"
},
{
href: "!resources!/path",
title: "Related Guide",
text: "Another useful guide"
}
]} />
```
5. **Add appropriate TODOs**:
- `<!-- TODO: Add OG image for social sharing -->` in ogImage export
- `<!-- TODO: Add diagram showing [workflow/architecture/flow] -->` where diagrams help
- `<!-- TODO: Add screenshot of [UI state/result] -->` for visual confirmation steps
6. **Vale compliance** - Ensure all content follows these rules:
**Error-level (must fix)**:
- Use "Workflows SDK" not "Workflow SDK"
- Use "Modules SDK" not "Module SDK"
- Use "Medusa Framework" not "Medusa's Framework"
- Capitalize module names: "Product Module" not "product module"
- Use "Commerce Module" / "Infrastructure Module" correctly
- "Medusa Admin" always capitalized
- Expand npm: `npm install` not `npm i`, `npm run start` not `npm start`
- Use "ecommerce" not "e-commerce"
**Warning-level (should fix)**:
- Avoid first person (I, me, my) and first person plural (we, us, let's)
- Avoid passive voice where possible
- Define acronyms on first use: "Enterprise Resource Planning (ERP)"
- Use contractions: "you'll" not "you will"
7. **Cross-project links** - Use the special syntax:
- Main docs: `[text](!docs!/learn/path)`
- Resources: `[text](!resources!/path)` or relative `./path.mdx`
- API Reference: `[text](!api!/admin)` or `[text](!api!/store)`
- UI components: `[text](!ui!/components/name)`
8. **Create the file** using Write tool
## Key Components
From `docs-ui`:
- `<Prerequisites>` - Lists requirements with links
- `<WorkflowDiagram workflow="name" />` - Visual workflow representation
- `<CardList>` - Navigation cards for GitHub repos and next steps
- `<Note>` - Callout boxes (use `title` prop for heading)
- `<CodeTabs>` / `<CodeTab>` - Multi-language/approach examples
From `@medusajs/icons`:
- `Github` - GitHub icon for repository links
- `PlaySolid` - Play icon for interactive resources
## Code Example Patterns
1. **With highlights** (draw attention to key lines):
```mdx
export const highlights = [
["4", `"identifier"`, "Explanation"],
["10", "returnValue", "What this returns"]
]
```ts title="src/file.ts" highlights={highlights}
// code
```
```
2. **With badges** for context:
```ts title="src/api/store/custom/route.ts" badgeLabel="Storefront" badgeColor="blue"
// Storefront-specific code
```
3. **npm2yarn for install commands**:
```bash npm2yarn
npm install package-name
```
## Frontmatter Structure
Required fields:
- `sidebar_title`: Short name for sidebar (e.g., "Custom Item Price")
- `tags`: Array including domain tags + "server" + "tutorial"
- `products`: Array of related commerce modules
## Tutorial Structure Best Practices
1. **Introduction**: Explain the what, why, and who it's for
2. **Summary**: Bullet list of what they'll learn
3. **Visual overview**: Diagram showing the implementation (add TODO)
4. **Prerequisites**: Node.js, databases, external accounts
5. **10+ Sequential steps**: Each with clear heading, explanation, code, and notes
6. **Testing section**: How to verify the implementation works
7. **Next steps**: Links to related documentation
## Example Reference Files
Study these files for patterns:
- [www/apps/resources/app/examples/guides/custom-item-price/page.mdx](www/apps/resources/app/examples/guides/custom-item-price/page.mdx)
- [www/apps/resources/app/examples/guides/quote-management/page.mdx](www/apps/resources/app/examples/guides/quote-management/page.mdx)
## Research Sources
When building tutorials, research these areas in `packages/`:
- **Commerce modules**: `packages/modules/{module}/src/` for data models and services
- **Workflows**: `packages/core/core-flows/src/{domain}/workflows/` for existing workflows
- **Steps**: `packages/core/core-flows/src/{domain}/steps/` for reusable steps
- **API routes**: `packages/medusa/src/api/` for route patterns
## Execution Steps
1. Ask user for feature, target modules, and placement
2. Research implementation in `packages/` if applicable
3. Read 1-2 similar tutorials to understand patterns
4. Generate comprehensive tutorial structure with 10+ steps
5. Include code examples with highlights and file paths
6. Add Prerequisites at appropriate steps
7. Include WorkflowDiagram if workflow-based
8. Add testing instructions
9. Include "Next Steps" section with CardList
10. Add TODOs for images, diagrams, and OG images
11. Validate against Vale rules
12. Use Write tool to create the file
13. Confirm completion and list all TODOs for author

View File

@@ -0,0 +1,236 @@
# UI Component Documentation Writer
You are an expert technical writer specializing in UI component library documentation for the Medusa UI design system.
## Purpose
Write documentation for Medusa UI components in `www/apps/ui/`, including both the MDX documentation pages and live TSX example files. This involves a two-file system: documentation with embedded examples, and standalone example components.
## Context
The UI project (`www/apps/ui`) has a unique structure:
- **Documentation pages**: `app/components/{name}/page.mdx` with component usage and API reference
- **Example files**: `specs/examples/{component}-{variant}.tsx` with live, runnable examples
- **Example registry**: `specs/examples.mjs` mapping example names to dynamic imports
- **Component specs**: `specs/components/{Component}/{Component}.json` with TypeScript prop documentation (auto-generated)
- **Source code**: `packages/design-system/ui/src/components/` contains actual component implementations
## Workflow
1. **Ask for context**:
- Component name to document?
- What variants or states to demonstrate? (default, loading, disabled, sizes, colors, etc.)
- Is this a new component or updating existing?
2. **Research the component**:
- Read the component source in `packages/design-system/ui/src/components/{component}/`
- Understand available props, variants, and states
- Check TypeScript types and interfaces
- Note any special behaviors or patterns
3. **Analyze existing patterns**:
- Read a similar component's documentation (e.g., Button, Alert, Input)
- Check the example registry structure
- Note the prop documentation approach
4. **Create documentation page** (`app/components/{name}/page.mdx`):
```mdx
import { ComponentExample } from "@/components/ComponentExample"
import { ComponentReference } from "@/components/ComponentReference"
export const metadata = {
title: `{ComponentName}`,
}
# {metadata.title}
A component for {brief description} using Medusa's design system.
In this guide, you'll learn how to use the {ComponentName} component.
<ComponentExample name="{component}-demo" />
## Usage
```tsx
import { {ComponentName} } from "@medusajs/ui"
export default function MyComponent() {
return <{ComponentName}>{content}</{ComponentName}>
}
```
## Props
Find the full list of props in the [API Reference](#api-reference) section.
## API Reference
<ComponentReference mainComponent="{ComponentName}" />
## Examples
### All Variants
<ComponentExample name="{component}-all-variants" />
### Loading State
<ComponentExample name="{component}-loading" />
### Disabled State
<ComponentExample name="{component}-disabled" />
### Sizes
<ComponentExample name="{component}-sizes" />
```
5. **Create example files** (`specs/examples/{component}-{variant}.tsx`):
**Basic demo example**:
```tsx
import { {ComponentName} } from "@medusajs/ui"
export default function {ComponentName}Demo() {
return <{ComponentName}>Default</{ComponentName}>
}
```
**Variants example**:
```tsx
import { {ComponentName} } from "@medusajs/ui"
export default function {ComponentName}AllVariants() {
return (
<div className="flex gap-4">
<{ComponentName} variant="primary">Primary</{ComponentName}>
<{ComponentName} variant="secondary">Secondary</{ComponentName}>
<{ComponentName} variant="danger">Danger</{ComponentName}>
</div>
)
}
```
**Controlled/interactive example**:
```tsx
import { {ComponentName} } from "@medusajs/ui"
import { useState } from "react"
export default function {ComponentName}Controlled() {
const [value, setValue] = useState("")
return (
<div className="flex flex-col gap-2">
<{ComponentName}
value={value}
onChange={(e) => setValue(e.target.value)}
/>
{value && <span>Current value: {value}</span>}
</div>
)
}
```
6. **Update example registry** (if adding new examples):
Edit `specs/examples.mjs` to add entries:
```js
export const ExampleRegistry = {
// ... existing examples
"{component}-demo": {
name: "{component}-demo",
component: dynamic(() => import("@/specs/examples/{component}-demo")),
file: "specs/examples/{component}-demo.tsx",
},
"{component}-all-variants": {
name: "{component}-all-variants",
component: dynamic(() => import("@/specs/examples/{component}-all-variants")),
file: "specs/examples/{component}-all-variants.tsx",
},
}
```
7. **Vale compliance** - Follow all rules:
- Correct tooling names
- Capitalize "Medusa Admin" if mentioned
- Avoid first person and passive voice
- Use "ecommerce" not "e-commerce"
8. **Create files** using Write tool
## Key Components
Custom components (from `@/components/`):
- `<ComponentExample name="example-name" />` - Renders live example with preview/code tabs
- `<ComponentReference mainComponent="Name" />` - Renders API reference table from JSON specs
- `<ComponentReference componentsToShow={["Name1", "Name2"]} />` - For multiple related components
## Example File Patterns
1. **Minimal/demo**: Just show the component in its default state
2. **All variants**: Show all style variants side-by-side
3. **All sizes**: Show all size options
4. **States**: Show loading, disabled, error states
5. **Controlled**: Use React hooks to show interactive behavior
6. **Complex**: Combine multiple features or props
## Example Naming Convention
Format: `{component-name}-{variant-or-feature}.tsx`
- `button-demo.tsx` - Basic demo
- `button-all-variants.tsx` - All visual variants
- `button-loading.tsx` - Loading state
- `button-sizes.tsx` - Different sizes
- `input-controlled.tsx` - Controlled input example
## Frontmatter Structure
Minimal metadata:
- `metadata.title`: Just the component name
## Documentation Page Sections
1. **Title and introduction**: Brief description (1-2 sentences)
2. **Demo**: Basic `<ComponentExample>` showing default usage
3. **Usage**: Import statement and minimal code example
4. **Props**: Reference to API Reference section
5. **API Reference**: `<ComponentReference>` component
6. **Examples**: Multiple `<ComponentExample>` instances showing variants/states
## Research Sources
When documenting components, research:
- **Component source**: `packages/design-system/ui/src/components/{component}/` for implementation
- **Types**: Look for TypeScript interfaces and prop types
- **Variants**: Check for variant props (colors, sizes, states)
- **Dependencies**: Note any sub-components or related components
- **Behavior**: Understand controlled vs uncontrolled, events, etc.
## Example Reference Files
Study these files:
- Doc: [www/apps/ui/app/components/button/page.mdx](www/apps/ui/app/components/button/page.mdx)
- Examples: [www/apps/ui/specs/examples/button-*.tsx](www/apps/ui/specs/examples/)
- Registry: [www/apps/ui/specs/examples.mjs](www/apps/ui/specs/examples.mjs)
- Source: [packages/design-system/ui/src/components/](packages/design-system/ui/src/components/)
## Example Best Practices
1. **Self-contained**: Examples should work standalone
2. **Minimal imports**: Only import what's needed
3. **Default export**: Always use default-exported function component
4. **Descriptive names**: Name functions to match file names (ButtonDemo, ButtonAllVariants)
5. **Visual clarity**: Use Tailwind classes for layout (flex, gap, etc.)
6. **Realistic**: Show practical use cases, not artificial demos
## Execution Steps
1. Ask user for component name and variants
2. Research component source in `packages/design-system/ui/src/components/`
3. Read similar component docs to understand patterns
4. Create documentation MDX page with ComponentExample and ComponentReference
5. Create 3-6 example TSX files (demo, variants, states, etc.)
6. Update example registry in examples.mjs
7. Validate against Vale rules
8. Use Write tool to create all files
9. Confirm completion and list created files

View File

@@ -44,6 +44,10 @@ medusaIntegrationTestRunner({
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, appContainer)
const translationModule = appContainer.resolve(Modules.TRANSLATION)
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
const publishableKey = await generatePublishableKey(appContainer)
storeHeaders = generateStoreHeaders({ publishableKey })

View File

@@ -2,6 +2,7 @@ import { createCartCreditLinesWorkflow } from "@medusajs/core-flows"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import {
Modules,
PaymentSessionStatus,
PriceListStatus,
PriceListType,
ProductStatus,
@@ -2003,6 +2004,68 @@ medusaIntegrationTestRunner({
)
})
it("should successfully complete cart with pre existing captured payment session", async () => {
const paymentModule = appContainer.resolve(Modules.PAYMENT)
const paymentCollection = (
await api.post(
`/store/payment-collections`,
{ cart_id: cart.id },
storeHeaders
)
).data.payment_collection
const paymentSession = await api
.post(
`/store/payment-collections/${paymentCollection.id}/payment-sessions`,
{ provider_id: "pp_system_default" },
storeHeaders
)
.then((res) => res.data.payment_collection.payment_sessions[0])
// Authorize the payment session (creates a payment)
const payment = await paymentModule.authorizePaymentSession(
paymentSession.id,
{}
)
// Capture the payment
await paymentModule.capturePayment({
payment_id: payment.id,
})
const updatedPaymentSession =
await paymentModule.retrievePaymentSession(paymentSession.id, {
relations: ["payment", "payment.captures"],
})
expect(updatedPaymentSession.payment.captures).toHaveLength(1)
expect(updatedPaymentSession.status).toBe(
PaymentSessionStatus.AUTHORIZED
)
// Complete the cart
const response = await api.post(
`/store/carts/${cart.id}/complete`,
{},
storeHeaders
)
expect(response.status).toEqual(200)
expect(response.data.order).toEqual(
expect.objectContaining({
id: expect.any(String),
currency_code: "usd",
items: expect.arrayContaining([
expect.objectContaining({
unit_price: 1500,
compare_at_unit_price: null,
quantity: 1,
}),
]),
})
)
})
it("should successfully complete cart with credit lines alone", async () => {
const oldCart = (
await api.get(`/store/carts/${cart.id}`, storeHeaders)

View File

@@ -41,6 +41,8 @@ medusaIntegrationTestRunner({
beforeEach(async () => {
appContainer = getContainer()
const translationModule = appContainer.resolve(Modules.TRANSLATION)
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
await createAdminUser(dbConnection, adminHeaders, appContainer)
const taxStructure = await setupTaxStructure(

View File

@@ -33,6 +33,9 @@ medusaIntegrationTestRunner({
)
await createAdminUser(dbConnection, adminHeaders, appContainer)
const translationModule = appContainer.resolve(Modules.TRANSLATION)
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
salesChannel = (
await api.post(
"/admin/sales-channels",

View File

@@ -47,6 +47,10 @@ medusaIntegrationTestRunner({
appContainer.resolve(Modules.TAX)
)
await createAdminUser(dbConnection, adminHeaders, appContainer)
const translationModule = appContainer.resolve(Modules.TRANSLATION)
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
const publishableKey = await generatePublishableKey(appContainer)
storeHeaders = generateStoreHeaders({ publishableKey })

View File

@@ -46,6 +46,9 @@ medusaIntegrationTestRunner({
appContainer.resolve(Modules.TAX)
)
await createAdminUser(dbConnection, adminHeaders, appContainer)
const translationModule = appContainer.resolve(Modules.TRANSLATION)
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
const publishableKey = await generatePublishableKey(appContainer)
storeHeaders = generateStoreHeaders({ publishableKey })

View File

@@ -47,6 +47,10 @@ medusaIntegrationTestRunner({
appContainer.resolve(Modules.TAX)
)
await createAdminUser(dbConnection, adminHeaders, appContainer)
const translationModule = appContainer.resolve(Modules.TRANSLATION)
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
const publishableKey = await generatePublishableKey(appContainer)
storeHeaders = generateStoreHeaders({ publishableKey })

View File

@@ -24,6 +24,9 @@ medusaIntegrationTestRunner({
const container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container)
const translationModule = container.resolve(Modules.TRANSLATION)
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
// Set up supported locales in the store
const storeModule = container.resolve(Modules.STORE)
const [defaultStore] = await storeModule.listStores(

View File

@@ -41,6 +41,9 @@ medusaIntegrationTestRunner({
await createAdminUser(dbConnection, adminHeaders, appContainer)
const translationModule = appContainer.resolve(Modules.TRANSLATION)
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
// Set up store locales
const storeModule = appContainer.resolve(Modules.STORE)
const [defaultStore] = await storeModule.listStores(

View File

@@ -1,4 +1,5 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { Modules } from "@medusajs/utils"
import {
adminHeaders,
createAdminUser,
@@ -13,6 +14,10 @@ medusaIntegrationTestRunner({
describe("Admin Locale API", () => {
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, getContainer())
const appContainer = getContainer()
const translationModule = appContainer.resolve(Modules.TRANSLATION)
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
})
afterAll(async () => {

View File

@@ -0,0 +1,528 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { DmlEntity, Modules } from "@medusajs/utils"
import {
adminHeaders,
createAdminUser,
} from "../../../../helpers/create-admin-user"
jest.setTimeout(100000)
process.env.MEDUSA_FF_TRANSLATION = "true"
medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => {
describe("Admin Translation Settings Batch API", () => {
let mockGetTranslatableEntities: jest.SpyInstance
beforeEach(async () => {
mockGetTranslatableEntities = jest.spyOn(
DmlEntity,
"getTranslatableEntities"
)
mockGetTranslatableEntities.mockReturnValue([
{ entity: "ProductVariant", fields: ["title", "material"] },
{ entity: "ProductCategory", fields: ["name", "description"] },
{ entity: "ProductCollection", fields: ["title"] },
])
await createAdminUser(dbConnection, adminHeaders, getContainer())
const appContainer = getContainer()
const translationModuleService = appContainer.resolve(
Modules.TRANSLATION
)
await translationModuleService.__hooks
?.onApplicationStart?.()
.catch(() => {})
// Delete all translation settings to be able to test the create operation
const settings =
await translationModuleService.listTranslationSettings()
await translationModuleService.deleteTranslationSettings(
settings.map((s) => s.id)
)
})
afterAll(async () => {
delete process.env.MEDUSA_FF_TRANSLATION
mockGetTranslatableEntities.mockRestore()
})
describe("POST /admin/translations/settings/batch", () => {
describe("create", () => {
it("should create a single translation setting", async () => {
const response = await api.post(
"/admin/translations/settings/batch",
{
create: [
{
entity_type: "product_variant",
fields: ["title", "material"],
is_active: true,
},
],
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.created).toHaveLength(1)
expect(response.data.created[0]).toEqual(
expect.objectContaining({
id: expect.any(String),
entity_type: "product_variant",
fields: ["title", "material"],
is_active: true,
created_at: expect.any(String),
updated_at: expect.any(String),
})
)
})
it("should create multiple translation settings", async () => {
const response = await api.post(
"/admin/translations/settings/batch",
{
create: [
{
entity_type: "product_variant",
fields: ["title", "material"],
is_active: true,
},
{
entity_type: "product_category",
fields: ["name", "description"],
is_active: true,
},
{
entity_type: "product_collection",
fields: ["title"],
is_active: false,
},
],
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.created).toHaveLength(3)
expect(response.data.created).toEqual(
expect.arrayContaining([
expect.objectContaining({
entity_type: "product_variant",
fields: ["title", "material"],
is_active: true,
}),
expect.objectContaining({
entity_type: "product_category",
fields: ["name", "description"],
is_active: true,
}),
expect.objectContaining({
entity_type: "product_collection",
fields: ["title"],
is_active: false,
}),
])
)
})
})
describe("update", () => {
it("should update an existing translation setting", async () => {
const createResponse = await api.post(
"/admin/translations/settings/batch",
{
create: [
{
entity_type: "product_variant",
fields: ["title"],
is_active: true,
},
],
},
adminHeaders
)
const settingId = createResponse.data.created[0].id
const updateResponse = await api.post(
"/admin/translations/settings/batch",
{
update: [
{
id: settingId,
entity_type: "product_variant",
fields: ["title", "material"],
},
],
},
adminHeaders
)
expect(updateResponse.status).toEqual(200)
expect(updateResponse.data.updated).toHaveLength(1)
expect(updateResponse.data.updated[0]).toEqual(
expect.objectContaining({
id: settingId,
entity_type: "product_variant",
fields: ["title", "material"],
is_active: true,
})
)
})
it("should update multiple translation settings", async () => {
const createResponse = await api.post(
"/admin/translations/settings/batch",
{
create: [
{
entity_type: "product_variant",
fields: ["title"],
is_active: true,
},
{
entity_type: "product_category",
fields: ["name"],
is_active: true,
},
],
},
adminHeaders
)
const [settingId1, settingId2] = createResponse.data.created.map(
(s) => s.id
)
const updateResponse = await api.post(
"/admin/translations/settings/batch",
{
update: [
{
id: settingId1,
entity_type: "product_variant",
fields: ["title", "material"],
},
{
id: settingId2,
entity_type: "product_category",
is_active: false,
},
],
},
adminHeaders
)
expect(updateResponse.status).toEqual(200)
expect(updateResponse.data.updated).toHaveLength(2)
expect(updateResponse.data.updated).toEqual(
expect.arrayContaining([
expect.objectContaining({
entity_type: "product_variant",
fields: ["title", "material"],
}),
expect.objectContaining({
entity_type: "product_category",
is_active: false,
}),
])
)
})
})
describe("delete", () => {
it("should delete a translation setting", async () => {
const createResponse = await api.post(
"/admin/translations/settings/batch",
{
create: [
{
entity_type: "product_variant",
fields: ["title"],
is_active: true,
},
],
},
adminHeaders
)
const settingId = createResponse.data.created[0].id
const deleteResponse = await api.post(
"/admin/translations/settings/batch",
{
delete: [settingId],
},
adminHeaders
)
expect(deleteResponse.status).toEqual(200)
expect(deleteResponse.data.deleted).toEqual({
ids: [settingId],
object: "translation_settings",
deleted: true,
})
})
it("should delete multiple translation settings", async () => {
const createResponse = await api.post(
"/admin/translations/settings/batch",
{
create: [
{
entity_type: "product_variant",
fields: ["title"],
is_active: true,
},
{
entity_type: "product_category",
fields: ["name"],
is_active: true,
},
{
entity_type: "product_collection",
fields: ["title"],
is_active: true,
},
],
},
adminHeaders
)
const ids = createResponse.data.created.map((s) => s.id)
const deleteResponse = await api.post(
"/admin/translations/settings/batch",
{
delete: ids,
},
adminHeaders
)
expect(deleteResponse.status).toEqual(200)
expect(deleteResponse.data.deleted).toEqual({
ids: expect.arrayContaining(ids),
object: "translation_settings",
deleted: true,
})
expect(deleteResponse.data.deleted.ids).toHaveLength(3)
})
it("should handle deleting with empty array", async () => {
const response = await api.post(
"/admin/translations/settings/batch",
{
delete: [],
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.deleted).toEqual({
ids: [],
object: "translation_settings",
deleted: true,
})
})
})
describe("combined operations", () => {
it("should handle create, update, and delete in a single batch", async () => {
const setupResponse = await api.post(
"/admin/translations/settings/batch",
{
create: [
{
entity_type: "product_variant",
fields: ["title"],
is_active: true,
},
{
entity_type: "product_category",
fields: ["name"],
is_active: true,
},
],
},
adminHeaders
)
const [settingId1, settingId2] = setupResponse.data.created.map(
(s) => s.id
)
const batchResponse = await api.post(
"/admin/translations/settings/batch",
{
create: [
{
entity_type: "product_collection",
fields: ["title"],
is_active: true,
},
],
update: [
{
id: settingId1,
entity_type: "product_variant",
fields: ["title", "material"],
is_active: false,
},
],
delete: [settingId2],
},
adminHeaders
)
expect(batchResponse.status).toEqual(200)
expect(batchResponse.data.created).toHaveLength(1)
expect(batchResponse.data.updated).toHaveLength(1)
expect(batchResponse.data.deleted.ids).toContain(settingId2)
expect(batchResponse.data.created[0]).toEqual(
expect.objectContaining({
entity_type: "product_collection",
fields: ["title"],
is_active: true,
})
)
expect(batchResponse.data.updated[0]).toEqual(
expect.objectContaining({
id: settingId1,
fields: ["title", "material"],
is_active: false,
})
)
})
it("should handle empty batch request", async () => {
const response = await api.post(
"/admin/translations/settings/batch",
{
create: [],
update: [],
delete: [],
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.created).toEqual([])
expect(response.data.updated).toEqual([])
expect(response.data.deleted.ids).toEqual([])
})
})
describe("validation", () => {
it("should reject non-translatable entity types", async () => {
const error = await api
.post(
"/admin/translations/settings/batch",
{
create: [
{
entity_type: "NonExistentEntity",
fields: ["title"],
is_active: true,
},
],
},
adminHeaders
)
.catch((e) => e)
expect(error.response.status).toEqual(400)
expect(error.response.data.message).toContain(
"NonExistentEntity is not a translatable entity"
)
})
it("should reject invalid fields for translatable entities", async () => {
const error = await api
.post(
"/admin/translations/settings/batch",
{
create: [
{
entity_type: "product_variant",
fields: ["title", "invalid_field", "another_invalid"],
is_active: true,
},
],
},
adminHeaders
)
.catch((e) => e)
expect(error.response.status).toEqual(400)
expect(error.response.data.message).toContain("product_variant")
expect(error.response.data.message).toContain("invalid_field")
expect(error.response.data.message).toContain("another_invalid")
})
it("should reject multiple invalid settings in a single batch", async () => {
const error = await api
.post(
"/admin/translations/settings/batch",
{
create: [
{
entity_type: "NonExistentEntity",
fields: ["title"],
is_active: true,
},
{
entity_type: "product_variant",
fields: ["title", "invalid_field"],
is_active: true,
},
],
},
adminHeaders
)
.catch((e) => e)
expect(error.response.status).toEqual(400)
expect(error.response.data.message).toContain(
"NonExistentEntity is not a translatable entity"
)
expect(error.response.data.message).toContain("product_variant")
expect(error.response.data.message).toContain("invalid_field")
})
it("should accept valid fields for translatable entities", async () => {
const response = await api.post(
"/admin/translations/settings/batch",
{
create: [
{
entity_type: "product_variant",
fields: ["title", "material"],
is_active: true,
},
{
entity_type: "product_category",
fields: ["name", "description"],
is_active: true,
},
{
entity_type: "product_collection",
fields: ["title"],
is_active: true,
},
],
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.created).toHaveLength(3)
})
})
})
})
},
})

View File

@@ -22,6 +22,9 @@ medusaIntegrationTestRunner({
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, getContainer())
const translationModule = appContainer.resolve(Modules.TRANSLATION)
await translationModule.__hooks?.onApplicationStart?.().catch(() => {})
const storeModule = appContainer.resolve(Modules.STORE)
const [defaultStore] = await storeModule.listStores(
{},

View File

@@ -89,7 +89,12 @@ export const Notifications = () => {
>
responseKey="notifications"
queryKey={notificationQueryKeys.all}
queryFn={(params) => sdk.admin.notification.list(params)}
queryFn={(params) =>
sdk.admin.notification.list({
...params,
channel: "feed",
})
}
queryOptions={{ enabled: open }}
renderEmpty={() => <NotificationsEmptyState t={t} />}
renderItem={(notification) => {

View File

@@ -0,0 +1,35 @@
import { describe, expect, it } from "@jest/globals"
import { getCodemod, listCodemods } from "../index"
describe("Codemod dispatcher", () => {
describe("listCodemods", () => {
it("should return array of available codemod names", () => {
const codemods = listCodemods()
expect(Array.isArray(codemods)).toBe(true)
expect(codemods.length).toBeGreaterThan(0)
expect(codemods).toContain("replace-zod-imports")
})
})
describe("getCodemod", () => {
it("should return codemod for valid name", () => {
const codemod = getCodemod("replace-zod-imports")
expect(codemod).not.toBeNull()
expect(codemod?.name).toBe("replace-zod-imports")
expect(codemod?.description).toBeTruthy()
expect(typeof codemod?.run).toBe("function")
})
it("should return null for invalid codemod name", () => {
const codemod = getCodemod("non-existent-codemod")
expect(codemod).toBeNull()
})
it("should return codemod with correct interface", () => {
const codemod = getCodemod("replace-zod-imports")
expect(codemod).toHaveProperty("name")
expect(codemod).toHaveProperty("description")
expect(codemod).toHaveProperty("run")
})
})
})

View File

@@ -0,0 +1,324 @@
import fs from "fs"
import path from "path"
import { afterEach, beforeEach, describe, expect, it } from "@jest/globals"
import replaceZodImports from "../replace-zod-imports"
describe("replace-zod-imports codemod", () => {
const tempDir = path.join(__dirname, "temp-test-codemod")
let originalCwd: string
beforeEach(() => {
// Save original working directory
originalCwd = process.cwd()
// Create temp directory for test files
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true })
}
fs.mkdirSync(tempDir, { recursive: true })
// Change to temp directory so codemod runs there
process.chdir(tempDir)
})
afterEach(() => {
// Restore original working directory
process.chdir(originalCwd)
// Clean up temp directory
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true })
}
})
describe("codemod metadata", () => {
it("should have correct name and description", () => {
expect(replaceZodImports.name).toBe("replace-zod-imports")
expect(replaceZodImports.description).toBeTruthy()
expect(typeof replaceZodImports.run).toBe("function")
})
})
describe("named import transformations", () => {
it("should transform named imports from zod", async () => {
const testFile = path.join(tempDir, "test1.ts")
fs.writeFileSync(testFile, `import { z, ZodSchema } from "zod"`)
await replaceZodImports.run({ dryRun: false })
const result = fs.readFileSync(testFile, "utf8")
expect(result).toBe(
`import { z, ZodSchema } from "@medusajs/framework/zod"`
)
})
it("should transform named imports with single quotes", async () => {
const testFile = path.join(tempDir, "test2.ts")
fs.writeFileSync(testFile, `import { z } from 'zod'`)
await replaceZodImports.run({ dryRun: false })
const result = fs.readFileSync(testFile, "utf8")
expect(result).toBe(`import { z } from "@medusajs/framework/zod"`)
})
})
describe("default import transformations", () => {
it("should transform default imports with identifier zod to aliased named imports", async () => {
const testFile = path.join(tempDir, "test3.ts")
fs.writeFileSync(testFile, `import zod from "zod"`)
await replaceZodImports.run({ dryRun: false })
const result = fs.readFileSync(testFile, "utf8")
expect(result).toBe(`import { z as zod } from "@medusajs/framework/zod"`)
})
it("should transform default imports with identifier z to named imports", async () => {
const testFile = path.join(tempDir, "test3b.ts")
fs.writeFileSync(testFile, `import z from "zod"`)
await replaceZodImports.run({ dryRun: false })
const result = fs.readFileSync(testFile, "utf8")
expect(result).toBe(`import { z } from "@medusajs/framework/zod"`)
})
})
describe("namespace import transformations", () => {
it("should transform namespace imports with identifier z", async () => {
const testFile = path.join(tempDir, "test4.ts")
fs.writeFileSync(testFile, `import * as z from "zod"`)
await replaceZodImports.run({ dryRun: false })
const result = fs.readFileSync(testFile, "utf8")
expect(result).toBe(`import { z as z } from "@medusajs/framework/zod"`)
})
it("should transform namespace imports with custom identifier", async () => {
const testFile = path.join(tempDir, "test4b.ts")
fs.writeFileSync(testFile, `import * as validator from "zod"`)
await replaceZodImports.run({ dryRun: false })
const result = fs.readFileSync(testFile, "utf8")
expect(result).toBe(
`import { z as validator } from "@medusajs/framework/zod"`
)
})
it("should transform namespace imports with zod identifier", async () => {
const testFile = path.join(tempDir, "test4c.ts")
fs.writeFileSync(testFile, `import * as zod from "zod"`)
await replaceZodImports.run({ dryRun: false })
const result = fs.readFileSync(testFile, "utf8")
expect(result).toBe(`import { z as zod } from "@medusajs/framework/zod"`)
})
})
describe("type import transformations", () => {
it("should transform type imports", async () => {
const testFile = path.join(tempDir, "test5.ts")
fs.writeFileSync(testFile, `import type { ZodSchema } from "zod"`)
await replaceZodImports.run({ dryRun: false })
const result = fs.readFileSync(testFile, "utf8")
expect(result).toBe(
`import type { ZodSchema } from "@medusajs/framework/zod"`
)
})
})
describe("require statement transformations", () => {
it("should transform require statements", async () => {
const testFile = path.join(tempDir, "test6.js")
fs.writeFileSync(testFile, `const zod = require("zod")`)
await replaceZodImports.run({ dryRun: false })
const result = fs.readFileSync(testFile, "utf8")
expect(result).toBe(`const zod = require("@medusajs/framework/zod")`)
})
it("should transform require with single quotes", async () => {
const testFile = path.join(tempDir, "test7.js")
fs.writeFileSync(testFile, `const z = require('zod')`)
await replaceZodImports.run({ dryRun: false })
const result = fs.readFileSync(testFile, "utf8")
expect(result).toBe(`const z = require("@medusajs/framework/zod")`)
})
})
describe("multiple imports in one file", () => {
it("should handle multiple zod imports", async () => {
const testFile = path.join(tempDir, "test8.ts")
const content = `import { z } from "zod"
import { something } from "other-package"
import type { ZodSchema } from "zod"
const zodRequire = require("zod")`
fs.writeFileSync(testFile, content)
await replaceZodImports.run({ dryRun: false })
const result = fs.readFileSync(testFile, "utf8")
const expected = `import { z } from "@medusajs/framework/zod"
import { something } from "other-package"
import type { ZodSchema } from "@medusajs/framework/zod"
const zodRequire = require("@medusajs/framework/zod")`
expect(result).toBe(expected)
})
})
describe("dry-run mode", () => {
it("should not modify files in dry-run mode", async () => {
const testFile = path.join(tempDir, "test9.ts")
const originalContent = `import { z } from "zod"`
fs.writeFileSync(testFile, originalContent)
const result = await replaceZodImports.run({ dryRun: true })
const afterContent = fs.readFileSync(testFile, "utf8")
expect(afterContent).toBe(originalContent)
expect(result.filesModified).toBeGreaterThan(0)
expect(result.errors).toBe(0)
})
})
describe("files without zod imports", () => {
it("should not modify files without zod imports", async () => {
const testFile = path.join(tempDir, "test10.ts")
const originalContent = `import { something } from "other-package"`
fs.writeFileSync(testFile, originalContent)
await replaceZodImports.run({ dryRun: false })
const afterContent = fs.readFileSync(testFile, "utf8")
expect(afterContent).toBe(originalContent)
})
it("should not modify partial matches like zodiac", async () => {
const testFile = path.join(tempDir, "test11.ts")
const originalContent = `import { something } from "zodiac"`
fs.writeFileSync(testFile, originalContent)
await replaceZodImports.run({ dryRun: false })
const afterContent = fs.readFileSync(testFile, "utf8")
expect(afterContent).toBe(originalContent)
})
})
describe("multiple files", () => {
it("should handle multiple files with different extensions", async () => {
const file1 = path.join(tempDir, "file1.ts")
const file2 = path.join(tempDir, "file2.js")
const file3 = path.join(tempDir, "file3.tsx")
fs.writeFileSync(file1, `import { z } from "zod"`)
fs.writeFileSync(file2, `const z = require("zod")`)
fs.writeFileSync(file3, `import type { ZodType } from "zod"`)
const result = await replaceZodImports.run({ dryRun: false })
expect(fs.readFileSync(file1, "utf8")).toBe(
`import { z } from "@medusajs/framework/zod"`
)
expect(fs.readFileSync(file2, "utf8")).toBe(
`const z = require("@medusajs/framework/zod")`
)
expect(fs.readFileSync(file3, "utf8")).toBe(
`import type { ZodType } from "@medusajs/framework/zod"`
)
expect(result.filesModified).toBeGreaterThanOrEqual(3)
expect(result.errors).toBe(0)
})
})
describe("result reporting", () => {
it("should return correct counts", async () => {
const file1 = path.join(tempDir, "count1.ts")
const file2 = path.join(tempDir, "count2.ts")
const file3 = path.join(tempDir, "no-zod.ts")
fs.writeFileSync(file1, `import { z } from "zod"`)
fs.writeFileSync(file2, `import { z } from "zod"`)
fs.writeFileSync(file3, `import { x } from "other"`)
const result = await replaceZodImports.run({ dryRun: false })
expect(result.filesScanned).toBeGreaterThanOrEqual(2)
expect(result.filesModified).toBeGreaterThanOrEqual(2)
expect(result.errors).toBe(0)
})
it("should return zero counts when no files have zod imports", async () => {
const testFile = path.join(tempDir, "empty.ts")
fs.writeFileSync(testFile, `import { x } from "other"`)
const result = await replaceZodImports.run({ dryRun: false })
expect(result.filesScanned).toBe(0)
expect(result.filesModified).toBe(0)
expect(result.errors).toBe(0)
})
})
describe("file formatting preservation", () => {
it("should preserve whitespace and comments", async () => {
const testFile = path.join(tempDir, "formatted.ts")
const content = `// Header comment
import { z } from "zod"
// Function comment
export function validate() {
return z.string()
}`
fs.writeFileSync(testFile, content)
await replaceZodImports.run({ dryRun: false })
const result = fs.readFileSync(testFile, "utf8")
const expected = `// Header comment
import { z } from "@medusajs/framework/zod"
// Function comment
export function validate() {
return z.string()
}`
expect(result).toBe(expected)
})
})
describe("directory exclusions", () => {
it("should ignore files in src/admin directories", async () => {
const adminDir = path.join(tempDir, "src", "admin")
fs.mkdirSync(adminDir, { recursive: true })
const adminFile = path.join(adminDir, "admin-component.tsx")
const originalContent = `import { z } from "zod"`
fs.writeFileSync(adminFile, originalContent)
const regularFile = path.join(tempDir, "regular-file.ts")
fs.writeFileSync(regularFile, `import { z } from "zod"`)
const result = await replaceZodImports.run({ dryRun: false })
// Admin file should not be modified
const adminContent = fs.readFileSync(adminFile, "utf8")
expect(adminContent).toBe(originalContent)
// Regular file should be modified
const regularContent = fs.readFileSync(regularFile, "utf8")
expect(regularContent).toBe(`import { z } from "@medusajs/framework/zod"`)
// Result should only count the regular file
expect(result.filesModified).toBe(1)
})
})
})

View File

@@ -0,0 +1,26 @@
import type { Codemod } from "./types"
import replaceZodImports from "./replace-zod-imports"
/**
* Registry of available codemods
*/
const CODEMODS: Record<string, Codemod> = {
"replace-zod-imports": replaceZodImports,
}
/**
* Get a codemod by name
* @param name - The name of the codemod to retrieve
* @returns The codemod if found, null otherwise
*/
export function getCodemod(name: string): Codemod | null {
return CODEMODS[name] || null
}
/**
* List all available codemod names
* @returns Array of codemod names
*/
export function listCodemods(): string[] {
return Object.keys(CODEMODS)
}

View File

@@ -0,0 +1,165 @@
import fs from "fs"
import reporter from "../reporter/index"
import type { Codemod, CodemodOptions, CodemodResult } from "./types"
import { glob } from "glob"
const CODEMOD: Codemod = {
name: "replace-zod-imports",
description: "Replace all zod imports with @medusajs/framework/zod imports",
run: replaceZodImports,
}
export default CODEMOD
// Replacement patterns for zod imports
// Order matters: more specific patterns must come before general ones
const REPLACEMENTS = [
// Default import with identifier "zod": import zod from "zod" -> import { z as zod } from "@medusajs/framework/zod"
{
pattern: /import\s+zod\s+from\s+['"]zod['"]/g,
replacement: `import { z as zod } from "@medusajs/framework/zod"`,
},
// Default import with identifier "z": import z from "zod" -> import { z } from "@medusajs/framework/zod"
{
pattern: /import\s+z\s+from\s+['"]zod['"]/g,
replacement: `import { z } from "@medusajs/framework/zod"`,
},
// Namespace import with other identifier: import * as something from "zod" -> import { z as something } from "@medusajs/framework/zod"
{
pattern: /import\s+\*\s+as\s+(\w+)\s+from\s+['"]zod['"]/g,
replacement: `import { z as $1 } from "@medusajs/framework/zod"`,
},
// Named/type imports: import { z } from "zod" or import type { ZodSchema } from "zod"
{
pattern: /from\s+['"]zod['"]/g,
replacement: `from "@medusajs/framework/zod"`,
},
// CommonJS require: require("zod")
{
pattern: /require\s*\(\s*['"]zod['"]\s*\)/g,
replacement: `require("@medusajs/framework/zod")`,
},
]
const ZOD_IMPORT_PATTERN = /from\s+['"]zod['"]|require\s*\(\s*['"]zod['"]\s*\)/
/**
* Replace all zod imports with @medusajs/framework/zod imports
*/
async function replaceZodImports(
options: CodemodOptions
): Promise<CodemodResult> {
const { dryRun = false } = options
const targetFiles = await getTargetFiles()
const numberOfFiles = Object.keys(targetFiles).length
if (numberOfFiles === 0) {
reporter.info(" No files found with zod imports")
return { filesScanned: 0, filesModified: 0, errors: 0 }
}
reporter.info(` Found ${numberOfFiles} files to process`)
let filesModified = 0
let errors = 0
for (const [filePath, content] of Object.entries(targetFiles)) {
try {
if (processFile(filePath, content, dryRun)) {
filesModified++
}
} catch (error) {
reporter.error(`✗ Error processing ${filePath}: ${error.message}`)
errors++
}
}
return { filesScanned: numberOfFiles, filesModified, errors }
}
/**
* Process a single file and replace zod imports
* @returns true if the file was modified, false otherwise
*/
function processFile(
filePath: string,
content: string,
dryRun: boolean
): boolean {
let modifiedContent = content
for (const { pattern, replacement } of REPLACEMENTS) {
modifiedContent = modifiedContent.replace(pattern, replacement)
}
if (modifiedContent === content) {
return false
}
if (dryRun) {
reporter.info(` Would update: ${filePath}`)
} else {
fs.writeFileSync(filePath, modifiedContent)
reporter.info(`✓ Updated: ${filePath}`)
}
return true
}
/**
* Find all TypeScript/JavaScript files that contain zod imports
* @returns Array of file paths with zod imports
*/
async function getTargetFiles(): Promise<Record<string, string>> {
try {
// Find TypeScript/JavaScript files, excluding build artifacts, dependencies, and src/admin
const files = await glob("**/*.{ts,js,tsx,jsx}", {
ignore: [
"**/node_modules/**",
"**/.git/**",
"**/dist/**",
"**/build/**",
"**/coverage/**",
"**/.medusa/**",
"**/src/admin/**",
],
nodir: true,
})
reporter.info(` Scanning ${files.length} files for zod imports...`)
const targetFiles: Record<string, string> = {}
let processedCount = 0
for (const file of files) {
try {
const content = fs.readFileSync(file, "utf8")
if (ZOD_IMPORT_PATTERN.test(content)) {
targetFiles[file] = content
}
processedCount++
if (processedCount % 100 === 0) {
process.stdout.write(
`\r Processed ${processedCount}/${files.length} files...`
)
}
} catch {
// Skip files that can't be read
continue
}
}
if (processedCount > 0) {
process.stdout.write(
`\r Processed ${processedCount} files. \n`
)
}
return targetFiles
} catch (error) {
reporter.error(`Error finding target files: ${error.message}`)
return {}
}
}

View File

@@ -0,0 +1,45 @@
/**
* Options for running a codemod
*/
export interface CodemodOptions {
/**
* Run the codemod without making actual file changes
*/
dryRun?: boolean
}
/**
* Result of running a codemod
*/
export interface CodemodResult {
/**
* Total number of files scanned for changes
*/
filesScanned: number
/**
* Number of files that were modified
*/
filesModified: number
/**
* Number of errors encountered during execution
*/
errors: number
}
/**
* A codemod that can be executed to transform code
*/
export interface Codemod {
/**
* Unique identifier for the codemod
*/
name: string
/**
* Human-readable description of what the codemod does
*/
description: string
/**
* Function that executes the codemod
*/
run: (options: CodemodOptions) => Promise<CodemodResult>
}

View File

@@ -2,6 +2,7 @@ import { setTelemetryEnabled } from "@medusajs/telemetry"
import { sync as existsSync } from "fs-exists-cached"
import path from "path"
import resolveCwd from "resolve-cwd"
import { getCodemod, listCodemods } from "./codemods/index"
import { newStarter } from "./commands/new"
import { didYouMean } from "./did-you-mean"
import reporter from "./reporter"
@@ -296,7 +297,7 @@ function buildLocalCommands(cli, isLocalProject) {
command: "plugin:build",
desc: "Build plugin source for publishing to a package registry",
handler: handlerP(
getCommandHandler("plugin/build", (args, cmd) => {
getCommandHandler("plugin/build", async (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
cmd(args)
return new Promise((resolve) => {})
@@ -307,7 +308,7 @@ function buildLocalCommands(cli, isLocalProject) {
command: "plugin:develop",
desc: "Start plugin development process in watch mode. Changes will be re-published to the local packages registry",
handler: handlerP(
getCommandHandler("plugin/develop", (args, cmd) => {
getCommandHandler("plugin/develop", async (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
cmd(args)
return new Promise(() => {})
@@ -318,7 +319,7 @@ function buildLocalCommands(cli, isLocalProject) {
command: "plugin:publish",
desc: "Publish the plugin to the local packages registry",
handler: handlerP(
getCommandHandler("plugin/publish", (args, cmd) => {
getCommandHandler("plugin/publish", async (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
cmd(args)
return new Promise(() => {})
@@ -336,7 +337,7 @@ function buildLocalCommands(cli, isLocalProject) {
},
},
handler: handlerP(
getCommandHandler("plugin/add", (args, cmd) => {
getCommandHandler("plugin/add", async (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
cmd(args)
return new Promise(() => {})
@@ -365,6 +366,62 @@ function buildLocalCommands(cli, isLocalProject) {
)
}),
})
.command({
command: `codemod <codemod-name>`,
desc: `Run automated code transformations`,
builder: (yargs) =>
yargs
.positional("codemod-name", {
type: "string",
describe: "Name of the codemod to run",
demandOption: true,
})
.option(`dry-run`, {
type: `boolean`,
description: `Preview changes without modifying files`,
default: false,
}),
handler: handlerP(async ({ codemodName, dryRun }) => {
const codemod = getCodemod(codemodName)
if (!codemod) {
const available = listCodemods()
reporter.error(`Unknown codemod: ${codemodName}`)
reporter.info(
`\nAvailable codemods:\n${available
.map((n) => ` - ${n}`)
.join("\n")}`
)
process.exit(1)
}
reporter.info(`Running codemod: ${codemod.name}`)
reporter.info(codemod.description)
if (dryRun) {
reporter.info(`\n DRY RUN MODE - No files will be modified\n`)
}
const result = await codemod.run({ dryRun })
reporter.info(`\n Summary:`)
reporter.info(` Files scanned: ${result.filesScanned}`)
reporter.info(` Files modified: ${result.filesModified}`)
reporter.info(` Errors: ${result.errors}`)
if (dryRun && result.filesModified > 0) {
reporter.info(`\n Run without --dry-run to apply changes`)
} else if (result.filesModified > 0) {
reporter.info(`\n Codemod completed successfully!`)
reporter.info(`\n Next steps:`)
reporter.info(` 1. Review changes: git diff`)
reporter.info(` 2. Run tests to verify`)
reporter.info(` 3. Commit if satisfied`)
} else {
reporter.info(`\n No modifications needed`)
}
}),
})
.command({
command: `develop`,
desc: `Start development server. Watches file and rebuilds when something changes`,
@@ -392,7 +449,7 @@ function buildLocalCommands(cli, isLocalProject) {
: `Set port. Defaults to ${defaultPort}`,
}),
handler: handlerP(
getCommandHandler(`develop`, (args, cmd) => {
getCommandHandler(`develop`, async (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
cmd(args)
@@ -447,7 +504,7 @@ function buildLocalCommands(cli, isLocalProject) {
"Number of server processes in cluster mode or a percentage of cluster size (e.g., 25%).",
}),
handler: handlerP(
getCommandHandler(`start`, (args, cmd) => {
getCommandHandler(`start`, async (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `production`
cmd(args)
// Return an empty promise to prevent handlerP from exiting early.
@@ -468,7 +525,7 @@ function buildLocalCommands(cli, isLocalProject) {
"Only build the admin to serve it separately (outDir .medusa/admin)",
}),
handler: handlerP(
getCommandHandler(`build`, (args, cmd) => {
getCommandHandler(`build`, async (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
cmd(args)
@@ -501,7 +558,7 @@ function buildLocalCommands(cli, isLocalProject) {
default: false,
}),
handler: handlerP(
getCommandHandler(`user`, (args, cmd) => {
getCommandHandler(`user`, async (args, cmd) => {
cmd(args)
// Return an empty promise to prevent handlerP from exiting early.
// The development server shouldn't ever exit until the user directly
@@ -514,7 +571,7 @@ function buildLocalCommands(cli, isLocalProject) {
command: `exec [file] [args..]`,
desc: `Run a function defined in a file.`,
handler: handlerP(
getCommandHandler(`exec`, (args, cmd) => {
getCommandHandler(`exec`, async (args, cmd) => {
cmd(args)
// Return an empty promise to prevent handlerP from exiting early.
// The development server shouldn't ever exit until the user directly

View File

@@ -20,7 +20,7 @@ export interface ValidateCartPaymentsStepInput {
export const validateCartPaymentsStepId = "validate-cart-payments"
/**
* This step validates a cart's payment sessions. Their status must
* be `pending` or `requires_more`. If not valid, the step throws an error.
* be `pending`, `requires_more`, `authorized`, or `captured`. If not valid, the step throws an error.
*
* :::tip
*
@@ -62,6 +62,7 @@ export const validateCartPaymentsStep = createStep(
PaymentSessionStatus.PENDING,
PaymentSessionStatus.REQUIRES_MORE,
PaymentSessionStatus.AUTHORIZED, // E.g. payment was authorized, but the cart was not completed
PaymentSessionStatus.CAPTURED, // E.g. payment was captured, but the cart was not completed
]
const paymentsToProcess = paymentCollection.payment_sessions?.filter((ps) =>

View File

@@ -1,8 +1,14 @@
import { createWorkflow, WorkflowData } from "@medusajs/framework/workflows-sdk"
import {
createWorkflow,
parallelize,
WorkflowData,
} from "@medusajs/framework/workflows-sdk"
import { AdditionalData } from "@medusajs/types"
import { refreshCartItemsWorkflow } from "../../cart/workflows/refresh-cart-items"
import { acquireLockStep, releaseLockStep } from "../../locking"
import { deleteLineItemsStep } from "../steps/delete-line-items"
import { emitEventStep } from "../../common/steps/emit-event"
import { CartWorkflowEvents } from "@medusajs/framework/utils"
/**
* The data to delete line items from a cart.
@@ -53,9 +59,18 @@ export const deleteLineItemsWorkflow = createWorkflow(
deleteLineItemsStep(input.ids)
refreshCartItemsWorkflow.runAsStep({
input: { cart_id: input.cart_id, additional_data: input.additional_data },
})
parallelize(
refreshCartItemsWorkflow.runAsStep({
input: {
cart_id: input.cart_id,
additional_data: input.additional_data,
},
}),
emitEventStep({
eventName: CartWorkflowEvents.UPDATED,
data: { id: input.cart_id },
})
)
releaseLockStep({
key: input.cart_id,

View File

@@ -0,0 +1,34 @@
import { Modules } from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { CreateTranslationSettingsDTO } from "@medusajs/types"
export const createTranslationSettingsStepId = "create-translation-settings"
export type CreateTranslationSettingsStepInput =
| CreateTranslationSettingsDTO
| CreateTranslationSettingsDTO[]
export const createTranslationSettingsStep = createStep(
createTranslationSettingsStepId,
async (data: CreateTranslationSettingsStepInput, { container }) => {
const service = container.resolve(Modules.TRANSLATION)
const normalizedInput = Array.isArray(data) ? data : [data]
const created = await service.createTranslationSettings(normalizedInput)
return new StepResponse(
created,
created.map((translationSettings) => translationSettings.id)
)
},
async (createdIds, { container }) => {
if (!createdIds?.length) {
return
}
const service = container.resolve(Modules.TRANSLATION)
await service.deleteTranslationSettings(createdIds)
}
)

View File

@@ -0,0 +1,28 @@
import { Modules } from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
export const deleteTranslationSettingsStepId = "delete-translation-settings"
export const deleteTranslationSettingsStep = createStep(
deleteTranslationSettingsStepId,
async (data: string[], { container }) => {
const service = container.resolve(Modules.TRANSLATION)
const previous = await service.listTranslationSettings({
id: data,
})
await service.deleteTranslationSettings(data)
return new StepResponse(void 0, previous)
},
async (previous, { container }) => {
if (!previous?.length) {
return
}
const service = container.resolve(Modules.TRANSLATION)
await service.createTranslationSettings(previous)
}
)

View File

@@ -2,3 +2,6 @@ export * from "./create-translations"
export * from "./delete-translations"
export * from "./update-translations"
export * from "./validate-translations"
export * from "./create-translation-settings"
export * from "./update-translation-settings"
export * from "./delete-translation-settings"

View File

@@ -0,0 +1,35 @@
import { Modules } from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { UpdateTranslationSettingsDTO } from "@medusajs/types"
export const updateTranslationSettingsStepId = "update-translation-settings"
export type UpdateTranslationSettingsStepInput =
| UpdateTranslationSettingsDTO
| UpdateTranslationSettingsDTO[]
export const updateTranslationSettingsStep = createStep(
updateTranslationSettingsStepId,
async (data: UpdateTranslationSettingsStepInput, { container }) => {
const service = container.resolve(Modules.TRANSLATION)
const normalizedInput = Array.isArray(data) ? data : [data]
const previous = await service.listTranslationSettings({
id: normalizedInput.map((d) => d.id),
})
const updated = await service.updateTranslationSettings(normalizedInput)
return new StepResponse(updated, previous)
},
async (previous, { container }) => {
if (!previous?.length) {
return
}
const service = container.resolve(Modules.TRANSLATION)
await service.updateTranslationSettings(previous)
}
)

View File

@@ -0,0 +1,35 @@
import {
createWorkflow,
parallelize,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
UpdateTranslationSettingsDTO,
CreateTranslationSettingsDTO,
} from "@medusajs/types"
import {
createTranslationSettingsStep,
deleteTranslationSettingsStep,
updateTranslationSettingsStep,
} from "../steps"
export const batchTranslationSettingsWorkflowId = "batch-translation-settings"
export interface BatchTranslationSettingsWorkflowInput {
create: CreateTranslationSettingsDTO[]
update: UpdateTranslationSettingsDTO[]
delete: string[]
}
export const batchTranslationSettingsWorkflow = createWorkflow(
batchTranslationSettingsWorkflowId,
(input: BatchTranslationSettingsWorkflowInput) => {
const [created, updated, deleted] = parallelize(
createTranslationSettingsStep(input.create),
updateTranslationSettingsStep(input.update),
deleteTranslationSettingsStep(input.delete)
)
return new WorkflowResponse({ created, updated, deleted })
}
)

View File

@@ -7,7 +7,6 @@ import {
} from "@medusajs/framework/workflows-sdk"
import { emitEventStep } from "../../common/steps/emit-event"
import { createTranslationsStep } from "../steps"
import { validateTranslationsStep } from "../steps"
import { TranslationWorkflowEvents } from "@medusajs/framework/utils"
/**
@@ -27,7 +26,7 @@ export const createTranslationsWorkflowId = "create-translations"
*
* You can use this workflow within your own customizations or custom workflows, allowing you
* to create translations in your custom flows.
*
*
* @since 2.12.3
* @featureFlag translation
*
@@ -55,7 +54,6 @@ export const createTranslationsWorkflow = createWorkflow(
(
input: WorkflowData<CreateTranslationsWorkflowInput>
): WorkflowResponse<TranslationDTO[]> => {
validateTranslationsStep(input.translations)
const translations = createTranslationsStep(input.translations)
const translationIdEvents = transform(

View File

@@ -2,3 +2,4 @@ export * from "./create-translations"
export * from "./delete-translations"
export * from "./update-translations"
export * from "./batch-translations"
export * from "./batch-translation-settings"

View File

@@ -7,7 +7,6 @@ import {
} from "@medusajs/framework/workflows-sdk"
import { emitEventStep } from "../../common/steps/emit-event"
import { updateTranslationsStep, UpdateTranslationsStepInput } from "../steps"
import { validateTranslationsStep } from "../steps"
import { TranslationWorkflowEvents } from "@medusajs/framework/utils"
/**
@@ -22,13 +21,13 @@ export const updateTranslationsWorkflowId = "update-translations"
*
* You can use this workflow within your own customizations or custom workflows, allowing you
* to update translations in your custom flows.
*
*
* @since 2.12.3
* @featureFlag translation
*
* @example
* To update translations by their IDs:
*
*
* ```ts
* const { result } = await updateTranslationsWorkflow(container)
* .run({
@@ -61,11 +60,6 @@ export const updateTranslationsWorkflow = createWorkflow(
(
input: WorkflowData<UpdateTranslationsWorkflowInput>
): WorkflowResponse<TranslationDTO[]> => {
const validateInput = transform(input, (input) => {
return "translations" in input ? input.translations : [input.update]
})
validateTranslationsStep(validateInput)
const translations = updateTranslationsStep(input)
const translationIdEvents = transform(

View File

@@ -27,7 +27,10 @@
"./telemetry": "./dist/telemetry/index.js",
"./feature-flags": "./dist/feature-flags/index.js",
"./utils": "./dist/utils/index.js",
"./types": "./dist/types/index.js",
"./types": {
"types": "./dist/types/index.d.ts",
"default": "./dist/types/index.js"
},
"./build-tools": "./dist/build-tools/index.js",
"./orchestration": "./dist/orchestration/index.js",
"./workflows-sdk": "./dist/workflows-sdk/index.js",

View File

@@ -38,4 +38,35 @@ describe("configLoader", () => {
expect(configModule.projectConfig.databaseName).toBe("foo")
expect(configModule.projectConfig.workerMode).toBe("worker")
})
it("should load config without throwing errors when throwOnError is false", async () => {
await configLoader(entryDirectory, "medusa-config", {
throwOnError: false,
})
const configModule = container.resolve(
ContainerRegistrationKeys.CONFIG_MODULE
)
expect(configModule).toBeDefined()
expect(configModule.projectConfig).toBeDefined()
})
it("should pass throwOnError option through to buildHttpConfig", async () => {
// When throwOnError is false, missing jwtSecret and cookieSecret should not cause errors
await configLoader(entryDirectory, "medusa-config-2", {
throwOnError: false,
})
const configModule = container.resolve(
ContainerRegistrationKeys.CONFIG_MODULE
)
expect(configModule).toBeDefined()
expect(configModule.projectConfig.databaseName).toBe("foo")
// http config should still be built with defaults even without throwing errors
expect(configModule.projectConfig.http).toBeDefined()
expect(configModule.projectConfig.http.jwtSecret).toBe("supersecret")
expect(configModule.projectConfig.http.cookieSecret).toBe("supersecret")
})
})

View File

@@ -70,8 +70,12 @@ export class ConfigManager {
* @protected
*/
protected buildHttpConfig(
projectConfig: Partial<ConfigModule["projectConfig"]>
projectConfig: Partial<ConfigModule["projectConfig"]>,
options?: {
throwOnError?: boolean
}
): ConfigModule["projectConfig"]["http"] {
const { throwOnError = true } = options ?? {}
const http = (projectConfig.http ??
{}) as ConfigModule["projectConfig"]["http"]
@@ -87,6 +91,7 @@ export class ConfigManager {
http.jwtPublicKey = http?.jwtPublicKey ?? process.env.JWT_PUBLIC_KEY
if (
throwOnError &&
http?.jwtPublicKey &&
((http.jwtVerifyOptions && !http.jwtVerifyOptions.algorithms?.length) ||
(http.jwtOptions && !http.jwtOptions.algorithm))
@@ -97,11 +102,13 @@ export class ConfigManager {
}
if (!http.jwtSecret) {
this.rejectErrors(
`http.jwtSecret not found.${
this.#isProduction ? "" : "Using default 'supersecret'."
}`
)
if (throwOnError) {
this.rejectErrors(
`http.jwtSecret not found.${
this.#isProduction ? "" : "Using default 'supersecret'."
}`
)
}
http.jwtSecret = "supersecret"
}
@@ -110,11 +117,13 @@ export class ConfigManager {
process.env.COOKIE_SECRET)!
if (!http.cookieSecret) {
this.rejectErrors(
`http.cookieSecret not found.${
this.#isProduction ? "" : " Using default 'supersecret'."
}`
)
if (throwOnError) {
this.rejectErrors(
`http.cookieSecret not found.${
this.#isProduction ? "" : " Using default 'supersecret'."
}`
)
}
http.cookieSecret = "supersecret"
}
@@ -128,7 +137,10 @@ export class ConfigManager {
* @protected
*/
protected normalizeProjectConfig(
config: Partial<ConfigModule>
config: Partial<ConfigModule>,
options?: {
throwOnError?: boolean
}
): ConfigModule["projectConfig"] {
const projConfig = config?.projectConfig ?? {}
const outputConfig = deepCopy(projConfig) as ConfigModule["projectConfig"]
@@ -140,7 +152,9 @@ export class ConfigManager {
)
}
outputConfig.http = this.buildHttpConfig(projConfig)
outputConfig.http = this.buildHttpConfig(projConfig, {
throwOnError: options?.throwOnError,
})
let workerMode = outputConfig?.workerMode!
@@ -168,13 +182,17 @@ export class ConfigManager {
loadConfig({
projectConfig = {},
baseDir,
throwOnError = true,
}: {
projectConfig: Partial<ConfigModule>
baseDir: string
throwOnError?: boolean
}): ConfigModule {
this.#baseDir = baseDir
const normalizedProjectConfig = this.normalizeProjectConfig(projectConfig)
const normalizedProjectConfig = this.normalizeProjectConfig(projectConfig, {
throwOnError,
})
this.#config = {
projectConfig: normalizedProjectConfig,

View File

@@ -25,22 +25,29 @@ container.register(
*
* @param entryDirectory The directory to find the config file from
* @param configFileName The name of the config file to search for in the entry directory
* @param options.throwOnError When false, missing config files and validation errors won't throw.
* Useful for build/compile commands. Defaults to true.
*/
export async function configLoader(
entryDirectory: string,
configFileName: string = "medusa-config"
configFileName: string = "medusa-config",
options?: {
throwOnError?: boolean
}
): Promise<ConfigModule> {
const { throwOnError = true } = options ?? {}
const config = await getConfigFile<ConfigModule>(
entryDirectory,
configFileName
)
if (config.error) {
if (config.error && throwOnError) {
handleConfigError(config.error)
}
return configManager.loadConfig({
projectConfig: config.configModule!,
baseDir: entryDirectory,
throwOnError,
})
}

View File

@@ -1 +1,8 @@
import "@medusajs/utils"
export * from "@medusajs/types"
import type { ModuleOptions as ModuleOptionsType } from "@medusajs/types"
// Re-declare ModuleOptions to enable augmentation from @medusajs/framework/types
// EventBusEventsOptions is exported via "export *" and gets augmentations from @medusajs/utils
export interface ModuleOptions extends ModuleOptionsType {}

View File

@@ -1,3 +1,5 @@
import "@medusajs/types"
import "@medusajs/utils"
import "../types/container"
export * from "@medusajs/utils"

View File

@@ -1139,32 +1139,30 @@ type ExternalModuleDeclarationOverride = ExternalModuleDeclaration & {
disable?: boolean
}
/**
* Generates a union of typed module configs for all known modules in the ModuleOptions registry.
* This enables automatic type inference when using registered module resolve strings.
*/
type KnownModuleConfigs = {
[K in keyof ModuleOptions]: Partial<
Omit<InternalModuleDeclaration, "options"> & {
type ModuleConfigForResolve<R extends string> = R extends keyof ModuleOptions
? {
resolve: R
key?: string
disable?: boolean
resolve: K
options?: ModuleOptions[K]
}
>
}[keyof ModuleOptions]
options?: ModuleOptions[R]
} & Partial<Omit<InternalModuleDeclaration, "options" | "resolve">>
: {
resolve?: string
key?: string
disable?: boolean
options?: object
} & Partial<Omit<InternalModuleDeclaration, "options" | "resolve">>
/**
* Generates a union of typed module configs for all known modules in the ModuleOptions registry.
* This distributes over all keys in ModuleOptions to create specific config types for each.
*/
type KnownModuleConfigs = ModuleConfigForResolve<keyof ModuleOptions & string>
/**
* Generic module config for modules not registered in ModuleOptions.
*/
type GenericModuleConfig = Partial<
Omit<InternalModuleDeclaration, "options"> & {
key?: string
disable?: boolean
resolve?: string
options?: Record<string, unknown>
}
>
type GenericModuleConfig = ModuleConfigForResolve<string & {}>
/**
* Modules accepted by the defineConfig function.
@@ -1177,20 +1175,36 @@ export type InputConfigModules = (
)[]
/**
* The configuration accepted by the "defineConfig" helper
* Base configuration type without modules
*/
export type InputConfig = Partial<
type InputConfigBase = Partial<
Omit<ConfigModule, "admin" | "modules"> & {
admin?: Partial<ConfigModule["admin"]>
modules:
| InputConfigModules
/**
* @deprecated use the array instead
*/
| ConfigModule["modules"]
}
>
/**
* Configuration with array-based modules (recommended)
*/
export type InputConfigWithArrayModules = InputConfigBase & {
modules?: InputConfigModules
}
/**
* Configuration with object-based modules (deprecated)
* @deprecated Use array-based modules instead
*/
export type InputConfigWithObjectModules = InputConfigBase & {
modules?: ConfigModule["modules"]
}
/**
* The configuration accepted by the "defineConfig" helper
*/
export type InputConfig =
| InputConfigWithArrayModules
| InputConfigWithObjectModules
type PluginAdminDetails = {
type: "local" | "package"
resolve: string

View File

@@ -1,5 +1,27 @@
import { Context } from "../shared-context"
// TODO: Comment temporarely and we will re enable it in the near future #14478
// /**
// * Configuration options for individual events.
// */
// export interface EventOptions {
// /**
// * Priority level for the event processing.
// * Lower numbers indicate higher priority.
// */
// priority?: number
// }
// /**
// * Registry for event bus events options types.
// * Events will be added to this registry to serve as a global configuration for all events
// * as part of the event bus module service module options.
// *
// * Modules augment this interface using declaration merging to register their event configurations.
// * Custom events can be added via declaration merging in your project.
// */
// export interface EventBusEventsOptions {}
export type Subscriber<TData = unknown> = (data: Event<TData>) => Promise<void>
export type SubscriberContext = {

View File

@@ -6,7 +6,7 @@ export interface AdminTranslation {
/**
* The ID of the entity being translated.
*
*
* @example
* "prod_123"
*/
@@ -14,7 +14,7 @@ export interface AdminTranslation {
/**
* The name of the table that the translation belongs to.
*
*
* @example
* "product"
*/
@@ -22,7 +22,7 @@ export interface AdminTranslation {
/**
* The BCP 47 language tag code for this translation.
*
*
* @example
* "en-US"
*/
@@ -31,7 +31,7 @@ export interface AdminTranslation {
/**
* The translations of the resource.
* The object's keys are the field names of the data model, and its value is the translated value.
*
*
* @example
* {
* "title": "Product Title",
@@ -55,3 +55,34 @@ export interface AdminTranslation {
*/
deleted_at: Date | string | null
}
export interface AdminTranslationSettings {
/**
* The ID of the settings.
*/
id: string
/**
* The date and time the settings were created.
*/
created_at: Date | string
/**
* The date and time the settings were last updated.
*/
updated_at: Date | string
/**
* The date and time the settings were deleted.
*/
deleted_at: Date | string | null
/**
* The entity type.
*/
entity_type: string
/**
* The translatable fields.
*/
fields: string[]
/**
* Whether the entity translatable status is enabled.
*/
is_active: boolean
}

View File

@@ -1,5 +1,5 @@
import { PaginatedResponse } from "../../common"
import { AdminTranslation } from "./entities"
import { AdminTranslation, AdminTranslationSettings } from "./entities"
export interface AdminTranslationsResponse {
/**
@@ -100,6 +100,25 @@ export interface AdminTranslationSettingsResponse {
translatable_fields: Record<string, string[]>
}
export interface AdminBatchTranslationSettingsResponse {
/**
* The created settings.
*/
created: AdminTranslationSettings[]
/**
* The updated settings.
*/
updated: AdminTranslationSettings[]
/**
* The deleted settings.
*/
deleted: {
ids: string[]
object: "translation_settings"
deleted: boolean
}
}
/**
* Response for translation entities endpoint.
* Returns paginated entities with only their translatable fields and all their translations.

View File

@@ -99,6 +99,11 @@ export interface TranslationSettingsDTO {
*/
fields: string[]
/**
* Whether the entity translatable status is enabled.
*/
is_active: boolean
/**
* The date and time the settings were created.
*/
@@ -168,13 +173,30 @@ export interface FilterableTranslationProps
locale_code?: string | string[] | OperatorMap<string>
}
export interface FilterableTranslationSettingsProps
extends BaseFilterable<FilterableTranslationSettingsProps> {
/**
* The IDs to filter the translation settings by.
*/
id?: string[] | string | OperatorMap<string | string[]>
/**
* Filter translation settings by entity type.
*/
entity_type?: string | string[] | OperatorMap<string | string[]>
/**
* Filter translation settings by active status.
*/
is_active?: boolean | OperatorMap<boolean>
}
/**
* Input for getStatistics method.
*/
export interface TranslationStatisticsInput {
/**
* Locales to check translations for.
*
*
* @example
* ["en-US", "fr-FR"]
*/
@@ -183,15 +205,18 @@ export interface TranslationStatisticsInput {
/**
* Key-value pairs of entity types and their configurations.
*/
entities: Record<string, {
/**
* Total number of records for the entity type.
* For example, total number of products.
*
* This is necessary to compute expected translation counts.
*/
count: number
}>
entities: Record<
string,
{
/**
* Total number of records for the entity type.
* For example, total number of products.
*
* This is necessary to compute expected translation counts.
*/
count: number
}
>
}
/**

View File

@@ -9,7 +9,7 @@ export interface CreateLocaleDTO {
/**
* The BCP 47 language tag code of the locale.
*
*
* @example
* "en-US"
*/
@@ -17,7 +17,7 @@ export interface CreateLocaleDTO {
/**
* The human-readable name of the locale.
*
*
* @example
* "English (United States)"
*/
@@ -30,7 +30,7 @@ export interface CreateLocaleDTO {
export interface UpdateLocaleDataDTO {
/**
* The BCP 47 language tag code of the locale.
*
*
* @example
* "en-US"
*/
@@ -38,7 +38,7 @@ export interface UpdateLocaleDataDTO {
/**
* The human-readable name of the locale.
*
*
* @example
* "English (United States)"
*/
@@ -66,7 +66,7 @@ export interface UpsertLocaleDTO {
/**
* The BCP 47 language tag code of the locale.
*
*
* @example
* "en-US"
*/
@@ -74,7 +74,7 @@ export interface UpsertLocaleDTO {
/**
* The human-readable name of the locale.
*
*
* @example
* "English (United States)"
*/
@@ -87,7 +87,7 @@ export interface UpsertLocaleDTO {
export interface CreateTranslationDTO {
/**
* The ID of the data model being translated.
*
*
* @example
* "prod_123"
*/
@@ -95,7 +95,7 @@ export interface CreateTranslationDTO {
/**
* The name of the table that the translation belongs to.
*
*
* @example
* "product"
*/
@@ -103,7 +103,7 @@ export interface CreateTranslationDTO {
/**
* The BCP 47 language tag code for this translation.
*
*
* @example
* "en-US"
*/
@@ -111,7 +111,7 @@ export interface CreateTranslationDTO {
/**
* The translated fields as key-value pairs.
*
*
* @example
* {
* "title": "Product Title",
@@ -127,7 +127,7 @@ export interface CreateTranslationDTO {
export interface UpdateTranslationDataDTO {
/**
* The ID of the data model being translated.
*
*
* @example
* "prod_123"
*/
@@ -135,7 +135,7 @@ export interface UpdateTranslationDataDTO {
/**
* The name of the table that the translation belongs to.
*
*
* @example
* "product"
*/
@@ -143,7 +143,7 @@ export interface UpdateTranslationDataDTO {
/**
* The BCP 47 language tag code for this translation.
*
*
* @example
* "en-US"
*/
@@ -151,7 +151,7 @@ export interface UpdateTranslationDataDTO {
/**
* The translated fields as key-value pairs.
*
*
* @example
* {
* "title": "Product Title",
@@ -182,7 +182,7 @@ export interface UpsertTranslationDTO {
/**
* The ID of the data model being translated.
*
*
* @example
* "prod_123"
*/
@@ -190,7 +190,7 @@ export interface UpsertTranslationDTO {
/**
* The name of the table that the translation belongs to.
*
*
* @example
* "product"
*/
@@ -198,7 +198,7 @@ export interface UpsertTranslationDTO {
/**
* The BCP 47 language tag code for this translation.
*
*
* @example
* "en-US"
*/
@@ -206,7 +206,7 @@ export interface UpsertTranslationDTO {
/**
* The translated fields as key-value pairs.
*
*
* @example
* {
* "title": "Product Title",
@@ -215,3 +215,52 @@ export interface UpsertTranslationDTO {
*/
translations?: Record<string, unknown>
}
export interface CreateTranslationSettingsDTO {
/**
* The entity type.
*
* @example
* "product"
*/
entity_type: string
/**
* The translatable fields.
*
* @example
* ["title", "description", "material"]
*/
fields: string[]
/**
* Whether the entity translatable status is enabled.
*/
is_active?: boolean
}
/**
* The translation settings to be created or updated.
*/
export interface UpdateTranslationSettingsDTO {
/**
* The ID of the translation settings to update.
*/
id: string
/**
* The entity type.
*
* @example
* "product"
*/
entity_type?: string
/**
* The translatable fields.
*
* @example
* ["title", "description", "material"]
*/
fields?: string[]
/**
* Whether the entity translatable status is enabled.
*/
is_active?: boolean
}

View File

@@ -5,23 +5,27 @@ import { Context } from "../shared-context"
import {
FilterableLocaleProps,
FilterableTranslationProps,
FilterableTranslationSettingsProps,
LocaleDTO,
TranslationDTO,
TranslationSettingsDTO,
TranslationStatisticsInput,
TranslationStatisticsOutput,
} from "./common"
import {
CreateLocaleDTO,
CreateTranslationDTO,
CreateTranslationSettingsDTO,
UpdateLocaleDTO,
UpdateLocaleDataDTO,
UpdateTranslationDTO,
UpdateTranslationDataDTO,
UpdateTranslationSettingsDTO,
} from "./mutations"
/**
* The main service interface for the Translation Module.
*
*
* @privateRemarks
* Method signatures match what MedusaService generates.
*/
@@ -43,12 +47,12 @@ export interface ITranslationModuleService extends IModuleService {
* ```
*
* To specify relations that should be retrieved:
*
*
* :::note
*
*
* You can only retrieve data models defined in the same module. To retrieve linked data models
* from other modules, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query) instead.
*
*
* :::
*
* ```ts
@@ -82,12 +86,12 @@ export interface ITranslationModuleService extends IModuleService {
* ```
*
* To specify relations that should be retrieved within the locales:
*
*
* :::note
*
*
* You can only retrieve data models defined in the same module. To retrieve linked data models
* from other modules, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query) instead.
*
*
* :::
*
* ```ts
@@ -141,12 +145,12 @@ export interface ITranslationModuleService extends IModuleService {
* ```
*
* To specify relations that should be retrieved within the locales:
*
*
* :::note
*
*
* You can only retrieve data models defined in the same module. To retrieve linked data models
* from other modules, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query) instead.
*
*
* :::
*
* ```ts
@@ -250,7 +254,7 @@ export interface ITranslationModuleService extends IModuleService {
*
* @example
* To update locales by their IDs:
*
*
* ```ts
* const locales = await translationModuleService.updateLocales([
* {
@@ -265,7 +269,7 @@ export interface ITranslationModuleService extends IModuleService {
* ```
*
* To update locales by a selector:
*
*
* ```ts
* const locales = await translationModuleService.updateLocales({
* selector: {
@@ -299,7 +303,7 @@ export interface ITranslationModuleService extends IModuleService {
* @param {string | object | string[] | object[]} primaryKeyValues - The IDs or objects with IDs identifying the locales to delete.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<void>} Resolves when the locales are deleted.
*
*
* @example
* await translationModuleService.deleteLocales(["loc_123", "loc_321"])
*/
@@ -336,7 +340,7 @@ export interface ITranslationModuleService extends IModuleService {
* @returns {Promise<Record<string, string[]> | void>} An object that includes the IDs of related records that were restored.
*
* If there are no related records restored, the promise resolves to `void`.
*
*
* @example
* await translationModuleService.restoreLocales(["loc_123", "loc_321"])
*/
@@ -362,12 +366,12 @@ export interface ITranslationModuleService extends IModuleService {
* ```
*
* To specify relations that should be retrieved:
*
*
* :::note
*
*
* You can only retrieve data models defined in the same module. To retrieve linked data models
* from other modules, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query) instead.
*
*
* :::
*
* ```ts
@@ -401,12 +405,12 @@ export interface ITranslationModuleService extends IModuleService {
* ```
*
* To specify relations that should be retrieved within the translations:
*
*
* :::note
*
*
* You can only retrieve data models defined in the same module. To retrieve linked data models
* from other modules, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query) instead.
*
*
* :::
*
* ```ts
@@ -419,7 +423,7 @@ export interface ITranslationModuleService extends IModuleService {
* }
* )
* ```
*
*
* By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter:
*
* ```ts
@@ -460,12 +464,12 @@ export interface ITranslationModuleService extends IModuleService {
* ```
*
* To specify relations that should be retrieved within the translations:
*
*
* :::note
*
*
* You can only retrieve data models defined in the same module. To retrieve linked data models
* from other modules, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query) instead.
*
*
* :::
*
* ```ts
@@ -478,7 +482,7 @@ export interface ITranslationModuleService extends IModuleService {
* }
* )
* ```
*
*
* By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter:
*
* ```ts
@@ -618,7 +622,7 @@ export interface ITranslationModuleService extends IModuleService {
* @param {string | object | string[] | object[]} primaryKeyValues - The IDs or objects with IDs identifying the translations to delete.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<void>} Resolves when the translations are deleted.
*
*
* @example
* await translationModuleService.deleteTranslations("tra_123")
*/
@@ -635,7 +639,7 @@ export interface ITranslationModuleService extends IModuleService {
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<Record<string, string[]> | void>} An object that includes the IDs of related records that were also soft deleted.
* If there are no related records, the promise resolves to `void`.
*
*
* @example
* await translationModuleService.softDeleteTranslations(["tra_123", "tra_321"])
*/
@@ -667,7 +671,7 @@ export interface ITranslationModuleService extends IModuleService {
/**
* This method retrieves translation statistics for the specified entities and locales.
* It's useful to understand the translation coverage of different entities across various locales.
*
*
* You can use this method to get insights into how many fields are translated, missing translations,
* and the expected number of translations based on the entities and locales provided.
*
@@ -731,4 +735,189 @@ export interface ITranslationModuleService extends IModuleService {
entityType?: string,
sharedContext?: Context
): Promise<Record<string, string[]>>
/**
* This method creates a translation setting.
*
* @param {CreateTranslationSettingsDTO} data - The translation setting to be created.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<TranslationSettingsDTO>} The created translation setting.
*
* @example
* const translationSetting = await translationModuleService.createTranslationSettings({
* entity_type: "product",
* fields: ["title", "description"],
* is_active: true,
* })
*/
createTranslationSettings(
data: CreateTranslationSettingsDTO,
sharedContext?: Context
): Promise<TranslationSettingsDTO>
/**
*
* @param data - The translation settings to be created.
* @param sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<TranslationSettingsDTO[]>} The created translation settings.
*
* @example
* const translationSettings = await translationModuleService.createTranslationSettings([
* {
* entity_type: "product",
* fields: ["title", "description"],
* is_active: true,
* },
* ])
*/
createTranslationSettings(
data: CreateTranslationSettingsDTO[],
sharedContext?: Context
): Promise<TranslationSettingsDTO[]>
/**
* This method updates an existent translation setting. The ID should be included in the data object.
}
* @param {UpdateTranslationSettingsDTO} data - The attributes to update in the translation setting (including id).
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<TranslationSettingsDTO>} The updated translation setting.
*
* @example
* const translationSettings = await translationModuleService.updateTranslationSettings([
* {
* id: "ts_123",
* entity_type: "product_collection",
* fields: ["title"],
* is_active: true,
* },
* ])
*/
updateTranslationSettings(
data: UpdateTranslationSettingsDTO,
sharedContext?: Context
): Promise<TranslationSettingsDTO>
/**
* This method updates one or more existent translation settings.
* @param {UpdateTranslationSettingsDTO[]} data - The translation settings to update.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<TranslationSettingsDTO[]>} The updated translation settings.
*
* @example
* const translationSettings = await translationModuleService.updateTranslationSettings([
* {
* id: "ts_123",
* entity_type: "product_collection",
* fields: ["title"],
* is_active: true,
* },
* ])
*/
updateTranslationSettings(
data: UpdateTranslationSettingsDTO[],
sharedContext?: Context
): Promise<TranslationSettingsDTO[]>
/**
* This method deletes one or more translation settings.
*
* @param {string[]} input - The IDs of the translation settings to delete.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<void>} Resolves when the translation settings are deleted.
*
* @example
* await translationModuleService.deleteTranslationSettings([
* "ts_123",
* "ts_321",
* ])
*/
deleteTranslationSettings(
input: string[],
sharedContext?: Context
): Promise<void>
/**
* This method retrieves a paginated list of translation settings based on optional filters and configuration.
*
* @param {FilterableTranslationSettingsProps} filters - The filters to apply on the retrieved translation settings.
* @param {FindConfig<TranslationSettingsDTO>} config - The configurations determining how the translation settings are retrieved. Its properties, such as `select` or `relations`, accept the
* attributes or relations associated with a translation settings.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<TranslationSettingsDTO[]>} The list of translation settings.
*
* @example
* const translationSettings = await translationModuleService.listTranslationSettings({
* entity_type: "product",
* is_active: true,
* })
* // Returns: [
* // {
* // id: "ts_123",
* // entity_type: "product",
* // fields: ["title", "description"],
* // is_active: true,
* // },
* // ]
*/
listTranslationSettings(
filters?: FilterableTranslationSettingsProps,
config?: FindConfig<TranslationSettingsDTO>,
sharedContext?: Context
): Promise<TranslationSettingsDTO[]>
/**
* This method retrieves a paginated list of translation settings based on optional filters and configuration, along with the total count.
*
* @param {FilterableTranslationSettingsProps} filters - The filters to apply on the retrieved translation settings.
* @param {FindConfig<TranslationSettingsDTO>} config - The configurations determining how the translation settings are retrieved. Its properties, such as `select` or `relations`, accept the
* attributes or relations associated with a translation settings.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<[TranslationSettingsDTO[], number]>} The list of translation settings along with their total count.
*
* @example
* const [translationSettings, count] = await translationModuleService.listAndCountTranslationSettings({
* entity_type: "product",
* is_active: true,
* })
* // Returns: [
* // [
* // {
* // id: "ts_123",
* // entity_type: "product",
* // fields: ["title", "description"],
* // is_active: true,
* // },
* // ],
* // 1,
* // ]
*/
listAndCountTranslationSettings(
filters?: FilterableTranslationSettingsProps,
config?: FindConfig<TranslationSettingsDTO>,
sharedContext?: Context
): Promise<[TranslationSettingsDTO[], number]>
/**
* This method retrieves a translation setting by its ID.
*
* @param {string} id - The ID of the translation setting to retrieve.
* @param {FindConfig<TranslationSettingsDTO>} config - The configurations determining how the translation setting is retrieved. Its properties, such as `select` or `relations`, accept the
* attributes or relations associated with a translation settings.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<TranslationSettingsDTO>} The retrieved translation setting.
*
* @example
* const translationSetting = await translationModuleService.retrieveTranslationSettings("ts_123")
* // Returns: {
* // id: "ts_123",
* // entity_type: "product",
* // fields: ["title", "description"],
* // is_active: true,
* // }
*/
retrieveTranslationSettings(
id: string,
config?: FindConfig<TranslationSettingsDTO>,
sharedContext?: Context
): Promise<TranslationSettingsDTO>
}

View File

@@ -1100,7 +1100,7 @@ describe("defineConfig", function () {
},
],
},
"resolve": "@medusajs/caching",
"resolve": "@medusajs/medusa/caching",
},
"cart": {
"resolve": "@medusajs/medusa/cart",
@@ -1327,7 +1327,7 @@ describe("defineConfig", function () {
},
],
},
"resolve": "@medusajs/caching",
"resolve": "@medusajs/medusa/caching",
},
"cart": {
"resolve": "@medusajs/medusa/cart",
@@ -1570,7 +1570,7 @@ describe("defineConfig", function () {
},
],
},
"resolve": "@medusajs/caching",
"resolve": "@medusajs/medusa/caching",
},
"cart": {
"resolve": "@medusajs/medusa/cart",

View File

@@ -3,6 +3,8 @@ import {
ConfigModule,
InputConfig,
InputConfigModules,
InputConfigWithArrayModules,
InputConfigWithObjectModules,
InternalModuleDeclaration,
MedusaCloudOptions,
} from "@medusajs/types"
@@ -43,6 +45,15 @@ export const DEFAULT_STORE_RESTRICTED_FIELDS = [
* make an application work seamlessly, but still provide you the ability
* to override configuration as needed.
*/
export function defineConfig(
config?: InputConfigWithArrayModules
): ConfigModule
/**
* @deprecated Use array-based modules configuration instead
*/
export function defineConfig(
config?: InputConfigWithObjectModules
): ConfigModule
export function defineConfig(config: InputConfig = {}): ConfigModule {
const options = {
isCloud: process.env.EXECUTION_CONTEXT === MEDUSA_CLOUD_EXECUTION_CONTEXT,

View File

@@ -1,3 +1,6 @@
// TODO: Comment temporarely and we will re enable it in the near future #14478
// import { EventOptions } from "@medusajs/types"
/**
* @category Cart
* @customNamespace Cart
@@ -63,7 +66,7 @@ export const CartWorkflowEvents = {
* ```
*/
CUSTOMER_TRANSFERRED: "cart.customer_transferred",
}
} as const
/**
* @category Customer
@@ -103,7 +106,7 @@ export const CustomerWorkflowEvents = {
* ```
*/
DELETED: "customer.deleted",
}
} as const
/**
* @category Order
@@ -259,7 +262,7 @@ export const OrderWorkflowEvents = {
* ```
*/
TRANSFER_REQUESTED: "order.transfer_requested",
}
} as const
/**
* @category Order Edit
@@ -308,7 +311,7 @@ export const OrderEditWorkflowEvents = {
* ```
*/
CANCELED: "order-edit.canceled",
}
} as const
/**
* @category User
@@ -348,7 +351,7 @@ export const UserWorkflowEvents = {
* ```
*/
DELETED: "user.deleted",
}
} as const
/**
* @category Auth
@@ -370,7 +373,7 @@ export const AuthWorkflowEvents = {
* ```
*/
PASSWORD_RESET: "auth.password_reset",
}
} as const
/**
* @category Sales Channel
@@ -410,7 +413,7 @@ export const SalesChannelWorkflowEvents = {
* ```
*/
DELETED: "sales-channel.deleted",
}
} as const
/**
* @category Product Category
@@ -450,7 +453,7 @@ export const ProductCategoryWorkflowEvents = {
* ```
*/
DELETED: "product-category.deleted",
}
} as const
/**
* @category Product Collection
@@ -490,7 +493,7 @@ export const ProductCollectionWorkflowEvents = {
* ```
*/
DELETED: "product-collection.deleted",
}
} as const
/**
* @category Product Variant
@@ -530,7 +533,7 @@ export const ProductVariantWorkflowEvents = {
* ```
*/
DELETED: "product-variant.deleted",
}
} as const
/**
* @category Product
@@ -570,7 +573,7 @@ export const ProductWorkflowEvents = {
* ```
*/
DELETED: "product.deleted",
}
} as const
/**
* @category Product Type
@@ -610,7 +613,7 @@ export const ProductTypeWorkflowEvents = {
* ```
*/
DELETED: "product-type.deleted",
}
} as const
/**
* @category Product Tag
@@ -650,7 +653,7 @@ export const ProductTagWorkflowEvents = {
* ```
*/
DELETED: "product-tag.deleted",
}
} as const
/**
* @category Product Option
@@ -690,7 +693,7 @@ export const ProductOptionWorkflowEvents = {
* ```
*/
DELETED: "product-option.deleted",
}
} as const
/**
* @category Invite
@@ -744,7 +747,7 @@ export const InviteWorkflowEvents = {
* ```
*/
RESENT: "invite.resent",
}
} as const
/**
* @category Region
@@ -784,7 +787,7 @@ export const RegionWorkflowEvents = {
* ```
*/
DELETED: "region.deleted",
}
} as const
/**
* @category Fulfillment
@@ -814,7 +817,7 @@ export const FulfillmentWorkflowEvents = {
* ```
*/
DELIVERY_CREATED: "delivery.created",
}
} as const
/**
* @category Shipping Option Type
@@ -860,7 +863,7 @@ export const ShippingOptionTypeWorkflowEvents = {
* ```
*/
DELETED: "shipping-option-type.deleted",
}
} as const
/**
* @category Shipping Option
@@ -906,7 +909,7 @@ export const ShippingOptionWorkflowEvents = {
* ```
*/
DELETED: "shipping-option.deleted",
}
} as const
/**
* @category Payment
@@ -935,7 +938,7 @@ export const PaymentEvents = {
* ```
*/
REFUNDED: "payment.refunded",
}
} as const
/**
* @category Translation
@@ -981,4 +984,122 @@ export const TranslationWorkflowEvents = {
* ```
*/
DELETED: "translation.deleted",
}
} as const
// TODO: Comment temporarely and we will re enable it in the near future #14478
// declare module "@medusajs/types" {
// export interface EventBusEventsOptions {
// // Cart events
// [CartWorkflowEvents.CREATED]?: EventOptions
// [CartWorkflowEvents.UPDATED]?: EventOptions
// [CartWorkflowEvents.CUSTOMER_UPDATED]?: EventOptions
// [CartWorkflowEvents.REGION_UPDATED]?: EventOptions
// [CartWorkflowEvents.CUSTOMER_TRANSFERRED]?: EventOptions
// // Customer events
// [CustomerWorkflowEvents.CREATED]?: EventOptions
// [CustomerWorkflowEvents.UPDATED]?: EventOptions
// [CustomerWorkflowEvents.DELETED]?: EventOptions
// // Order events
// [OrderWorkflowEvents.UPDATED]?: EventOptions
// [OrderWorkflowEvents.PLACED]?: EventOptions
// [OrderWorkflowEvents.CANCELED]?: EventOptions
// [OrderWorkflowEvents.COMPLETED]?: EventOptions
// [OrderWorkflowEvents.ARCHIVED]?: EventOptions
// [OrderWorkflowEvents.FULFILLMENT_CREATED]?: EventOptions
// [OrderWorkflowEvents.FULFILLMENT_CANCELED]?: EventOptions
// [OrderWorkflowEvents.RETURN_REQUESTED]?: EventOptions
// [OrderWorkflowEvents.RETURN_RECEIVED]?: EventOptions
// [OrderWorkflowEvents.CLAIM_CREATED]?: EventOptions
// [OrderWorkflowEvents.EXCHANGE_CREATED]?: EventOptions
// [OrderWorkflowEvents.TRANSFER_REQUESTED]?: EventOptions
// // Order Edit events
// [OrderEditWorkflowEvents.REQUESTED]?: EventOptions
// [OrderEditWorkflowEvents.CONFIRMED]?: EventOptions
// [OrderEditWorkflowEvents.CANCELED]?: EventOptions
// // User events
// [UserWorkflowEvents.CREATED]?: EventOptions
// [UserWorkflowEvents.UPDATED]?: EventOptions
// [UserWorkflowEvents.DELETED]?: EventOptions
// // Auth events
// [AuthWorkflowEvents.PASSWORD_RESET]?: EventOptions
// // Sales Channel events
// [SalesChannelWorkflowEvents.CREATED]?: EventOptions
// [SalesChannelWorkflowEvents.UPDATED]?: EventOptions
// [SalesChannelWorkflowEvents.DELETED]?: EventOptions
// // Product Category events
// [ProductCategoryWorkflowEvents.CREATED]?: EventOptions
// [ProductCategoryWorkflowEvents.UPDATED]?: EventOptions
// [ProductCategoryWorkflowEvents.DELETED]?: EventOptions
// // Product Collection events
// [ProductCollectionWorkflowEvents.CREATED]?: EventOptions
// [ProductCollectionWorkflowEvents.UPDATED]?: EventOptions
// [ProductCollectionWorkflowEvents.DELETED]?: EventOptions
// // Product Variant events
// [ProductVariantWorkflowEvents.CREATED]?: EventOptions
// [ProductVariantWorkflowEvents.UPDATED]?: EventOptions
// [ProductVariantWorkflowEvents.DELETED]?: EventOptions
// // Product events
// [ProductWorkflowEvents.CREATED]?: EventOptions
// [ProductWorkflowEvents.UPDATED]?: EventOptions
// [ProductWorkflowEvents.DELETED]?: EventOptions
// // Product Type events
// [ProductTypeWorkflowEvents.CREATED]?: EventOptions
// [ProductTypeWorkflowEvents.UPDATED]?: EventOptions
// [ProductTypeWorkflowEvents.DELETED]?: EventOptions
// // Product Tag events
// [ProductTagWorkflowEvents.CREATED]?: EventOptions
// [ProductTagWorkflowEvents.UPDATED]?: EventOptions
// [ProductTagWorkflowEvents.DELETED]?: EventOptions
// // Product Option events
// [ProductOptionWorkflowEvents.CREATED]?: EventOptions
// [ProductOptionWorkflowEvents.UPDATED]?: EventOptions
// [ProductOptionWorkflowEvents.DELETED]?: EventOptions
// // Invite events
// [InviteWorkflowEvents.ACCEPTED]?: EventOptions
// [InviteWorkflowEvents.CREATED]?: EventOptions
// [InviteWorkflowEvents.DELETED]?: EventOptions
// [InviteWorkflowEvents.RESENT]?: EventOptions
// // Region events
// [RegionWorkflowEvents.CREATED]?: EventOptions
// [RegionWorkflowEvents.UPDATED]?: EventOptions
// [RegionWorkflowEvents.DELETED]?: EventOptions
// // Fulfillment events
// [FulfillmentWorkflowEvents.SHIPMENT_CREATED]?: EventOptions
// [FulfillmentWorkflowEvents.DELIVERY_CREATED]?: EventOptions
// // Shipping Option Type events
// [ShippingOptionTypeWorkflowEvents.CREATED]?: EventOptions
// [ShippingOptionTypeWorkflowEvents.UPDATED]?: EventOptions
// [ShippingOptionTypeWorkflowEvents.DELETED]?: EventOptions
// // Shipping Option events
// [ShippingOptionWorkflowEvents.CREATED]?: EventOptions
// [ShippingOptionWorkflowEvents.UPDATED]?: EventOptions
// [ShippingOptionWorkflowEvents.DELETED]?: EventOptions
// // Payment events
// [PaymentEvents.CAPTURED]?: EventOptions
// [PaymentEvents.REFUNDED]?: EventOptions
// // Translation events
// [TranslationWorkflowEvents.CREATED]?: EventOptions
// [TranslationWorkflowEvents.UPDATED]?: EventOptions
// [TranslationWorkflowEvents.DELETED]?: EventOptions
// }
// }

View File

@@ -2,30 +2,42 @@ import { KebabCase, SnakeCase } from "@medusajs/types"
import { camelToSnakeCase, kebabCase, lowerCaseFirst } from "../common"
import { CommonEvents } from "./common-events"
type ReturnType<TNames extends string[]> = {
type ReturnType<TNames extends string[], TPrefix extends string = ""> = {
[K in TNames[number] as `${Uppercase<
SnakeCase<K & string>
>}_CREATED`]: `${KebabCase<K & string>}.created`
>}_CREATED`]: TPrefix extends ""
? `${KebabCase<K & string>}.created`
: `${TPrefix}.${KebabCase<K & string>}.created`
} & {
[K in TNames[number] as `${Uppercase<
SnakeCase<K & string>
>}_UPDATED`]: `${KebabCase<K & string>}.updated`
>}_UPDATED`]: TPrefix extends ""
? `${KebabCase<K & string>}.updated`
: `${TPrefix}.${KebabCase<K & string>}.updated`
} & {
[K in TNames[number] as `${Uppercase<
SnakeCase<K & string>
>}_DELETED`]: `${KebabCase<K & string>}.deleted`
>}_DELETED`]: TPrefix extends ""
? `${KebabCase<K & string>}.deleted`
: `${TPrefix}.${KebabCase<K & string>}.deleted`
} & {
[K in TNames[number] as `${Uppercase<
SnakeCase<K & string>
>}_RESTORED`]: `${KebabCase<K & string>}.restored`
>}_RESTORED`]: TPrefix extends ""
? `${KebabCase<K & string>}.restored`
: `${TPrefix}.${KebabCase<K & string>}.restored`
} & {
[K in TNames[number] as `${Uppercase<
SnakeCase<K & string>
>}_ATTACHED`]: `${KebabCase<K & string>}.attached`
>}_ATTACHED`]: TPrefix extends ""
? `${KebabCase<K & string>}.attached`
: `${TPrefix}.${KebabCase<K & string>}.attached`
} & {
[K in TNames[number] as `${Uppercase<
SnakeCase<K & string>
>}_DETACHED`]: `${KebabCase<K & string>}.detached`
>}_DETACHED`]: TPrefix extends ""
? `${KebabCase<K & string>}.detached`
: `${TPrefix}.${KebabCase<K & string>}.detached`
}
/**
@@ -64,10 +76,10 @@ export function buildModuleResourceEventName({
* @param names
* @param prefix
*/
export function buildEventNamesFromEntityName<TNames extends string[]>(
names: TNames,
prefix?: string
): ReturnType<TNames> {
export function buildEventNamesFromEntityName<
TNames extends string[],
TPrefix extends string = ""
>(names: TNames, prefix?: TPrefix): ReturnType<TNames, TPrefix> {
const events = {}
for (let i = 0; i < names.length; i++) {
@@ -85,7 +97,7 @@ export function buildEventNamesFromEntityName<TNames extends string[]>(
}
}
return events as ReturnType<TNames>
return events as ReturnType<TNames, TPrefix>
}
export const EventPriority = {

View File

@@ -1,3 +1,5 @@
// TODO: Comment temporarely and we will re enable it in the near future #14478
// import { EventOptions } from "@medusajs/types"
import { buildEventNamesFromEntityName } from "../event-bus"
import { Modules } from "../modules-sdk"
@@ -37,4 +39,101 @@ export const FulfillmentEvents = {
* @deprecated use `FulfillmentWorkflowEvents.DELIVERY_CREATED` instead
*/
DELIVERY_CREATED: "delivery.created",
}
} as const
// TODO: Comment temporarely and we will re enable it in the near future #14478
// declare module "@medusajs/types" {
// export interface EventBusEventsOptions {
// // Fulfillment Set events
// [FulfillmentEvents.FULFILLMENT_SET_CREATED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_SET_UPDATED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_SET_DELETED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_SET_RESTORED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_SET_ATTACHED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_SET_DETACHED]?: EventOptions
// // Service Zone events
// [FulfillmentEvents.SERVICE_ZONE_CREATED]?: EventOptions
// [FulfillmentEvents.SERVICE_ZONE_UPDATED]?: EventOptions
// [FulfillmentEvents.SERVICE_ZONE_DELETED]?: EventOptions
// [FulfillmentEvents.SERVICE_ZONE_RESTORED]?: EventOptions
// [FulfillmentEvents.SERVICE_ZONE_ATTACHED]?: EventOptions
// [FulfillmentEvents.SERVICE_ZONE_DETACHED]?: EventOptions
// // Geo Zone events
// [FulfillmentEvents.GEO_ZONE_CREATED]?: EventOptions
// [FulfillmentEvents.GEO_ZONE_UPDATED]?: EventOptions
// [FulfillmentEvents.GEO_ZONE_DELETED]?: EventOptions
// [FulfillmentEvents.GEO_ZONE_RESTORED]?: EventOptions
// [FulfillmentEvents.GEO_ZONE_ATTACHED]?: EventOptions
// [FulfillmentEvents.GEO_ZONE_DETACHED]?: EventOptions
// // Shipping Option events
// [FulfillmentEvents.SHIPPING_OPTION_CREATED]?: EventOptions
// [FulfillmentEvents.SHIPPING_OPTION_UPDATED]?: EventOptions
// [FulfillmentEvents.SHIPPING_OPTION_DELETED]?: EventOptions
// [FulfillmentEvents.SHIPPING_OPTION_RESTORED]?: EventOptions
// [FulfillmentEvents.SHIPPING_OPTION_ATTACHED]?: EventOptions
// [FulfillmentEvents.SHIPPING_OPTION_DETACHED]?: EventOptions
// // Shipping Option Type events
// [FulfillmentEvents.SHIPPING_OPTION_TYPE_CREATED]?: EventOptions
// [FulfillmentEvents.SHIPPING_OPTION_TYPE_UPDATED]?: EventOptions
// [FulfillmentEvents.SHIPPING_OPTION_TYPE_DELETED]?: EventOptions
// [FulfillmentEvents.SHIPPING_OPTION_TYPE_RESTORED]?: EventOptions
// [FulfillmentEvents.SHIPPING_OPTION_TYPE_ATTACHED]?: EventOptions
// [FulfillmentEvents.SHIPPING_OPTION_TYPE_DETACHED]?: EventOptions
// // Shipping Profile events
// [FulfillmentEvents.SHIPPING_PROFILE_CREATED]?: EventOptions
// [FulfillmentEvents.SHIPPING_PROFILE_UPDATED]?: EventOptions
// [FulfillmentEvents.SHIPPING_PROFILE_DELETED]?: EventOptions
// [FulfillmentEvents.SHIPPING_PROFILE_RESTORED]?: EventOptions
// [FulfillmentEvents.SHIPPING_PROFILE_ATTACHED]?: EventOptions
// [FulfillmentEvents.SHIPPING_PROFILE_DETACHED]?: EventOptions
// // Shipping Option Rule events
// [FulfillmentEvents.SHIPPING_OPTION_RULE_CREATED]?: EventOptions
// [FulfillmentEvents.SHIPPING_OPTION_RULE_UPDATED]?: EventOptions
// [FulfillmentEvents.SHIPPING_OPTION_RULE_DELETED]?: EventOptions
// [FulfillmentEvents.SHIPPING_OPTION_RULE_RESTORED]?: EventOptions
// [FulfillmentEvents.SHIPPING_OPTION_RULE_ATTACHED]?: EventOptions
// [FulfillmentEvents.SHIPPING_OPTION_RULE_DETACHED]?: EventOptions
// // Fulfillment events
// [FulfillmentEvents.FULFILLMENT_CREATED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_UPDATED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_DELETED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_RESTORED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_ATTACHED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_DETACHED]?: EventOptions
// // Fulfillment Address events
// [FulfillmentEvents.FULFILLMENT_ADDRESS_CREATED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_ADDRESS_UPDATED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_ADDRESS_DELETED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_ADDRESS_RESTORED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_ADDRESS_ATTACHED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_ADDRESS_DETACHED]?: EventOptions
// // Fulfillment Item events
// [FulfillmentEvents.FULFILLMENT_ITEM_CREATED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_ITEM_UPDATED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_ITEM_DELETED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_ITEM_RESTORED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_ITEM_ATTACHED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_ITEM_DETACHED]?: EventOptions
// // Fulfillment Label events
// [FulfillmentEvents.FULFILLMENT_LABEL_CREATED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_LABEL_UPDATED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_LABEL_DELETED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_LABEL_RESTORED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_LABEL_ATTACHED]?: EventOptions
// [FulfillmentEvents.FULFILLMENT_LABEL_DETACHED]?: EventOptions
// // Deprecated events
// [FulfillmentEvents.SHIPMENT_CREATED]?: EventOptions
// [FulfillmentEvents.DELIVERY_CREATED]?: EventOptions
// }
// }

View File

@@ -1,3 +1,5 @@
// TODO: Comment temporarely and we will re enable it in the near future #14478
// import { EventOptions } from "@medusajs/types"
import { buildEventNamesFromEntityName } from "../event-bus"
import { Modules } from "../modules-sdk"
@@ -11,3 +13,32 @@ export const InventoryEvents = buildEventNamesFromEntityName(
eventBaseNames,
Modules.INVENTORY
)
// TODO: Comment temporarely and we will re enable it in the near future #14478
// declare module "@medusajs/types" {
// export interface EventBusEventsOptions {
// // Inventory Item events
// [InventoryEvents.INVENTORY_ITEM_CREATED]?: EventOptions
// [InventoryEvents.INVENTORY_ITEM_UPDATED]?: EventOptions
// [InventoryEvents.INVENTORY_ITEM_DELETED]?: EventOptions
// [InventoryEvents.INVENTORY_ITEM_RESTORED]?: EventOptions
// [InventoryEvents.INVENTORY_ITEM_ATTACHED]?: EventOptions
// [InventoryEvents.INVENTORY_ITEM_DETACHED]?: EventOptions
// // Reservation Item events
// [InventoryEvents.RESERVATION_ITEM_CREATED]?: EventOptions
// [InventoryEvents.RESERVATION_ITEM_UPDATED]?: EventOptions
// [InventoryEvents.RESERVATION_ITEM_DELETED]?: EventOptions
// [InventoryEvents.RESERVATION_ITEM_RESTORED]?: EventOptions
// [InventoryEvents.RESERVATION_ITEM_ATTACHED]?: EventOptions
// [InventoryEvents.RESERVATION_ITEM_DETACHED]?: EventOptions
// // Inventory Level events
// [InventoryEvents.INVENTORY_LEVEL_CREATED]?: EventOptions
// [InventoryEvents.INVENTORY_LEVEL_UPDATED]?: EventOptions
// [InventoryEvents.INVENTORY_LEVEL_DELETED]?: EventOptions
// [InventoryEvents.INVENTORY_LEVEL_RESTORED]?: EventOptions
// [InventoryEvents.INVENTORY_LEVEL_ATTACHED]?: EventOptions
// [InventoryEvents.INVENTORY_LEVEL_DETACHED]?: EventOptions
// }
// }

View File

@@ -61,8 +61,8 @@ export const MODULE_PACKAGE_NAMES = {
[Modules.INDEX]: "@medusajs/medusa/index-module",
[Modules.LOCKING]: "@medusajs/medusa/locking",
[Modules.SETTINGS]: "@medusajs/medusa/settings",
[Modules.CACHING]: "@medusajs/caching",
[Modules.TRANSLATION]: "@medusajs/translation",
[Modules.CACHING]: "@medusajs/medusa/caching",
[Modules.TRANSLATION]: "@medusajs/medusa/translation",
[Modules.RBAC]: "@medusajs/medusa/rbac",
}

View File

@@ -1,3 +1,5 @@
// TODO: Comment temporarely and we will re enable it in the near future #14478
// import { EventOptions } from "@medusajs/types"
import { buildEventNamesFromEntityName } from "../event-bus"
import { Modules } from "../modules-sdk"
@@ -7,3 +9,16 @@ export const NotificationEvents = buildEventNamesFromEntityName(
eventBaseNames,
Modules.NOTIFICATION
)
// TODO: Comment temporarely and we will re enable it in the near future #14478
// declare module "@medusajs/types" {
// export interface EventBusEventsOptions {
// // Notification events
// [NotificationEvents.NOTIFICATION_CREATED]?: EventOptions
// [NotificationEvents.NOTIFICATION_UPDATED]?: EventOptions
// [NotificationEvents.NOTIFICATION_DELETED]?: EventOptions
// [NotificationEvents.NOTIFICATION_RESTORED]?: EventOptions
// [NotificationEvents.NOTIFICATION_ATTACHED]?: EventOptions
// [NotificationEvents.NOTIFICATION_DETACHED]?: EventOptions
// }
// }

View File

@@ -1,3 +1,5 @@
// TODO: Comment temporarely and we will re enable it in the near future #14478
// import { EventOptions } from "@medusajs/types"
import { buildEventNamesFromEntityName } from "../event-bus"
import { Modules } from "../modules-sdk"
@@ -13,3 +15,48 @@ export const PricingEvents = buildEventNamesFromEntityName(
eventBaseNames,
Modules.PRICING
)
// TODO: Comment temporarely and we will re enable it in the near future #14478
// declare module "@medusajs/types" {
// export interface EventBusEventsOptions {
// // Price List Rule events
// [PricingEvents.PRICE_LIST_RULE_CREATED]?: EventOptions
// [PricingEvents.PRICE_LIST_RULE_UPDATED]?: EventOptions
// [PricingEvents.PRICE_LIST_RULE_DELETED]?: EventOptions
// [PricingEvents.PRICE_LIST_RULE_RESTORED]?: EventOptions
// [PricingEvents.PRICE_LIST_RULE_ATTACHED]?: EventOptions
// [PricingEvents.PRICE_LIST_RULE_DETACHED]?: EventOptions
// // Price List events
// [PricingEvents.PRICE_LIST_CREATED]?: EventOptions
// [PricingEvents.PRICE_LIST_UPDATED]?: EventOptions
// [PricingEvents.PRICE_LIST_DELETED]?: EventOptions
// [PricingEvents.PRICE_LIST_RESTORED]?: EventOptions
// [PricingEvents.PRICE_LIST_ATTACHED]?: EventOptions
// [PricingEvents.PRICE_LIST_DETACHED]?: EventOptions
// // Price Rule events
// [PricingEvents.PRICE_RULE_CREATED]?: EventOptions
// [PricingEvents.PRICE_RULE_UPDATED]?: EventOptions
// [PricingEvents.PRICE_RULE_DELETED]?: EventOptions
// [PricingEvents.PRICE_RULE_RESTORED]?: EventOptions
// [PricingEvents.PRICE_RULE_ATTACHED]?: EventOptions
// [PricingEvents.PRICE_RULE_DETACHED]?: EventOptions
// // Price Set events
// [PricingEvents.PRICE_SET_CREATED]?: EventOptions
// [PricingEvents.PRICE_SET_UPDATED]?: EventOptions
// [PricingEvents.PRICE_SET_DELETED]?: EventOptions
// [PricingEvents.PRICE_SET_RESTORED]?: EventOptions
// [PricingEvents.PRICE_SET_ATTACHED]?: EventOptions
// [PricingEvents.PRICE_SET_DETACHED]?: EventOptions
// // Price events
// [PricingEvents.PRICE_CREATED]?: EventOptions
// [PricingEvents.PRICE_UPDATED]?: EventOptions
// [PricingEvents.PRICE_DELETED]?: EventOptions
// [PricingEvents.PRICE_RESTORED]?: EventOptions
// [PricingEvents.PRICE_ATTACHED]?: EventOptions
// [PricingEvents.PRICE_DETACHED]?: EventOptions
// }
// }

View File

@@ -1,3 +1,5 @@
// TODO: Comment temporarely and we will re enable it in the near future #14478
// import { EventOptions } from "@medusajs/types"
import { buildEventNamesFromEntityName } from "../event-bus"
import { Modules } from "../modules-sdk"
@@ -27,3 +29,80 @@ export const ProductEvents = buildEventNamesFromEntityName(
eventBaseNames,
Modules.PRODUCT
)
// TODO: Comment temporarely and we will re enable it in the near future #14478
// declare module "@medusajs/types" {
// export interface EventBusEventsOptions {
// // Product events
// [ProductEvents.PRODUCT_CREATED]?: EventOptions
// [ProductEvents.PRODUCT_UPDATED]?: EventOptions
// [ProductEvents.PRODUCT_DELETED]?: EventOptions
// [ProductEvents.PRODUCT_RESTORED]?: EventOptions
// [ProductEvents.PRODUCT_ATTACHED]?: EventOptions
// [ProductEvents.PRODUCT_DETACHED]?: EventOptions
// // Product Variant events
// [ProductEvents.PRODUCT_VARIANT_CREATED]?: EventOptions
// [ProductEvents.PRODUCT_VARIANT_UPDATED]?: EventOptions
// [ProductEvents.PRODUCT_VARIANT_DELETED]?: EventOptions
// [ProductEvents.PRODUCT_VARIANT_RESTORED]?: EventOptions
// [ProductEvents.PRODUCT_VARIANT_ATTACHED]?: EventOptions
// [ProductEvents.PRODUCT_VARIANT_DETACHED]?: EventOptions
// // Product Option events
// [ProductEvents.PRODUCT_OPTION_CREATED]?: EventOptions
// [ProductEvents.PRODUCT_OPTION_UPDATED]?: EventOptions
// [ProductEvents.PRODUCT_OPTION_DELETED]?: EventOptions
// [ProductEvents.PRODUCT_OPTION_RESTORED]?: EventOptions
// [ProductEvents.PRODUCT_OPTION_ATTACHED]?: EventOptions
// [ProductEvents.PRODUCT_OPTION_DETACHED]?: EventOptions
// // Product Option Value events
// [ProductEvents.PRODUCT_OPTION_VALUE_CREATED]?: EventOptions
// [ProductEvents.PRODUCT_OPTION_VALUE_UPDATED]?: EventOptions
// [ProductEvents.PRODUCT_OPTION_VALUE_DELETED]?: EventOptions
// [ProductEvents.PRODUCT_OPTION_VALUE_RESTORED]?: EventOptions
// [ProductEvents.PRODUCT_OPTION_VALUE_ATTACHED]?: EventOptions
// [ProductEvents.PRODUCT_OPTION_VALUE_DETACHED]?: EventOptions
// // Product Type events
// [ProductEvents.PRODUCT_TYPE_CREATED]?: EventOptions
// [ProductEvents.PRODUCT_TYPE_UPDATED]?: EventOptions
// [ProductEvents.PRODUCT_TYPE_DELETED]?: EventOptions
// [ProductEvents.PRODUCT_TYPE_RESTORED]?: EventOptions
// [ProductEvents.PRODUCT_TYPE_ATTACHED]?: EventOptions
// [ProductEvents.PRODUCT_TYPE_DETACHED]?: EventOptions
// // Product Tag events
// [ProductEvents.PRODUCT_TAG_CREATED]?: EventOptions
// [ProductEvents.PRODUCT_TAG_UPDATED]?: EventOptions
// [ProductEvents.PRODUCT_TAG_DELETED]?: EventOptions
// [ProductEvents.PRODUCT_TAG_RESTORED]?: EventOptions
// [ProductEvents.PRODUCT_TAG_ATTACHED]?: EventOptions
// [ProductEvents.PRODUCT_TAG_DETACHED]?: EventOptions
// // Product Category events
// [ProductEvents.PRODUCT_CATEGORY_CREATED]?: EventOptions
// [ProductEvents.PRODUCT_CATEGORY_UPDATED]?: EventOptions
// [ProductEvents.PRODUCT_CATEGORY_DELETED]?: EventOptions
// [ProductEvents.PRODUCT_CATEGORY_RESTORED]?: EventOptions
// [ProductEvents.PRODUCT_CATEGORY_ATTACHED]?: EventOptions
// [ProductEvents.PRODUCT_CATEGORY_DETACHED]?: EventOptions
// // Product Collection events
// [ProductEvents.PRODUCT_COLLECTION_CREATED]?: EventOptions
// [ProductEvents.PRODUCT_COLLECTION_UPDATED]?: EventOptions
// [ProductEvents.PRODUCT_COLLECTION_DELETED]?: EventOptions
// [ProductEvents.PRODUCT_COLLECTION_RESTORED]?: EventOptions
// [ProductEvents.PRODUCT_COLLECTION_ATTACHED]?: EventOptions
// [ProductEvents.PRODUCT_COLLECTION_DETACHED]?: EventOptions
// // Product Image events
// [ProductEvents.PRODUCT_IMAGE_CREATED]?: EventOptions
// [ProductEvents.PRODUCT_IMAGE_UPDATED]?: EventOptions
// [ProductEvents.PRODUCT_IMAGE_DELETED]?: EventOptions
// [ProductEvents.PRODUCT_IMAGE_RESTORED]?: EventOptions
// [ProductEvents.PRODUCT_IMAGE_ATTACHED]?: EventOptions
// [ProductEvents.PRODUCT_IMAGE_DETACHED]?: EventOptions
// }
// }

View File

@@ -1,3 +1,5 @@
// TODO: Comment temporarely and we will re enable it in the near future #14478
// import { EventOptions } from "@medusajs/types"
import { buildEventNamesFromEntityName } from "../event-bus"
import { Modules } from "../modules-sdk"
@@ -6,4 +8,28 @@ const eventBaseNames: ["user", "invite"] = ["user", "invite"]
export const UserEvents = {
...buildEventNamesFromEntityName(eventBaseNames, Modules.USER),
INVITE_TOKEN_GENERATED: `${Modules.USER}.user.invite.token_generated`,
}
} as const
// TODO: Comment temporarely and we will re enable it in the near future #14478
// declare module "@medusajs/types" {
// export interface EventBusEventsOptions {
// // User events
// [UserEvents.USER_CREATED]?: EventOptions
// [UserEvents.USER_UPDATED]?: EventOptions
// [UserEvents.USER_DELETED]?: EventOptions
// [UserEvents.USER_RESTORED]?: EventOptions
// [UserEvents.USER_ATTACHED]?: EventOptions
// [UserEvents.USER_DETACHED]?: EventOptions
// // Invite events
// [UserEvents.INVITE_CREATED]?: EventOptions
// [UserEvents.INVITE_UPDATED]?: EventOptions
// [UserEvents.INVITE_DELETED]?: EventOptions
// [UserEvents.INVITE_RESTORED]?: EventOptions
// [UserEvents.INVITE_ATTACHED]?: EventOptions
// [UserEvents.INVITE_DETACHED]?: EventOptions
// // Custom events
// [UserEvents.INVITE_TOKEN_GENERATED]?: EventOptions
// }
// }

View File

@@ -5,6 +5,7 @@ import {
} from "@medusajs/framework"
import {
AdminBatchTranslations,
AdminBatchTranslationSettings,
AdminGetTranslationsParams,
AdminTranslationEntitiesParams,
AdminTranslationSettingsParams,
@@ -44,6 +45,11 @@ export const adminTranslationsRoutesMiddlewares: MiddlewareRoute[] = [
validateAndTransformQuery(AdminTranslationSettingsParams, {}),
],
},
{
method: ["POST"],
matcher: "/admin/translations/settings/batch",
middlewares: [validateAndTransformBody(AdminBatchTranslationSettings)],
},
{
method: ["GET"],
matcher: "/admin/translations/entities",

View File

@@ -0,0 +1,39 @@
import { batchTranslationSettingsWorkflow } from "@medusajs/core-flows"
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework"
import { defineFileConfig, FeatureFlag } from "@medusajs/framework/utils"
import { HttpTypes } from "@medusajs/types"
import TranslationFeatureFlag from "../../../../../feature-flags/translation"
import { AdminBatchTranslationSettingsType } from "../../validators"
/**
* @since 2.12.5
* @featureFlag translation
*/
export const POST = async (
req: AuthenticatedMedusaRequest<AdminBatchTranslationSettingsType>,
res: MedusaResponse<HttpTypes.AdminBatchTranslationSettingsResponse>
) => {
const { create = [], update = [], delete: deleteIds = [] } = req.validatedBody
const { result } = await batchTranslationSettingsWorkflow(req.scope).run({
input: {
create,
update,
delete: deleteIds,
},
})
return res.status(200).json({
created: result.created,
updated: result.updated,
deleted: {
ids: deleteIds,
object: "translation_settings",
deleted: true,
},
})
}
defineFileConfig({
isDisabled: () => !FeatureFlag.isFeatureEnabled(TranslationFeatureFlag.key),
})

View File

@@ -72,6 +72,27 @@ export const AdminTranslationSettingsParams = z.object({
entity_type: z.string().optional(),
})
const AdminUpdateTranslationSettings = z.object({
id: z.string(),
entity_type: z.string().optional(),
fields: z.array(z.string()).optional(),
is_active: z.boolean().optional(),
})
const AdminCreateTranslationSettings = z.object({
entity_type: z.string(),
fields: z.array(z.string()),
is_active: z.boolean().optional(),
})
export type AdminBatchTranslationSettingsType = z.infer<
typeof AdminBatchTranslationSettings
>
export const AdminBatchTranslationSettings = createBatchBody(
AdminCreateTranslationSettings,
AdminUpdateTranslationSettings
)
export type AdminTranslationEntitiesParamsType = z.infer<
typeof AdminTranslationEntitiesParams
>

View File

@@ -12,6 +12,7 @@ export default async function build({
}) {
const container = await initializeContainer(directory, {
skipDbConnection: true,
throwOnError: false,
})
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)

View File

@@ -134,10 +134,13 @@ export async function initializeContainer(
rootDirectory: string,
options?: {
skipDbConnection?: boolean
throwOnError?: boolean
}
): Promise<MedusaContainer> {
await featureFlagsLoader(rootDirectory)
const configDir = await configLoader(rootDirectory, "medusa-config")
const configDir = await configLoader(rootDirectory, "medusa-config", {
throwOnError: options?.throwOnError,
})
await featureFlagsLoader(join(__dirname, ".."))
const customLogger = configDir.logger ?? defaultLogger

View File

@@ -5,7 +5,7 @@ import { CustomerGroupCustomer } from "@models"
const CustomerGroup = model
.define("CustomerGroup", {
id: model.id({ prefix: "cusgroup" }).primaryKey(),
name: model.text().searchable(),
name: model.text().searchable().translatable(),
metadata: model.json().nullable(),
created_by: model.text().nullable(),
customers: model.manyToMany(() => Customer, {

View File

@@ -23,7 +23,9 @@
"author": "Medusa",
"license": "MIT",
"devDependencies": {
"@medusajs/framework": "2.12.5"
"@medusajs/framework": "2.12.5",
"@medusajs/types": "2.12.5",
"@medusajs/utils": "2.12.5"
},
"scripts": {
"watch": "yarn run -T tsc --build --watch",

View File

@@ -1,5 +1,7 @@
import {
Event,
// TODO: Comment temporarely and we will re enable it in the near future #14478
// EventBusEventsOptions,
InternalModuleDeclaration,
Logger,
Message,
@@ -53,6 +55,8 @@ export default class RedisEventBusService extends AbstractEventBusModuleService
protected readonly queueOptions_: Omit<QueueOptions, "connection">
protected readonly workerOptions_: Omit<WorkerOptions, "connection">
protected readonly jobOptions_: EmitOptions
// TODO: Comment temporarely and we will re enable it in the near future #14478
// private readonly eventOptions_: EventBusEventsOptions
protected queue_: Queue
protected bullWorker_: Worker
@@ -79,6 +83,11 @@ export default class RedisEventBusService extends AbstractEventBusModuleService
this.queueOptions_ = eventBusRedisQueueOptions ?? {}
this.workerOptions_ = eventBusRedisWorkerOptions ?? {}
this.jobOptions_ = eventBusRedisJobOptions ?? {}
// TODO: Comment temporarely and we will re enable it in the near future #14478
// this.eventOptions_ =
// _moduleOptions.eventOptions ??
// _moduleDeclaration.options?.eventOptions ??
// {}
this.queue_ = new Queue(this.queueName_, {
prefix: `${this.constructor.name}`,
@@ -153,6 +162,11 @@ export default class RedisEventBusService extends AbstractEventBusModuleService
...eventData.options,
}
// TODO: Comment temporarely and we will re enable it in the near future #14478
// finalOptions.priority =
// eventData.options?.priority ??
// this.eventOptions_[eventData.name]?.priority
if (
finalOptions.priority != undefined &&
(finalOptions.priority < 1 ||

View File

@@ -1,3 +1,10 @@
// TODO: Comment temporarely and we will re enable it in the near future #14478
// import type { EventBusEventsOptions } from "@medusajs/types"
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import type { ModuleOptions } from "@medusajs/types"
import {
BulkJobOptions,
Job,
@@ -68,6 +75,8 @@ export type EventBusRedisModuleOptions = {
* @see https://api.docs.bullmq.io/interfaces/BaseJobOptions.html
*/
jobOptions?: EmitOptions
// eventOptions?: EventBusEventsOptions
}
declare module "@medusajs/types" {

View File

@@ -4,8 +4,8 @@ import { ShippingOption } from "./shipping-option"
export const ShippingOptionType = model.define("shipping_option_type", {
id: model.id({ prefix: "sotype" }).primaryKey(),
label: model.text().searchable(),
description: model.text().searchable().nullable(),
label: model.text().searchable().translatable(),
description: model.text().searchable().translatable().nullable(),
code: model.text().searchable(),
shipping_options: model.hasMany(() => ShippingOption, {
mappedBy: "type",

View File

@@ -10,7 +10,7 @@ import { ShippingProfile } from "./shipping-profile"
export const ShippingOption = model
.define("shipping_option", {
id: model.id({ prefix: "so" }).primaryKey(),
name: model.text().searchable(),
name: model.text().searchable().translatable(),
price_type: model
.enum(ShippingOptionPriceType)
.default(ShippingOptionPriceType.FLAT),

View File

@@ -4,8 +4,8 @@ import Product from "./product"
const ProductCategory = model
.define("ProductCategory", {
id: model.id({ prefix: "pcat" }).primaryKey(),
name: model.text().searchable(),
description: model.text().searchable().default(""),
name: model.text().searchable().translatable(),
description: model.text().searchable().translatable().default(""),
handle: model.text().searchable(),
mpath: model.text(),
is_active: model.boolean().default(false),

View File

@@ -4,7 +4,7 @@ import Product from "./product"
const ProductCollection = model
.define("ProductCollection", {
id: model.id({ prefix: "pcol" }).primaryKey(),
title: model.text().searchable(),
title: model.text().searchable().translatable(),
handle: model.text(),
metadata: model.json().nullable(),
products: model.hasMany(() => Product, {

View File

@@ -4,7 +4,7 @@ import { ProductOption, ProductVariant } from "./index"
const ProductOptionValue = model
.define("ProductOptionValue", {
id: model.id({ prefix: "optval" }).primaryKey(),
value: model.text(),
value: model.text().translatable(),
metadata: model.json().nullable(),
option: model
.belongsTo(() => ProductOption, {

View File

@@ -5,7 +5,7 @@ import ProductOptionValue from "./product-option-value"
const ProductOption = model
.define("ProductOption", {
id: model.id({ prefix: "opt" }).primaryKey(),
title: model.text().searchable(),
title: model.text().searchable().translatable(),
metadata: model.json().nullable(),
product: model.belongsTo(() => Product, {
mappedBy: "options",

View File

@@ -6,7 +6,7 @@ const ProductTag = model
{ tableName: "product_tag", name: "ProductTag" },
{
id: model.id({ prefix: "ptag" }).primaryKey(),
value: model.text().searchable(),
value: model.text().searchable().translatable(),
metadata: model.json().nullable(),
products: model.manyToMany(() => Product, {
mappedBy: "tags",

View File

@@ -4,7 +4,7 @@ import { Product } from "@models"
const ProductType = model
.define("ProductType", {
id: model.id({ prefix: "ptyp" }).primaryKey(),
value: model.text().searchable(),
value: model.text().searchable().translatable(),
metadata: model.json().nullable(),
products: model.hasMany(() => Product, {
mappedBy: "type",

View File

@@ -5,7 +5,7 @@ import ProductVariantProductImage from "./product-variant-product-image"
const ProductVariant = model
.define("ProductVariant", {
id: model.id({ prefix: "variant" }).primaryKey(),
title: model.text().searchable(),
title: model.text().searchable().translatable(),
sku: model.text().searchable().nullable(),
barcode: model.text().searchable().nullable(),
ean: model.text().searchable().nullable(),
@@ -15,7 +15,7 @@ const ProductVariant = model
hs_code: model.text().nullable(),
origin_country: model.text().nullable(),
mid_code: model.text().nullable(),
material: model.text().nullable(),
material: model.text().translatable().nullable(),
weight: model.number().nullable(),
length: model.number().nullable(),
height: model.number().nullable(),

View File

@@ -11,10 +11,10 @@ import ProductVariant from "./product-variant"
const Product = model
.define("Product", {
id: model.id({ prefix: "prod" }).primaryKey(),
title: model.text().searchable(),
title: model.text().searchable().translatable(),
handle: model.text(),
subtitle: model.text().searchable().nullable(),
description: model.text().searchable().nullable(),
subtitle: model.text().searchable().translatable().nullable(),
description: model.text().searchable().translatable().nullable(),
is_giftcard: model.boolean().default(false),
status: model
.enum(ProductUtils.ProductStatus)
@@ -27,7 +27,7 @@ const Product = model
origin_country: model.text().nullable(),
hs_code: model.text().nullable(),
mid_code: model.text().nullable(),
material: model.text().nullable(),
material: model.text().translatable().nullable(),
discountable: model.boolean().default(true),
external_id: model.text().nullable(),
metadata: model.json().nullable(),

View File

@@ -3,7 +3,7 @@ import RegionCountry from "./country"
export default model.define("region", {
id: model.id({ prefix: "reg" }).primaryKey(),
name: model.text().searchable(),
name: model.text().searchable().translatable(),
currency_code: model.text().searchable(),
automatic_taxes: model.boolean().default(true),
countries: model.hasMany(() => RegionCountry),

View File

@@ -7,7 +7,7 @@ const TaxRate = model
id: model.id({ prefix: "txr" }).primaryKey(),
rate: model.float().nullable(),
code: model.text().searchable(),
name: model.text().searchable(),
name: model.text().searchable().translatable(),
is_default: model.boolean().default(false),
is_combinable: model.boolean().default(false),
tax_region: model.belongsTo(() => TaxRegion, {

View File

@@ -1,15 +1,42 @@
import { ITranslationModuleService } from "@medusajs/framework/types"
import { Module, Modules } from "@medusajs/framework/utils"
import { DmlEntity, Module, Modules } from "@medusajs/framework/utils"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import TranslationModuleService from "@services/translation-module"
import { createLocaleFixture, createTranslationFixture } from "../__fixtures__"
jest.setTimeout(100000)
// Set up the mock before module initialization
let mockGetTranslatableEntities: jest.SpyInstance
moduleIntegrationTestRunner<ITranslationModuleService>({
moduleName: Modules.TRANSLATION,
hooks: {
beforeModuleInit: async () => {
mockGetTranslatableEntities = jest.spyOn(
DmlEntity,
"getTranslatableEntities"
)
mockGetTranslatableEntities.mockReturnValue([
{
entity: "Product",
fields: ["title", "description", "subtitle", "material"],
},
{ entity: "ProductVariant", fields: ["title", "material"] },
{ entity: "ProductCategory", fields: ["name"] },
])
},
},
testSuite: ({ service }) => {
describe("Translation Module Service", () => {
beforeEach(async () => {
await service.__hooks?.onApplicationStart?.().catch(() => {})
})
afterAll(() => {
// Restore the mock after all tests complete
mockGetTranslatableEntities.mockRestore()
})
it(`should export the appropriate linkable configuration`, () => {
const linkable = Module(Modules.TRANSLATION, {
service: TranslationModuleService,

View File

@@ -1,12 +1,11 @@
import "./types"
import { Module } from "@medusajs/framework/utils"
import TranslationModuleService from "@services/translation-module"
import loadConfig from "./loaders/config"
import loadDefaults from "./loaders/defaults"
export const TRANSLATION_MODULE = "translation"
export default Module(TRANSLATION_MODULE, {
service: TranslationModuleService,
loaders: [loadDefaults, loadConfig],
loaders: [loadDefaults],
})

View File

@@ -1,60 +0,0 @@
import {
LoaderOptions,
Logger,
ModulesSdkTypes,
} from "@medusajs/framework/types"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { TRANSLATABLE_FIELDS_CONFIG_KEY } from "@utils/constants"
import { asValue } from "awilix"
import { translatableFieldsConfig } from "../utils/translatable-fields"
import Settings from "@models/settings"
import type { TranslationModuleOptions } from "../types"
export default async ({
container,
options,
}: LoaderOptions<TranslationModuleOptions>): Promise<void> => {
const logger =
container.resolve<Logger>(ContainerRegistrationKeys.LOGGER) ?? console
const settingsService: ModulesSdkTypes.IMedusaInternalService<
typeof Settings
> = container.resolve("translationSettingsService")
const mergedConfig: Record<string, string[]> = translatableFieldsConfig
const userProvidedFields = options?.entities ?? []
for (const field of userProvidedFields) {
mergedConfig[field.type] ??= []
mergedConfig[field.type] = Array.from(
new Set([...(mergedConfig[field.type] ?? []), ...field.fields])
)
}
try {
const existingSettings = await settingsService.list(
{},
{ select: ["id", "entity_type"] }
)
const existingByEntityType = new Map(
existingSettings.map((s) => [s.entity_type, s.id])
)
const settingsToUpsert = Object.entries(mergedConfig).map(
([entityType, fields]) => {
const existingId = existingByEntityType.get(entityType)
return existingId
? { id: existingId, entity_type: entityType, fields }
: { entity_type: entityType, fields }
}
)
const resp = await settingsService.upsert(settingsToUpsert)
logger.debug(`Loaded ${resp.length} translation settings`)
} catch (error) {
logger.warn(
`Failed to load translation settings, skipping loader. Original error: ${error.message}`
)
}
container.register(TRANSLATABLE_FIELDS_CONFIG_KEY, asValue(mergedConfig))
}

View File

@@ -293,6 +293,16 @@
"nullable": false,
"mappedType": "json"
},
"is_active": {
"name": "is_active",
"type": "boolean",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "true",
"mappedType": "boolean"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",

View File

@@ -0,0 +1,13 @@
import { Migration } from "@medusajs/framework/mikro-orm/migrations";
export class Migration20260108122757 extends Migration {
override async up(): Promise<void> {
this.addSql(`alter table if exists "translation_settings" add column if not exists "is_active" boolean not null default true;`);
}
override async down(): Promise<void> {
this.addSql(`alter table if exists "translation_settings" drop column if exists "is_active";`);
}
}

View File

@@ -18,6 +18,10 @@ const Settings = model
* ["title", "description", "material"]
*/
fields: model.json(),
/**
* Wether the entity translatable status is enabled.
*/
is_active: model.boolean().default(true),
})
.indexes([
{

View File

@@ -2,6 +2,7 @@ import { raw } from "@medusajs/framework/mikro-orm/core"
import {
Context,
CreateTranslationDTO,
CreateTranslationSettingsDTO,
DAL,
FilterableTranslationProps,
FindConfig,
@@ -9,21 +10,25 @@ import {
LocaleDTO,
ModulesSdkTypes,
TranslationTypes,
UpdateTranslationSettingsDTO,
} from "@medusajs/framework/types"
import { SqlEntityManager } from "@medusajs/framework/mikro-orm/postgresql"
import {
arrayDifference,
DmlEntity,
EmitEvents,
InjectManager,
MedusaContext,
MedusaError,
MedusaErrorTypes,
MedusaService,
normalizeLocale,
toSnakeCase,
} from "@medusajs/framework/utils"
import Locale from "@models/locale"
import Translation from "@models/translation"
import Settings from "@models/settings"
import { computeTranslatedFieldCount } from "@utils/compute-translated-field-count"
import { TRANSLATABLE_FIELDS_CONFIG_KEY } from "@utils/constants"
import { filterTranslationFields } from "@utils/filter-translation-fields"
type InjectedDependencies = {
@@ -33,7 +38,6 @@ type InjectedDependencies = {
translationSettingsService: ModulesSdkTypes.IMedusaInternalService<
typeof Settings
>
[TRANSLATABLE_FIELDS_CONFIG_KEY]: Record<string, string[]>
}
export default class TranslationModuleService
@@ -78,6 +82,55 @@ export default class TranslationModuleService
this.settingsService_ = translationSettingsService
}
__hooks = {
onApplicationStart: async () => {
return this.onApplicationStart_()
},
}
protected async onApplicationStart_() {
const translatableEntities = DmlEntity.getTranslatableEntities()
const translatableEntitiesSet = new Set(
translatableEntities.map((entity) => toSnakeCase(entity.entity))
)
const currentTranslationSettings = await this.settingsService_.list()
const currentTranslationSettingsSet = new Set(
currentTranslationSettings.map((setting) => setting.entity_type)
)
const settingsToUpsert: (
| CreateTranslationSettingsDTO
| UpdateTranslationSettingsDTO
)[] = []
for (const setting of currentTranslationSettings) {
if (
!translatableEntitiesSet.has(setting.entity_type) &&
setting.is_active
) {
settingsToUpsert.push({
id: setting.id,
is_active: false,
})
}
}
for (const entity of translatableEntities) {
const snakeCaseEntityType = toSnakeCase(entity.entity)
const hasCurrentSettings =
currentTranslationSettingsSet.has(snakeCaseEntityType)
if (!hasCurrentSettings) {
settingsToUpsert.push({
entity_type: snakeCaseEntityType,
fields: entity.fields,
})
}
}
await this.settingsService_.upsert(settingsToUpsert)
}
@InjectManager()
async getTranslatableFields(
entityType?: string,
@@ -90,7 +143,8 @@ export default class TranslationModuleService
sharedContext
)
return settings.reduce((acc, setting) => {
acc[setting.entity_type] = setting.fields as unknown as string[]
acc[toSnakeCase(setting.entity_type)] =
setting.fields as unknown as string[]
return acc
}, {} as Record<string, string[]>)
}
@@ -377,6 +431,42 @@ export default class TranslationModuleService
return Array.isArray(data) ? serialized : serialized[0]
}
@InjectManager()
@EmitEvents()
// @ts-expect-error
async createTranslationSettings(
data: CreateTranslationSettingsDTO[] | CreateTranslationSettingsDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
| TranslationTypes.TranslationSettingsDTO
| TranslationTypes.TranslationSettingsDTO[]
> {
const dataArray = Array.isArray(data) ? data : [data]
await this.validateSettings_(dataArray, sharedContext)
// @ts-expect-error TS can't match union type to overloads
return await super.createTranslationSettings(data, sharedContext)
}
@InjectManager()
@EmitEvents()
// @ts-expect-error
async updateTranslationSettings(
data: UpdateTranslationSettingsDTO | UpdateTranslationSettingsDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<
| TranslationTypes.TranslationSettingsDTO[]
| TranslationTypes.TranslationSettingsDTO
> {
const dataArray = Array.isArray(data) ? data : [data]
await this.validateSettings_(dataArray, sharedContext)
// @ts-expect-error TS can't match union type to overloads
return await super.updateTranslationSettings(data, sharedContext)
}
@InjectManager()
async getStatistics(
input: TranslationTypes.TranslationStatisticsInput,
@@ -492,4 +582,79 @@ export default class TranslationModuleService
return result
}
/**
* Validates the translation settings to create or update against the translatable entities and their translatable fields configuration.
* @param dataToValidate - The data to validate.
* @param sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
*/
@InjectManager()
protected async validateSettings_(
dataToValidate: (
| CreateTranslationSettingsDTO
| UpdateTranslationSettingsDTO
)[],
@MedusaContext() sharedContext: Context = {}
) {
const translatableEntities = DmlEntity.getTranslatableEntities()
const translatableEntitiesMap = new Map(
translatableEntities.map((entity) => [toSnakeCase(entity.entity), entity])
)
const invalidSettings: {
entity_type: string
is_invalid_entity: boolean
invalidFields?: string[]
}[] = []
for (const item of dataToValidate) {
let itemEntityType = item.entity_type
if (!itemEntityType) {
const translationSetting = await this.retrieveTranslationSettings(
//@ts-expect-error - if no entity_type, we are on an update
item.id,
{ select: ["entity_type"] },
sharedContext
)
itemEntityType = translationSetting.entity_type
}
const entity = translatableEntitiesMap.get(itemEntityType)
if (!entity) {
invalidSettings.push({
entity_type: itemEntityType,
is_invalid_entity: true,
})
} else {
const invalidFields = arrayDifference(item.fields ?? [], entity.fields)
if (invalidFields.length) {
invalidSettings.push({
entity_type: itemEntityType,
is_invalid_entity: false,
invalidFields,
})
}
}
}
if (invalidSettings.length) {
throw new MedusaError(
MedusaErrorTypes.INVALID_DATA,
"Invalid translation settings:\n" +
invalidSettings
.map(
(setting) =>
`- ${setting.entity_type} ${
setting.is_invalid_entity
? "is not a translatable entity"
: `doesn't have the following fields set as translatable: ${setting.invalidFields?.join(
", "
)}`
}`
)
.join("\n")
)
}
}
}

View File

@@ -1 +0,0 @@
export const TRANSLATABLE_FIELDS_CONFIG_KEY = "translatableFieldsConfig"

View File

@@ -1,41 +0,0 @@
export const PRODUCT_TRANSLATABLE_FIELDS = [
"title",
"description",
"material",
"subtitle",
]
export const PRODUCT_VARIANT_TRANSLATABLE_FIELDS = ["title", "material"]
export const PRODUCT_TYPE_TRANSLATABLE_FIELDS = ["value"]
export const PRODUCT_COLLECTION_TRANSLATABLE_FIELDS = ["title"]
export const PRODUCT_CATEGORY_TRANSLATABLE_FIELDS = ["name", "description"]
export const PRODUCT_TAG_TRANSLATABLE_FIELDS = ["value"]
export const PRODUCT_OPTION_TRANSLATABLE_FIELDS = ["title"]
export const PRODUCT_OPTION_VALUE_TRANSLATABLE_FIELDS = ["value"]
export const REGION_TRANSLATABLE_FIELDS = ["name"]
export const CUSTOMER_GROUP_TRANSLATABLE_FIELDS = ["name"]
export const SHIPPING_OPTION_TRANSLATABLE_FIELDS = ["name"]
export const SHIPPING_OPTION_TYPE_TRANSLATABLE_FIELDS = ["label", "description"]
export const TAX_RATE_TRANSLATABLE_FIELDS = ["name"]
// export const RETURN_REASON_TRANSLATABLE_FIELDS = [
// "value",
// "label",
// "description",
// ]
export const translatableFieldsConfig = {
product: PRODUCT_TRANSLATABLE_FIELDS,
product_variant: PRODUCT_VARIANT_TRANSLATABLE_FIELDS,
product_type: PRODUCT_TYPE_TRANSLATABLE_FIELDS,
product_collection: PRODUCT_COLLECTION_TRANSLATABLE_FIELDS,
product_category: PRODUCT_CATEGORY_TRANSLATABLE_FIELDS,
product_tag: PRODUCT_TAG_TRANSLATABLE_FIELDS,
product_option: PRODUCT_OPTION_TRANSLATABLE_FIELDS,
product_option_value: PRODUCT_OPTION_VALUE_TRANSLATABLE_FIELDS,
region: REGION_TRANSLATABLE_FIELDS,
customer_group: CUSTOMER_GROUP_TRANSLATABLE_FIELDS,
shipping_option: SHIPPING_OPTION_TRANSLATABLE_FIELDS,
shipping_option_type: SHIPPING_OPTION_TYPE_TRANSLATABLE_FIELDS,
tax_rate: TAX_RATE_TRANSLATABLE_FIELDS,
// return_reason: RETURN_REASON_TRANSLATABLE_FIELDS,
}

View File

@@ -0,0 +1 @@
// noop for typeRoots in compiler options

View File

@@ -0,0 +1 @@
// noop, for compiler options

Some files were not shown because too many files have changed in this diff Show More