Compare commits
10 Commits
8426fca710
...
3df466dadd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3df466dadd | ||
|
|
c5b919850c | ||
|
|
8890f28470 | ||
|
|
250b6fdf22 | ||
|
|
41e1d5e506 | ||
|
|
1347698876 | ||
|
|
cbc9f3d059 | ||
|
|
73631604cc | ||
|
|
d60ea7268a | ||
|
|
42235825ee |
5
.changeset/bold-moons-hear.md
Normal file
5
.changeset/bold-moons-hear.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/cli": patch
|
||||
---
|
||||
|
||||
feat(medusa-cli): add codemod command + codemod for replacing zod imports
|
||||
10
.changeset/cold-lamps-search.md
Normal file
10
.changeset/cold-lamps-search.md
Normal 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.
|
||||
13
.changeset/fine-wings-stick.md
Normal file
13
.changeset/fine-wings-stick.md
Normal 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
|
||||
6
.changeset/kind-rivers-fly.md
Normal file
6
.changeset/kind-rivers-fly.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
"@medusajs/framework": patch
|
||||
---
|
||||
|
||||
feat(medusa): Prevent build command from throwing on missing config
|
||||
5
.changeset/lucky-buttons-yell.md
Normal file
5
.changeset/lucky-buttons-yell.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/dashboard": patch
|
||||
---
|
||||
|
||||
fix(dashboard): filter feed channel notifications in admin dashboard
|
||||
5
.changeset/metal-maps-tickle.md
Normal file
5
.changeset/metal-maps-tickle.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/utils": patch
|
||||
---
|
||||
|
||||
fix(utils): fix import of caching and translation modules
|
||||
5
.changeset/slow-spies-agree.md
Normal file
5
.changeset/slow-spies-agree.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/core-flows": patch
|
||||
---
|
||||
|
||||
chore(core-flows): Emit cart updated event on `deleteLineItemsWorkflow`
|
||||
5
.changeset/upset-keys-deny.md
Normal file
5
.changeset/upset-keys-deny.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/core-flows": patch
|
||||
---
|
||||
|
||||
feat(core-flows): Allow payment session status captured to be processable upon cart completion
|
||||
226
.claude/skills/api-ref-doc/SKILL.md
Normal file
226
.claude/skills/api-ref-doc/SKILL.md
Normal 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
|
||||
295
.claude/skills/book-doc/SKILL.md
Normal file
295
.claude/skills/book-doc/SKILL.md
Normal 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
|
||||
170
.claude/skills/how-to/SKILL.md
Normal file
170
.claude/skills/how-to/SKILL.md
Normal 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
|
||||
216
.claude/skills/recipe/SKILL.md
Normal file
216
.claude/skills/recipe/SKILL.md
Normal 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
|
||||
448
.claude/skills/resources-doc/SKILL.md
Normal file
448
.claude/skills/resources-doc/SKILL.md
Normal 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
|
||||
358
.claude/skills/tutorial/SKILL.md
Normal file
358
.claude/skills/tutorial/SKILL.md
Normal 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
|
||||
236
.claude/skills/ui-component-doc/SKILL.md
Normal file
236
.claude/skills/ui-component-doc/SKILL.md
Normal 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
|
||||
@@ -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 })
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 })
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -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(
|
||||
{},
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
35
packages/cli/medusa-cli/src/codemods/__tests__/index.test.ts
Normal file
35
packages/cli/medusa-cli/src/codemods/__tests__/index.test.ts
Normal 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")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
26
packages/cli/medusa-cli/src/codemods/index.ts
Normal file
26
packages/cli/medusa-cli/src/codemods/index.ts
Normal 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)
|
||||
}
|
||||
165
packages/cli/medusa-cli/src/codemods/replace-zod-imports.ts
Normal file
165
packages/cli/medusa-cli/src/codemods/replace-zod-imports.ts
Normal 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 {}
|
||||
}
|
||||
}
|
||||
45
packages/cli/medusa-cli/src/codemods/types.ts
Normal file
45
packages/cli/medusa-cli/src/codemods/types.ts
Normal 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>
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
@@ -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 })
|
||||
}
|
||||
)
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import "@medusajs/types"
|
||||
import "@medusajs/utils"
|
||||
import "../types/container"
|
||||
|
||||
export * from "@medusajs/utils"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
@@ -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
|
||||
>
|
||||
|
||||
@@ -12,6 +12,7 @@ export default async function build({
|
||||
}) {
|
||||
const container = await initializeContainer(directory, {
|
||||
skipDbConnection: true,
|
||||
throwOnError: false,
|
||||
})
|
||||
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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";`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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([
|
||||
{
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const TRANSLATABLE_FIELDS_CONFIG_KEY = "translatableFieldsConfig"
|
||||
@@ -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,
|
||||
}
|
||||
1
packages/plugins/draft-order/index.d.ts
vendored
Normal file
1
packages/plugins/draft-order/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
// noop for typeRoots in compiler options
|
||||
1
packages/plugins/draft-order/src/index.ts
Normal file
1
packages/plugins/draft-order/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
// noop, for compiler options
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user