docs: updates to use DML and other changes (#7834)

- Change existing data model guides and add new ones for DML
- Change module's docs around service factory + remove guides that are now necessary
- Hide/remove all mentions of module relationships, or label them as coming soon.
- Change all data model creation snippets to use DML
- use `property` instead of `field` when referring to a data model's properties.
- Fix all snippets in commerce module guides to use new method suffix (no more main model methods)
- Rework recipes, removing/hiding a lot of sections as a lot of recipes are incomplete with the current state of DML.


### Other changes

- Highlight fixes in some guides
- Remove feature flags guide
- Fix code block styles when there are no line numbers.

### Upcoming changes in other PRs

- Re-generate commerce module references (for the updates in the method names)
- Ensure that the data model references are generated correctly for models using DML.
- (probably at a very later point) revisit recipes
This commit is contained in:
Shahed Nasser
2024-06-26 07:55:59 +00:00
committed by GitHub
parent 62dacdda75
commit 0462cc5acf
126 changed files with 1808 additions and 14242 deletions
@@ -75,9 +75,12 @@ Widgets that are injected into a details page (for example, `product.details.aft
For example:
```tsx title="src/admin/widgets/product-widget.tsx" highlights={[["5"]]}
```tsx title="src/admin/widgets/product-widget.tsx" highlights={[["8"]]}
import { defineWidgetConfig } from "@medusajs/admin-shared"
import { DetailWidgetProps, AdminProduct } from "@medusajs/types"
import {
DetailWidgetProps,
AdminProduct,
} from "@medusajs/types"
const ProductWidget = ({
data,
@@ -41,8 +41,8 @@ To create an API route that accepts multiple path parameters, create within the
For example, create an API route at `src/api/store/hello-world/[id]/name/[name]/route.ts`:
export const multiplePathHighlights = [
["11", "req.params.id", "Access the path parameter `id`"],
["11", "req.params.name", "Access the path parameter `name`"]
["12", "req.params.id", "Access the path parameter `id`"],
["13", "req.params.name", "Access the path parameter `name`"]
]
```ts title="src/api/store/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights}
@@ -56,7 +56,9 @@ export const GET = (
res: MedusaResponse
) => {
res.json({
message: `[GET] Hello ${req.params.id} - ${req.params.name}!`,
message: `[GET] Hello ${
req.params.id
} - ${req.params.name}!`,
})
}
```
@@ -125,12 +125,15 @@ To protect custom API Routes that dont start with `/store/customers/me` or `/
For example:
export const highlights = [
["8", "authenticate", "Only authenticated admin users can access routes starting with `/custom/admin`"],
["14", "authenticate", "Only authenticated customers can access routes starting with `/custom/customers`"]
["11", "authenticate", "Only authenticated admin users can access routes starting with `/custom/admin`"],
["17", "authenticate", "Only authenticated customers can access routes starting with `/custom/customers`"]
]
```ts title="src/api/middlewares.ts" highlights={highlights}
import { MiddlewaresConfig, authenticate } from "@medusajs/medusa"
import {
MiddlewaresConfig,
authenticate,
} from "@medusajs/medusa"
export const config: MiddlewaresConfig = {
routes: [
@@ -1,89 +0,0 @@
export const metadata = {
title: `${pageNumber} Common Data Model Definitions`,
}
# {metadata.title}
In this chapter, you'll learn where to find resources on common data model definitions related to field's column types, relations, and indices.
## MikroORM Learning Resources
Refer to the following resources to learn about common definitions:
1. [Field column types](https://mikro-orm.io/docs/defining-entities).
2. [Creating indices](https://mikro-orm.io/docs/defining-entities#indexes).
Refer to MikroORM's documentation for more details related to your use case.
---
## Relations Between Data Models
You can build relations between data models in the same module using foreign keys.
Refer to [MikroORM's relations definitions](https://mikro-orm.io/docs/relationships) for more details.
<Note>
For building relationships between data models in different modules, refer to the [Module Relationships chapter](../../modules/module-relationships/page.mdx) instead.
</Note>
---
## Example Data Model
The following example showcase a data model with common definitions:
```ts collapsibleLines="1-10" expandButtonLabel="Show Imports"
import {
Entity,
Enum,
OneToOne,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { BaseEntity } from "@medusajs/utils"
// assuming this is another implemented data model
import ProductVariant from "./product-variant"
export enum MediaType {
MAIN = "main",
PREVIEW = "preview"
}
@Entity()
class ProductMedia extends BaseEntity {
@PrimaryKey({ columnType: "text" })
id: string
@Property({ columnType: "text" })
name: string
@Enum({ items: ["main", "preview"] })
type: MediaType
@Property({ columnType: "text" })
file_key: string
@Property({ columnType: "text" })
mime_type: string
@Property({ columnType: "text" })
variant_id: string
@OneToOne({
entity: ProductVariant,
onDelete: "cascade",
})
variant: ProductVariant
}
export default ProductMedia
```
In the example above:
- The `ProductMedia` data model has the columns `id`, `name`, `type`, `file_key`, `mime_type`, and `variant_id`.
- The data model has an index on the `variant_id` column.
- The data model has a one-to-one relation to a `ProductVariant` data model.
@@ -0,0 +1,84 @@
export const metadata = {
title: `${pageNumber} Configure Data Model Properties`,
}
# {metadata.title}
In this chapter, youll learn how to configure data model properties, such as setting their default value.
## Propertys Default Value
Use the `default` method of the `model` utility to specify the default value of a property.
For example:
export const defaultHighlights = [
["6", "default", "Set the default value to `black`."],
["9", "default", "Set the default value to `0`."]
]
```ts highlights={defaultHighlights}
import { model } from "@medusajs/utils"
const MyCustom = model.define("my_custom", {
color: model
.enum(["black", "white"])
.default("black"),
age: model
.number()
.default(0),
// ...
})
export default MyCustom
```
In this example, you set the default value of the `color` enum property to `black`, and that of the `age` number property to `0`.
---
## Nullable Property
Use the `nullable` method to indicate that a propertys value can be `null`.
For example:
export const nullableHighlights = [
["4", "nullable", "Configure the `price` property to allow `null` values."]
]
```ts highlights={nullableHighlights}
import { model } from "@medusajs/utils"
const MyCustom = model.define("my_custom", {
price: model.bigNumber().nullable(),
// ...
})
export default MyCustom
```
---
## Unique Property
The `unique` method indicates that a propertys value must be unique in the database.
For example:
export const uniqueHighlights = [
["5", "unique", "Configure the `email` property to allow unique values only."]
]
```ts highlights={uniqueHighlights}
import { model } from "@medusajs/utils"
const User = model.define("user", {
email: model.text().unique(),
// ...
})
export default User
```
In this example, multiple users cant have the same email.
@@ -0,0 +1,32 @@
export const metadata = {
title: `${pageNumber} Data Model Indexes`,
}
# {metadata.title}
In this chapter, youll learn how to define indexes on a data model.
## Define Index
The `index` method defines a custom index on a property.
For example:
export const highlights = [
["5", "index", "Define an index on the `name` property."]
]
```ts highlights={highlights}
import { model } from "@medusajs/utils"
const MyCustom = model.define("my_custom", {
id: model.id(),
name: model.text().index(
"IDX_MY_CUSTOM_NAME"
),
})
export default MyCustom
```
In this example, you define an index on the `name` property. The `index` method optionally accepts the name of the index as a parameter.
@@ -0,0 +1,40 @@
export const metadata = {
title: `${pageNumber} Data Models Primary Key`,
}
# {metadata.title}
In this chapter, youll learn how to configure the primary key of a data model.
## id Property
A property defined with the `id` method is, by default, considered the data models primary key.
---
## primaryKey Method
To set any `text` or `number` property as a primary key, use the `primaryKey` method.
For example:
export const highlights = [
["4", "primaryKey", "Define the `code` property to be the data model's primary key."]
]
```ts highlights={highlights}
import { model } from "@medusajs/utils"
const MyCustom = model.define("my_custom", {
code: model.text().primaryKey(),
// ...
})
export default MyCustom
```
<Note>
A property thats defined with the `primaryKey` method takes precedence over an `id` property.
</Note>
@@ -0,0 +1,198 @@
export const metadata = {
title: `${pageNumber} Data Model Property Types`,
}
# {metadata.title}
In this chapter, youll learn about the types of properties in a data models schema.
## id
The `id` method defines an automatically generated string ID property.
For example:
export const idHighlights = [["4", ".id()", "Define an `id` property."]]
```ts highlights={idHighlights}
import { model } from "@medusajs/utils"
const MyCustom = model.define("my_custom", {
id: model.id(),
// ...
})
export default MyCustom
```
By default, this property is considered to be the data models primary key.
---
## text
The `text` method defines a string property.
For example:
export const textHighlights = [["4", "text", "Define a `text` property."]]
```ts highlights={textHighlights}
import { model } from "@medusajs/utils"
const MyCustom = model.define("my_custom", {
name: model.text(),
// ...
})
export default MyCustom
```
---
## number
The `number` method defines a number property.
For example:
export const numberHighlights = [["4", "number", "Define a `number` property."]]
```ts highlights={numberHighlights}
import { model } from "@medusajs/utils"
const MyCustom = model.define("my_custom", {
age: model.number(),
// ...
})
export default MyCustom
```
---
## bigNumber
The `bigNumber` method defines a number property that expects large numbers, such as prices.
For example:
export const bigNumberHighlights = [["4", "bigNumber", "Define a `bigNumber` property."]]
```ts highlights={bigNumberHighlights}
import { model } from "@medusajs/utils"
const MyCustom = model.define("my_custom", {
price: model.bigNumber(),
// ...
})
export default MyCustom
```
---
## boolean
The `boolean` method defines a boolean property.
For example:
export const booleanHighlights = [["4", "boolean", "Define a `boolean` property."]]
```ts highlights={booleanHighlights}
import { model } from "@medusajs/utils"
const MyCustom = model.define("my_custom", {
hasAccount: model.boolean(),
// ...
})
export default MyCustom
```
---
### enum
The `enum` method defines a property whose value can only be one of the specified values.
For example:
export const enumHighlights = [["4", "enum", "Define a `enum` property."]]
```ts highlights={enumHighlights}
import { model } from "@medusajs/utils"
const MyCustom = model.define("my_custom", {
color: model.enum(["black", "white"]),
// ...
})
export default MyCustom
```
The `enum` method accepts an array of possible string values.
---
## dateTime
The `dateTime` method defines a timestamp property.
For example:
export const dateTimeHighlights = [["4", "dateTime", "Define a `dateTime` property."]]
```ts highlights={dateTimeHighlights}
import { model } from "@medusajs/utils"
const MyCustom = model.define("my_custom", {
date_of_birth: model.dateTime(),
// ...
})
export default MyCustom
```
---
## json
The `json` method defines a property whose value is a stringified JSON object.
For example:
export const jsonHighlights = [["4", "json", "Define a `json` property."]]
```ts highlights={jsonHighlights}
import { model } from "@medusajs/utils"
const MyCustom = model.define("my_custom", {
metadata: model.json(),
// ...
})
export default MyCustom
```
---
## array
The `array` method defines an array or strings property.
For example:
export const arrHightlights = [["4", "array", "Define an `array` property."]]
```ts highlights={arrHightlights}
import { model } from "@medusajs/utils"
const MyCustom = model.define("my_custom", {
names: model.array(),
// ...
})
export default MyCustom
```
@@ -0,0 +1,190 @@
export const metadata = {
title: `${pageNumber} Data Model Relationships`,
}
# {metadata.title}
In this chapter, youll learn how to define relationships between data models in your module.
## What is a Relationship Property?
A relationship property is defined using relation methods, such as `hasOne` or `belongsTo`. It represents a relationship between two data models in a module.
---
## One-to-One Relationship
To define a one-to-one relationship, create relationship properties in the data models using the following methods:
1. `hasOne`: indicates that the model has one record of the specified model.
2. `belongsTo`: indicates that the model belongs to one record of the specified model.
For example:
export const oneToOneHighlights = [
["5", "hasOne", "A user has one email."],
["10", "belongsTo", "An email belongs to a user."],
["11", `"email"`, "The relationship's name in the `User` data model."]
]
```ts highlights={oneToOneHighlights}
import { model } from "@medusajs/utils"
const User = model.define("user", {
id: model.id(),
email: model.hasOne(() => Email),
})
const Email = model.define("email", {
id: model.id(),
user: model.belongsTo(() => User, {
mappedBy: "email",
}),
})
```
The `hasOne` and `belongsTo` methods accept a function as a first parameter. The function returns the associated data model.
The `belongsTo` method also requires passing as a second parameter an object with the property `mappedBy`. Its value is the name of the relationship property in the other data model.
In the example above, a user has one email, and an email belongs to one user.
---
## One-to-Many Relationship
To define a one-to-many relationship, create relationship properties in the data models using the following methods:
1. `hasMany`: indicates that the model has more than one records of the specified model.
2. `belongsTo`: indicates that the model belongs to one record of the specified model.
For example:
export const oneToManyHighlights = [
["5", "hasMany", "A store has many products"],
["10", "belongsTo", "A product has one store."],
["11", `"products"`, "The relationship's name in the `Store` data model."]
]
```ts highlights={oneToManyHighlights}
import { model } from "@medusajs/utils"
const Store = model.define("store", {
id: model.id(),
products: model.hasMany(() => Product),
})
const Product = model.define("product", {
id: model.id(),
store: model.belongsTo(() => Store, {
mappedBy: "products",
}),
})
```
In this example, a store has many products, but a product belongs to one store.
---
## Many-to-Many Relationship
To define a many-to-many relationship, create relationship properties in the data models using the `manyToMany` method.
For example:
export const manyToManyHighlights = [
["5", "manyToMany", "An order is associated with many products."],
["10", "manyToMany", "A product is associated with many orders."]
]
```ts highlights={manyToManyHighlights}
import { model } from "@medusajs/utils"
const Order = model.define("order", {
id: model.id(),
products: model.manyToMany(() => Product),
})
const Product = model.define("product", {
id: model.id(),
order: model.manyToMany(() => Order),
})
```
In this example, an order is associated with many products, and a product is associated with many orders.
---
## Configure Relationship Property Name
The relationship property methods accept as a second parameter an object of options. The `mappedBy` property defines the name of the relationship in the other data model.
As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method.
For example:
export const relationNameHighlights = [
["6", `"owner"`, "The relationship's name in the `Email` data model."],
["13", `"email"`, "The relationship's name in the `User` data model."]
]
```ts highlights={relationNameHighlights}
import { model } from "@medusajs/utils"
const User = model.define("user", {
id: model.id(),
email: model.hasOne(() => Email, {
mappedBy: "owner",
}),
})
const Email = model.define("email", {
id: model.id(),
owner: model.belongsTo(() => User, {
mappedBy: "email",
}),
})
```
In this example, you specify in the `User` data models relationship property that the name of the relationship in the `Email` data model is `owner`.
This is useful if the relationship propertys name is different than that of the associated data model.
---
## Cascades
When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it.
For example, if a store is deleted, its products should also be deleted.
The `cascades` method used on a data model configures which child records an operation is cascaded to.
For example:
export const highlights = [
["8", "", "When a store is deleted, delete its associated products."]
]
```ts highlights={highlights}
import { model } from "@medusajs/utils"
const Store = model.define("store", {
id: model.id(),
products: model.hasMany(() => Product),
})
.cascades({
delete: ["products"],
})
const Product = model.define("product", {
id: model.id(),
store: model.belongsTo(() => Store, {
mappedBy: "products",
}),
})
```
The `cascades` method accepts an object. Its key is the operations name, such as `delete`. The value is an array of relationship property names that the operation is cascaded to.
In the example above, when a store is deleted, its associated products are also deleted.
@@ -0,0 +1,46 @@
export const metadata = {
title: `${pageNumber} Searchable Data Model Property`,
}
# {metadata.title}
In this chapter, you'll learn what a searchable property is and how to define it.
## What is a Searchable Property?
Methods generated by the [service factory](../../modules/service-factory/page.mdx) that accept filters, such as `list{ModelName}s`, accept a `q` property as part of the filters.
When the `q` filter is passed, the query is applied on searchable properties in a data model.
---
## Define a Searchable Property
The `searchable` method of the `model` utility indicates that a `text` property is searchable.
For example:
```ts
import { model } from "@medusajs/utils"
const MyCustom = model.define("my_custom", {
name: model.text().searchable(),
// ...
})
export default MyCustom
```
In this example, the `name` property is searchable.
### Search Example
If you pass a `q` filter to the `listMyCustoms` method:
```ts
const myCustoms = await helloModuleService.listMyCustoms({
q: "John",
})
```
The `q` filter is applied on the `name` property of the `MyCustom` records.
@@ -1,99 +1,19 @@
export const metadata = {
title: `${pageNumber} Soft-Deletable Models`,
title: `${pageNumber} Soft-Deletable Data Models`,
}
# {metadata.title}
In this document, you'll learn how to create soft-deletable data models.
In this chapter, youll learn about soft-deletable data models.
## What is a Soft-Deletable Model?
## What is a Soft-Deletable Data Model?
A soft-deletable data model is a model whose records aren't actually removed from the database when they're deleted.
A soft-deletable data model is a model that has a `deleted_at` `dateTime` property.
Instead, their `deleted_at` field is set to the date the record was deleted.
When retrieving or listing records of that data model, records having their `deleted_at` field set aren't retrieved unless the `withDeleted` filter is provided.
When a record of the data model is deleted, this field is set to the current date, marking it as deleted.
---
## How to Create a Soft-Deletable Model?
## Configure Data Model Soft-Deletion
To create a soft-deletable model, first, add the following filter decorator to the data model class:
```ts title="src/module/hello/models/my-soft-deletable.ts" highlights={[["7"]]} collapsibleLines="1-7" expandButtonLabel="Show Imports"
// other imports...
import { Entity, Filter } from "@mikro-orm/core"
import { DALUtils } from "@medusajs/utils"
import { BaseEntity } from "@medusajs/utils"
@Entity()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class MySoftDeletable extends BaseEntity {
// ...
}
export default MySoftDeletable
```
Then, add a `deleted_at` field to the data model:
```ts highlights={[["7"], ["8"]]}
// other imports...
import { Property } from "@mikro-orm/core"
class MySoftDeletable extends BaseEntity {
// ...
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null = null
}
```
---
## Manage Soft-Deletable Models
Services extending the service factory have methods to soft delete and restore records for all models specified during its creation.
### Soft Delete a Record
For example, to soft delete a `MySoftDeletable` record:
```ts
await helloModuleService.softDelete([
"id_123", "id_321",
])
```
The method receives an array of IDs of records to delete.
### Retrieve Soft-Deleted Records
The `retrieve`, `list`, and `listAndCount` methods accept as a second parameter a configuration object.
To retrieve soft-deleted records, set `withDeleted` to `true` in the configuration object passed as a second parameter.
For example:
```ts
const deletedRecords = await helloModuleService
.listMySoftDeletables({
// ...
}, {
withDeleted: true,
})
```
### Restore a Soft-Deleted Record
To restore a `MySoftDeletable` record:
```ts
await helloModuleService.restore([
"id_123", "id_321",
])
```
The method also receives an array of IDs of records to restore.
If the data model isn't the main data model, its method names are `softDelete` and `restore` suffixed with the plural name of the model. For example, `softDeleteMySoftDeletable`.
By default, all data models have a `deleted_at` property and are considered soft-deletable.
@@ -4,24 +4,17 @@ export const metadata = {
# {metadata.title}
In this chapter, you'll learn about the module's container and how to register resources in that container.
In this chapter, you'll learn about the module's container and how to resolve resources in that container.
## Module's Container
Each module has a local container only used by the resources of that module.
So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container.
So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container, such as:
---
- `logger`: A utility to log message in the Medusa application's logs.
## Resources Registered in the Module's Container
Some resources registered in the module's container are:
- The module's main service.
- A generated service for each data model in your module. The registration name is the camel-case data model name suffixed by `Service`. For example, `myCustomService`.
![Example of registered resources in the container](https://res.cloudinary.com/dza7lstvk/image/upload/v1714400573/Medusa%20Book/modules-container_mkcbaq.jpg)
{/* TODO add other relevant resources, such as event bus */}
---
@@ -33,23 +26,25 @@ A service's constructor accepts as a first parameter an object used to resolve r
For example:
```ts highlights={[["5"], ["12"]]}
import { ModulesSdkTypes } from "@medusajs/types"
import { MyCustom } from "./models/my-custom"
```ts highlights={[["4"], ["10"]]}
import { Logger } from "@medusajs/medusa"
type InjectedDependencies = {
myCustomService: ModulesSdkTypes.InternalModuleService<any>
logger: Logger
}
export default class HelloModuleService {
protected myCustomService_:
ModulesSdkTypes.InternalModuleService<MyCustom>
constructor({ myCustomService }: InjectedDependencies) {
this.myCustomService_ = myCustomService
protected logger_: Logger
constructor({ logger }: InjectedDependencies) {
this.logger_ = logger
this.logger_.info("[HelloModuleService]: Hello World!")
}
// ...
}
```
### Loader
@@ -58,16 +53,18 @@ A loader function in a module accepts as a parameter an object having the proper
For example:
```ts highlights={[["8"]]}
```ts highlights={[["9"]]}
import {
LoaderOptions,
} from "@medusajs/modules-sdk"
import { Logger } from "@medusajs/medusa"
export default function helloWorldLoader({
container,
}: LoaderOptions) {
const myCustomService = container.resolve("myCustomService")
// ...
const logger: Logger = container.resolve("logger")
logger.info("[helloWorldLoader]: Hello, World!")
}
```
@@ -1,68 +0,0 @@
export const metadata = {
title: `${pageNumber} Database Operations in Service Methods`,
}
# {metadata.title}
In this document, youll learn how to implement database operations, such as creating a record, in the main service.
## Use the Data Model's Generated Service
To perform database operations on a data model, use the model's generated service in the module's container.
For example:
export const highlights = [
["13", "", "Inject myCustomService, which is the generated service of the `MyCustom` data model."],
["22", "", "Add a new field for the generated service of the MyCustom data model."],
["29", "", "Set the class field to the injected dependency."],
["35", "create", "Use the `create` method of the generated service."]
]
```ts title="src/modules/hello/service.ts" highlights={highlights}
// other imports...
import { ModulesSdkTypes } from "@medusajs/types"
import { MyCustom } from "./models/my-custom"
// ...
// recommended to define type in another file
type CreateMyCustomDTO = {
name: string
}
type InjectedDependencies = {
myCustomService: ModulesSdkTypes.InternalModuleService<any>
}
class HelloModuleService extends ModulesSdkUtils
.abstractModuleServiceFactory<
// ...
>(
// ...
) {
protected myCustomService_: ModulesSdkTypes.InternalModuleService<MyCustom>
constructor(
{ myCustomService }: InjectedDependencies
) {
// @ts-ignore
super(...arguments)
this.myCustomService_ = myCustomService
}
async create(
data: CreateMyCustomDTO
): Promise<MyCustomDTO> {
const myCustom = await this.myCustomService_.create(
data
)
return myCustom
}
}
```
In the above example, you resolve `myCustomService` in the main service's constructor. The `myCustomService` is the generated service for the `myCustom` data model.
Then, in the `create` method of the main service, you use `myCustomService`'s `create` method to create the record.
@@ -14,7 +14,7 @@ For example, Medusa has a link module that defines a relationship between the Pr
![Diagram showcasing the link module between the Product and Pricing modules](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651569/Medusa%20Resources/product-pricing_vlxsiq.jpg)
Link modules provide more flexibility in managing relationships between modules while maintaining module isolation. The Medusa application only creates the link tables when both modules are available.
Link modules create the relationship between modules while maintaining module isolation. The Medusa application only creates the link tables when both modules are available.
<Note type="soon">
@@ -1,275 +0,0 @@
import { TypeList, CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `${pageNumber} Module Relationships`,
}
# {metadata.title}
In this document, youll learn about creating relationships between modules.
## What is a Module Relationship?
A module can have a relationship to another module in the form of a reference.
The Medusa application resolves these relationships while maintaining isolation between the modules and allowing you to retrieve data across them.
<Note title="Use module relationships when" type="success">
- You want to build relationships between the data models of modules.
- You want to assoaciate more fields with the data model of another module.
</Note>
<Note title="Don't use module relationships if" type="error">
- Youre building relationships between data models in the same module. Use foreign keys instead.
</Note>
---
## How to Create a Module Relationship?
<Note title="Steps Summary">
1. Define a `__joinerConfig` method in the module's main service.
2. Configure module to be queryable.
</Note>
Consider youre creating a data model that adds custom fields associated with a product:
```ts title="src/modules/hello/models/custom-product-data.ts" highlights={[["17"]]} collapsibleLines="1-8" expandButtonLabel="Show Imports"
import { BaseEntity } from "@medusajs/utils"
import {
Entity,
PrimaryKey,
Property,
} from "@mikro-orm/core"
@Entity()
export class CustomProductData extends BaseEntity {
@PrimaryKey({ columnType: "text" })
id!: string
@Property({ columnType: "text" })
custom_field: string
@Property({ columnType: "text", nullable: true })
product_id?: string
}
```
The `CustomProductData` data model has a `product_id` field to reference the product it adds custom fields for.
<Note title="Tip">
When you add a new data model, make sure to:
- [Create a migration for it.](../../../basics/data-models/page.mdx#create-a-migration)
- [Add it to the second parameter of the main service's factory function.](../service-factory/page.mdx#abstractModuleServiceFactory-parameters)
</Note>
### 1. Define `__joinerConfig` Method
To create a relationship to the `product` data model of the Product Module, create a public `__joinerConfig` method in your main module's service:
export const relationshipsHighlight = [
["39", "serviceName", "The name of the module that this relationship is referencing."],
["40", "alias", "The alias of the data model youre referencing in the other module."],
["41", "primaryKey", "The name of the field youre referencing in the other modules data model."],
["42", "foreignKey", "The name of the field in your data models referencing the other modules model."],
]
```ts title="src/modules/hello/service.ts" highlights={relationshipsHighlight} collapsibleLines="1-6" expandButtonLabel="Show Imports"
// other imports...
import { MyCustom } from "./models/custom-product-data"
import { CustomProductData } from "./models/custom-product-data"
import { ModuleJoinerConfig } from "@medusajs/types"
import { Modules } from "@medusajs/modules-sdk"
class HelloModuleService extends ModulesSdkUtils
.abstractModuleServiceFactory<
// ...
>(
// ...
) {
// ...
__joinerConfig(): ModuleJoinerConfig {
return {
serviceName: "helloModuleService",
alias: [
{
name: ["my_custom"],
args: {
entity: MyCustom.name,
},
},
{
name: ["custom_product_data"],
args: {
entity: CustomProductData.name,
// Only needed if data model isn't main data model
// of service
methodSuffix: "CustomProductDatas",
},
},
],
relationships: [
{
serviceName: Modules.PRODUCT,
alias: "product",
primaryKey: "id",
foreignKey: "product_id",
},
],
}
}
// ...
}
```
This creates a relationship to the `Product` data model of the Product Module using the alias `product`. The `product_id` fields in your data models are considered references to the `id` field of the `Product` data model.
#### `__joinerConfig` Return Type
<TypeList types={[
{
name: "serviceName",
type: "`string`",
optional: false,
description: "The name of your module (as added in `medusa-config.js`)."
},
{
name: "alias",
type: "`object[]`",
description: "The alias definitions for each data model in your module. This allows other modules to reference your module's data models in relationships.",
children: [
{
name: "name",
type: "`string[]`",
description: "The alias names of a data model used later when fetching or referencing the data model."
},
{
name: "args",
type: "`object`",
description: "The alias's arguments.",
children: [
{
name: "entity",
type: "string",
description: "The name of the data model this alias is defined for."
},
{
name: "methodSuffix",
type: "string",
description: "The plural name of the data model. This is only required if the data model isn't the main data model of the module's service."
}
]
}
]
},
{
name: "relationships",
type: "`object[]`",
description: "Your module's relationships to other modules.",
children: [
{
name: "serviceName",
type: "`string`",
optional: false,
description: "The name of the module (as added in `medusa-config.js`) that this relationship is referencing. When referencing a Medusa commerce module, use the `Modules` enum imported from `@medusajs/modules-sdk`."
},
{
name: "alias",
type: "`string`",
optional: false,
description: "The alias of the data model youre referencing in the other module. You can find it in the `__joinerConfig` method of the other module's service."
},
{
name: "primaryKey",
type: "`string`",
optional: false,
description: "The name of the field youre referencing in the other modules data model."
},
{
name: "foreignKey",
type: "`string`",
optional: false,
description: "The name of the field in your data models that references the other modules data model."
}
]
}
]} sectionTitle="Define __joinerConfig Method" />
### 2. Adjust Module Configuration
To use relationships in a module, adjust its configuration object passed to `modules` in `medusa-config.js`:
export const configHighlights = [
["7", "isQueryable", "Enable this property to use relationships in a module."]
]
```js title="medusa-config.js" highlights={configHighlights}
module.exports = defineConfig({
// ...
modules: {
helloModuleService: {
// ...
definition: {
isQueryable: true,
},
},
},
})
```
Enabling the `isQueryable` property is required to use relationships in a module.
---
## Reference Inner Data Models
If the data model youre referencing isnt the main data model of the main module service, pass to the relationship definition the `args` property:
```ts title="src/modules/hello/service.ts" highlights={[["20", "methodSuffix", "The suffix of the referenced data model's methods."]]}
class HelloModuleService extends ModulesSdkUtils
.abstractModuleServiceFactory<
// ...
>(
// ...
) {
// ...
__joinerConfig(): ModuleJoinerConfig {
return {
// ...
relationships: [
{
serviceName: Modules.PRODUCT,
primaryKey: "id",
foreignKey: "variant_id",
alias: "variant",
args: {
methodSuffix: "Variants",
},
},
],
}
}
}
```
The `args` propertys value is an object accepting a `methodSuffix` property. The `methodSuffix` propertys value is the plural name of the data model.
---
## Querying Module Relationships
The next chapter explains how to query data across module relationships.
@@ -34,7 +34,6 @@ module.exports = defineConfig({
},
},
})
```
The `options` propertys value is an object. You can pass any properties you want.
@@ -47,36 +46,30 @@ The modules main service receives the module options as a second parameter.
For example:
```ts title="src/modules/hello/service.ts" highlights={[["15"], ["21"], ["25"], ["26"], ["27"]]}
// ...
```ts title="src/modules/hello/service.ts" highlights={[["12"], ["14", "options?: ModuleOptions"], ["17"], ["18"], ["19"]]}
import { MedusaService } from "@medusajs/utils"
import MyCustom from "./models/my-custom"
// recommended to define type in another file
type HelloModuleOptions = {
type ModuleOptions = {
capitalize?: boolean
}
class HelloModuleService extends ModulesSdkUtils
.abstractModuleServiceFactory<
// ...
>(
// ...
) {
// ...
protected options_: HelloModuleOptions
constructor(
{
// ...
}: InjectedDependencies,
protected readonly moduleOptions: HelloModuleOptions
) {
//...
this.options_ = moduleOptions || {
capitalize: false,
}
export default class HelloModuleService extends MedusaService({
MyCustom,
}){
protected options_: ModuleOptions
constructor({}, options?: ModuleOptions) {
super(...arguments)
this.options_ = options || {
capitalize: false,
}
}
// ...
}
```
---
@@ -93,13 +86,13 @@ import {
} from "@medusajs/modules-sdk"
// recommended to define type in another file
type HelloModuleOptions = {
type ModuleOptions = {
capitalize?: boolean
}
export default function helloWorldLoader({
options,
}: LoaderOptions<HelloModuleOptions>) {
}: LoaderOptions<ModuleOptions>) {
console.log(
"[HELLO MODULE] Just started the Medusa application!",
@@ -10,21 +10,43 @@ In this chapter, youll learn about the remote query and how to use it to fetc
## What is the Remote Query?
The remote query fetches data across modules and their relationships having their `isQueryable` configuration enabled. Its a function registered in the Medusa container under the `remoteQuery` key.
The remote query fetches data across modules. Its a function registered in the Medusa container under the `remoteQuery` key.
In your resources, such as API routes or workflows, you can resolve the remote query to fetch data across custom modules and Medusas commerce modules.
---
## Example: Query Hello Module
## isQueryable Configuration
Before you use remote query on your module, you must enable the `isQueryable` configuration of the module.
For example:
```js
module.exports = defineConfig({
// ...
modules: {
helloModuleService: {
resolve: "./modules/hello",
definition: {
isQueryable: true,
},
},
},
})
```
---
## Remote Query Example
For example, create the route `src/api/store/query/route.ts` with the following content:
export const exampleHighlights = [
["18", "", "Resolve the remote query from the Medusa container."],
["21", "remoteQueryObjectFromString", "Utility function to build the query."],
["22", "entryPoint", "The alias name of the model youre querying."],
["23", "fields", "An array of the data models field names to retrieve in the result."],
["22", "entryPoint", "The name of the data model you're querying."],
["23", "fields", "An array of the data models properties to retrieve in the result."],
["27", "remoteQuery", "Run the query using the remote query."]
]
@@ -50,12 +72,12 @@ export async function GET(
)
const query = remoteQueryObjectFromString({
entryPoint: "custom_product_data",
fields: ["id", "custom_field", "product.title"],
entryPoint: "my_custom",
fields: ["id", "test"],
})
res.json({
custom_product_data: await remoteQuery(query),
my_customs: await remoteQuery(query),
})
}
```
@@ -64,8 +86,8 @@ In the above example, you resolve `remoteQuery` from the Medusa container.
Then, you create a query using the `remoteQueryObjectFromString` utility function imported from `@medusajs/utils`. This function accepts as a parameter an object with the following required properties:
- `entryPoint`: The alias name of the model youre querying. You defined the alias name in the `__joinerConfig` method of your main service.
- `fields`: An array of the data models field names to retrieve in the result. You can also specify fields of a relationship using dot notation.
- `entryPoint`: The data model's name, as specified in the first parameter of the `model.define` method used for the data model's definition.
- `fields`: An array of the data models properties to retrieve in the result.
You then pass the query to the `remoteQuery` function to retrieve the results.
@@ -75,17 +97,17 @@ You then pass the query to the `remoteQuery` function to retrieve the results.
```ts highlights={[["6"], ["7"], ["8"], ["9"]]}
const query = remoteQueryObjectFromString({
entryPoint: "custom_product_data",
fields: ["id", "custom_field", "product.title"],
entryPoint: "my_custom",
fields: ["id", "name"],
variables: {
filters: {
id: [
"cpd_01HWSVWR4D2XVPQ06DQ8X9K7AX",
"cpd_01HWSVWK3KYHKQEE6QGS2JC3FX",
"mc_01HWSVWR4D2XVPQ06DQ8X9K7AX",
"mc_01HWSVWK3KYHKQEE6QGS2JC3FX",
],
},
},
})
})
const result = await remoteQuery(query)
```
@@ -102,7 +124,7 @@ The `remoteQueryObjectFromString` function accepts a `variables` property. You c
{
name: "filters",
type: "`object`",
description: "The filters to apply on any of the data model's fields."
description: "The filters to apply on any of the data model's properties."
}
]
},
@@ -116,8 +138,8 @@ The `remoteQueryObjectFromString` function accepts a `variables` property. You c
```ts highlights={[["5"], ["6"], ["7"]]}
const query = remoteQueryObjectFromString({
entryPoint: "custom_product_data",
fields: ["id", "custom_field", "product.title"],
entryPoint: "my_custom",
fields: ["id", "name"],
variables: {
order: {
name: "DESC",
@@ -128,12 +150,12 @@ const query = remoteQueryObjectFromString({
const result = await remoteQuery(query)
```
To sort returned records, pass an `order` property to the `variables` property's value.
To sort returned records, pass an `order` property to `variables`.
The `order` property is an object whose keys are field names, and values are either:
The `order` property is an object whose keys are property names, and values are either:
- `ASC` to sort records by that field in ascending order.
- `DESC` to sort records by that field in descending order.
- `ASC` to sort records by that property in ascending order.
- `DESC` to sort records by that property in descending order.
---
@@ -141,8 +163,8 @@ The `order` property is an object whose keys are field names, and values are eit
```ts highlights={[["5", "skip", "The number of records to skip before fetching the results."], ["6", "take", "The number of records to fetch."]]}
const query = remoteQueryObjectFromString({
entryPoint: "custom_product_data",
fields: ["id", "custom_field", "product.title"],
entryPoint: "my_custom",
fields: ["id", "name"],
variables: {
skip: 0,
take: 10,
@@ -155,7 +177,7 @@ const {
} = await remoteQuery(query)
```
To paginate the returned records, pass the following properties to the `variables` property's value:
To paginate the returned records, pass the following properties to `variables`:
- `skip`: (required to apply pagination) The number of records to skip before fetching the results.
- `take`: The number of records to fetch.
@@ -215,7 +237,6 @@ The remote query function alternatively accepts a string with GraphQL syntax as
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import { remoteQueryObjectFromString } from "@medusajs/utils"
import { ContainerRegistrationKeys } from "@medusajs/utils"
import type {
RemoteQueryFunction,
@@ -231,20 +252,15 @@ The remote query function alternatively accepts a string with GraphQL syntax as
const query = `
query {
custom_product_data {
my_custom {
id
custom_field
product {
title
}
name
}
}
`
const result = await remoteQuery(query)
res.json({
custom_product_data: result,
my_customs: result,
})
}
```
@@ -256,15 +272,12 @@ The remote query function alternatively accepts a string with GraphQL syntax as
The `remoteQuery` function accepts as a second parameter an object of variables to reference in the GraphQL query.
```ts highlights={[["2"], ["3"], ["16"], ["17"], ["18"], ["19"]]}
```ts highlights={[["2"], ["3"], ["13"], ["14"], ["15"], ["16"]]}
const query = `
query($id: ID) {
custom_product_data(id: $id) {
my_custom(id: $id) {
id
custom_field
product {
title
}
name
}
}
`
@@ -273,8 +286,8 @@ The remote query function alternatively accepts a string with GraphQL syntax as
query,
{
id: [
"cpd_01HWSVWR4D2XVPQ06DQ8X9K7AX",
"cpd_01HWSVWK3KYHKQEE6QGS2JC3FX",
"mc_01HWSVWR4D2XVPQ06DQ8X9K7AX",
"mc_01HWSVWK3KYHKQEE6QGS2JC3FX",
]
}
)
@@ -285,22 +298,19 @@ The remote query function alternatively accepts a string with GraphQL syntax as
### Sort Records with GraphQL
To sort the records by a field, pass in the query an `order` argument whose value is an object. The objects key is the fields name, and the value is either:
To sort the records by a property, pass in the query an `order` argument whose value is an object. The objects key is the propertys name, and the value is either:
- `ASC` to sort items by that field in ascending order.
- `DESC` to sort items by that field in descending order.
- `ASC` to sort items by that property in ascending order.
- `DESC` to sort items by that property in descending order.
For example:
```ts highlights={[["3"]]}
const query = `
query {
custom_product_data(order: {custom_field: DESC}) {
my_custom(order: {name: DESC}) {
id
custom_field
product {
title
}
name
}
}
`
@@ -320,12 +330,9 @@ The remote query function alternatively accepts a string with GraphQL syntax as
```ts highlights={[["2"], ["3"]]}
const query = `
query($skip: Int, $take: Int) {
custom_product_data(skip: $skip, take: $take) {
my_custom(skip: $skip, take: $take) {
id
custom_field
product {
title
}
name
}
}
`
@@ -6,11 +6,13 @@ export const metadata = {
# {metadata.title}
In this document, youll learn about what the service factory is and how to use it to create a service.
In this chapter, youll learn about what the service factory is and how to use it.
## What is the Service Factory?
Medusa provides a service factory that your modules main service can extend. The service factory implements data management methods for your data models.
Medusa provides a service factory that your modules main service can extend.
The service factory generates data management methods for your data models, so you don't have to implement them manually.
<Note title="Use the service factory when" type="success">
@@ -22,34 +24,41 @@ Medusa provides a service factory that your modules main service can extend.
## How to Extend the Service Factory?
Medusa provides the service factory as a function your service extends. The function creates and returns a service class with generated data-management methods.
Medusa provides the service factory as a `MedusaService` function your service extends. The function creates and returns a service class with generated data-management methods.
For example, create the file `src/modules/hello/service.ts` with the following content:
```ts title="src/modules/hello/service.ts"
import { ModulesSdkUtils } from "@medusajs/utils"
import { MyCustom } from "./models/my-custom"
export const highlights = [
["4", "MedusaService", "The service factory function."],
["5", "MyCustom", "The data models to generate data-management methods for."]
]
class HelloModuleService extends ModulesSdkUtils
.abstractModuleServiceFactory(
MyCustom, []
) {
// TODO implement custom methods
}
```ts title="src/modules/hello/service.ts" highlights={highlights}
import { MedusaService } from "@medusajs/utils"
import MyCustom from "./models/my-custom"
class HelloModuleService extends MedusaService({
MyCustom,
}){
// TODO implement custom methods
}
export default HelloModuleService
```
### abstractModuleServiceFactory Parameters
### MedusaService Parameters
The `abstractModuleServiceFactory` function accepts two parameters:
The `MedusaService` function accepts one parameter, which is an object of data models to generate data-management methods for.
1. The first parameter is the main data model this service is creating methods for. For example, `MyCustom`.
2. The second parameter is an array of data models to generate methods for. If you have an `AnotherCustom` data model, this is where you add it.
In the example above, the `HelloModuleService` now has methods to manage the `MyCustom` data model, such as `createMyCustoms`.
### Generated Methods
The service factory generates the following methods for the main data model:
The service factory generates data-management methods for each of the data models provided in the first parameter.
The method's names are the operation's name, suffixed by the data model's name.
For example, the following methods are generated for the code snippet above:
<Table>
<Table.Header>
@@ -62,7 +71,7 @@ The service factory generates the following methods for the main data model:
<Table.Row>
<Table.Cell>
`list`
`listMyCustoms`
</Table.Cell>
<Table.Cell>
@@ -74,7 +83,7 @@ The service factory generates the following methods for the main data model:
<Table.Row>
<Table.Cell>
`listAndCount`
`listAndCountMyCustoms`
</Table.Cell>
<Table.Cell>
@@ -86,7 +95,7 @@ The service factory generates the following methods for the main data model:
<Table.Row>
<Table.Cell>
`retrieve`
`retrieveMyCustom`
</Table.Cell>
<Table.Cell>
@@ -98,7 +107,31 @@ The service factory generates the following methods for the main data model:
<Table.Row>
<Table.Cell>
`delete`
`createMyCustoms`
</Table.Cell>
<Table.Cell>
Create and retrieve records of the data model.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`updateMyCustoms`
</Table.Cell>
<Table.Cell>
Update and retrieve records of the data model.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`deleteMyCustoms`
</Table.Cell>
<Table.Cell>
@@ -110,77 +143,32 @@ The service factory generates the following methods for the main data model:
<Table.Row>
<Table.Cell>
`softDelete`
`softDeleteMyCustoms`
</Table.Cell>
<Table.Cell>
Soft-deletes a record by an ID or filter. This only applies if the data model has a `deleted_at` field.
Soft-deletes records using an array of IDs or an object of filters.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`restore`
`restoreMyCustoms`
</Table.Cell>
<Table.Cell>
Restores a soft-deleted record by an ID or filter. This only applies if the data model has a `deleted_at` field.
Restores soft-deleted records using an array of IDs or an object of filters.
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
The same methods are generated for data models passed in the second parameter of the service factory. The methods' names end with the data model's name. For example, `listAnotherCustom`.
<Note>
### Type Arguments
Except for the `retrieve` method, the suffixed data model's name is plural.
For a better development experience and accurate typing of the generated methods, the `abstractModuleServiceFactory` function accepts three type arguments:
export const typeArgsHighlights = [
["25", "InjectedDependencies", "The type of dependencies resolved from the Module's container."],
["26", "MyCustomDTO", "The expected input/output type of the main data model's generated methods."],
["27", "AllModelsDTO", "The expected input/output type of the generated methods of every data model."],
]
```ts title="src/modules/hello/service.ts" highlights={typeArgsHighlights} collapsibleLines="1-22" expandButtonLabel="Show More"
import { ModulesSdkUtils } from "@medusajs/utils"
import { MyCustom } from "./models/my-custom"
// recommended to define type in another file
type MyCustomDTO = {
id: string
name: string
}
type InjectedDependencies = {
// TODO add dependencies
}
type AllModelsDTO = {
MyCustom: {
dto: MyCustomDTO
}
}
// add other data models in your module here.
const generateMethodsFor = []
class HelloModuleService extends ModulesSdkUtils
.abstractModuleServiceFactory<
InjectedDependencies,
MyCustomDTO,
AllModelsDTO
>(MyCustom, generateMethodsFor) {
// TODO implement custom methods
}
export default HelloModuleService
```
1. The first one is the type of the dependencies to resolve from the module's container.
2. The second one is the expected input and output type of the main data models methods.
3. The third type is the expected input and output type of all data models that the service factory generates methods for.
</Note>
@@ -8,7 +8,9 @@ In this chapter, you'll learn about Medusa's commerce modules.
## What is a Commerce Module?
Medusa provides all its commerce features as separate modules, such as the Product Module, Cart Module, or Order Module. These modules and your custom modules are interchangeable in the Medusa application, making Medusas architecture more flexible.
Medusa provides all its commerce features as separate modules, such as the Product or Order modules.
These modules and your custom modules are interchangeable in the Medusa application, making Medusas architecture more flexible.
Refer to [this reference](!resources!/commerce-modules) for a full list of commerce modules in Medusa.
@@ -47,6 +49,6 @@ When you resolve the `ModuleRegistrationName.PRODUCT` (or `productModuleService`
<Note title="Tip">
To resolve the main service of any commerce module, use the `ModuleRegistrationName` enum imported from `@medusajs/modules-sdk` to refer to its registration name in the Medusa container.
To resolve the main service of any commerce module, use the registration name defined in the `ModuleRegistrationName` enum imported from `@medusajs/modules-sdk`.
</Note>
+35 -38
View File
@@ -8,9 +8,7 @@ In this chapter, youll learn what data models are and how to create a data mo
## What is a Data Model?
A data model is a class that represents a table in the database. A data model is created in a module. You can then create a service that manages that data model.
Data models are based on [MikroORM](https://mikro-orm.io/docs/quick-start). So, you can use its decorators, types, and utilities when creating a model.
A data model is a class that represents a table in the database. It's created in a module.
---
@@ -20,67 +18,66 @@ Data models are based on [MikroORM](https://mikro-orm.io/docs/quick-start). So,
1. Create data model class in a module.
2. Generate migration for the data model.
3. Add migration scripts to the module's definition.
4. Run migration to add table for data model in the database.
</Note>
A data model is a class created in a TypeScript or JavaScript file under a module's `models` directory.
A data model is created in a TypeScript or JavaScript file under a module's `models` directory. It's defined using the `model` utility imported from `@medjusajs/utils`.
For example, create the file `src/modules/hello/models/my-custom.ts` with the following content:
```ts title="src/modules/hello/models/my-custom.ts"
import { BaseEntity } from "@medusajs/utils"
import {
Entity,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { model } from "@medusajs/utils"
@Entity()
export class MyCustom extends BaseEntity {
@PrimaryKey({ columnType: "text" })
id!: string
const MyCustom = model.define("my_custom", {
id: model.id(),
name: model.text(),
})
@Property({ columnType: "text" })
name: string
}
export default MyCustom
```
This defines a new data model `MyCustom` with the fields `id` and `name`. Data models extend the `BaseEntity` class imported from `@medusajs/utils`.
You define a data model using the `model`'s `define` method. It accepts two parameters:
1. The first one is the name of the data model's table in the database.
2. The second is an object, which is the data model's schema. The schema's properties are defined using the `model`'s methods.
The example above defines the data model `MyCustom` with the properties `id` and `name`.
### Create a Migration
After creating the data model, you must create a migration that creates a table in your database for this data model.
A migration defines changes to be made in the database, such as create or update tables.
A migration is a class created in a TypeScript or JavaScript file under a module's `migrations` directory. It implements an `up` and `down` method, where the `up` method reflects changes on the database, and the `down` method reverts the changes from the database.
So, you must create a migration that creates a table for your data model in the database.
<Details summaryContent="Generate with MikroORM">
MikroORM provides a CLI tool that helps you generate migrations. To use it:
A migration is a class created in a TypeScript or JavaScript file under a module's `migrations` directory. It has two methods:
1. Create the file `src/modules/hello/mikro-orm.config.dev.ts` with the following content:
- The `up` method reflects changes on the database.
- The `down` method reverts the changes made in the `up` method.
```ts highlights={[["8", "hello", "The module's name."]]}
<Details summaryContent="Generate Migration">
To generate migrations:
1. Create the file `src/modules/hello/migrations-config.ts` with the following content:
```ts highlights={[["7", '"medusa-hello"', "Use any database name relevant for your module."]]}
import { defineMikroOrmCliConfig } from "@medusajs/utils"
import path from "path"
import { TSMigrationGenerator } from "@medusajs/utils"
import { MyCustom } from "./models/my-custom"
import MyCustom from "./models/my-custom"
module.exports = {
entities: [MyCustom],
schema: "public",
clientUrl: "postgres://postgres@localhost/medusa-hello",
type: "postgresql",
export default defineMikroOrmCliConfig({
entities: [MyCustom] as any[],
databaseName: "medusa-hello",
migrations: {
path: path.join(__dirname, "migrations"),
generator: TSMigrationGenerator,
},
}
})
```
2. Run the following command in the root directory of your Medusa application:
```bash
npx cross-env MIKRO_ORM_CLI=./src/modules/hello/mikro-orm.config.dev.ts mikro-orm migration:create
npx cross-env MIKRO_ORM_CLI=./src/modules/hello/migrations-config.ts mikro-orm migration:create
```
<Note title="Tip">
@@ -89,7 +86,7 @@ A migration is a class created in a TypeScript or JavaScript file under a module
</Note>
After running the command, a new file is created under the `src/modules/hello/migrations` directory. This file holds `up` and `down` methods that define the actions to execute when running and reverting the migration respectively.
After running the command, a migration file is generated under the `src/modules/hello/migrations` directory.
</Details>
@@ -98,10 +95,10 @@ For example:
```ts title="src/modules/migrations/Migration20240429090012.ts"
import { Migration } from "@mikro-orm/migrations"
export class Migration20240429090012 extends Migration {
export class Migration20240624145652 extends Migration {
async up(): Promise<void> {
this.addSql("create table if not exists \"my_custom\" (\"id\" varchar(255) not null, \"name\" text not null, constraint \"my_custom_pkey\" primary key (\"id\"));")
this.addSql("create table if not exists \"my_custom\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"my_custom_pkey\" primary key (\"id\"));")
}
async down(): Promise<void> {
@@ -81,9 +81,9 @@ For example:
{/* TODO change how event names are loaded */}
export const highlights = [
["11"],
["14", "resolve", "Resolve the `IProductModuleService`."],
["14", "ModuleRegistrationName.PRODUCT", "The resource registration name imported from `@medusajs/modules-sdk`."]
["10", "container", "Recieve the Medusa Container in the object parameter."],
["13", "resolve", "Resolve the Product Module's main service."],
["13", "ModuleRegistrationName.PRODUCT", "The resource registration name imported from `@medusajs/modules-sdk`."]
]
```ts title="src/subscribers/product-created.ts" highlights={highlights}
@@ -111,4 +111,4 @@ export const config: SubscriberConfig = {
}
```
You use the container to resolve the `IProductModuleService`, then log the title of the created product.
You use the container to resolve the Product Module's main service, then log the title of the created product.
@@ -15,7 +15,7 @@ You use the Medusa container to resolve resources, such as services.
For example, in a custom API route you can resolve any service registered in the Medusa application using the `scope.resolve` method of the `MedusaRequest` parameter:
export const highlights = [
["13", "resolve", "Resolve the `IProductModuleService`"],
["13", "resolve", "Resolve the Product Module's main service."],
["13", "ModuleRegistrationName.PRODUCT", "The resource registration name imported from `@medusajs/modules-sdk`."]
]
@@ -41,5 +41,3 @@ export const GET = async (
})
}
```
You resolve the `IProductModuleService` and uses it to return the full count of products in the Medusa application.
@@ -115,7 +115,7 @@ The scheduled job function receives an object parameter that has a `container` p
For example:
export const highlights = [
["12", "resolve", "Resolve the `IProductModuleService`."],
["12", "resolve", "Resolve the Product Module's main service."],
["12", "ModuleRegistrationName.PRODUCT", "The resource registration name imported from `@medusajs/modules-sdk`."]
]
@@ -147,4 +147,4 @@ export const config: ScheduledJobConfig = {
}
```
In the scheduled job function, you resolve the `IProductModuleService` and retrieve the number of products in the store, then log the number in the terminal.
In the scheduled job function, you resolve the Product Module's main service and retrieve the number of products in the store, then log the number in the terminal.
+6 -4
View File
@@ -228,11 +228,11 @@ Each step in the workflow receives as a second parameter a `context` object. The
For example:
export const highlights = [
["14", "resolve", "Resolve the `IProductModuleService`."],
["14", "ModuleRegistrationName.PRODUCT", "The resource registration name imported from `@medusajs/modules-sdk`."]
["15", "resolve", "Resolve the Product Module's main service."],
["15", "ModuleRegistrationName.PRODUCT", "The resource registration name imported from `@medusajs/modules-sdk`."]
]
```ts title="src/workflows/product-count.ts" highlights={highlights}
```ts title="src/workflows/product-count.ts" highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports"
import {
createStep,
StepResponse,
@@ -265,4 +265,6 @@ const myWorkflow = createWorkflow<unknown, WorkflowOutput>(
)
export default myWorkflow
```
```
In the step, you resolve the Product Module's main service and use it to retrieve the product count.
+26 -10
View File
@@ -106,18 +106,10 @@ export const sidebar = sidebarAttachHrefCommonOptions(
path: "/advanced-development/modules/service-factory",
title: "Service Factory",
},
{
path: "/advanced-development/modules/database-operations-in-services",
title: "Database Operations",
},
{
path: "/advanced-development/modules/options",
title: "Module Options",
},
{
path: "/advanced-development/modules/module-relationships",
title: "Module Relationships",
},
{
path: "/advanced-development/modules/remote-query",
title: "Remote Query",
@@ -136,13 +128,37 @@ export const sidebar = sidebarAttachHrefCommonOptions(
title: "Data Models",
children: [
{
path: "/advanced-development/data-models/common-definitions",
title: "Common Definitions",
path: "/advanced-development/data-models/property-types",
title: "Property Types",
},
{
path: "/advanced-development/data-models/configure-properties",
title: "Configure Properties",
},
{
path: "/advanced-development/data-models/primary-key",
title: "Primary Key",
},
{
path: "/advanced-development/data-models/relationships",
title: "Relationships",
},
{
path: "/advanced-development/data-models/relationship-cascades",
title: "Relationship Cascades",
},
{
path: "/advanced-development/data-models/indexes",
title: "Data Model Index",
},
{
path: "/advanced-development/data-models/soft-deletable",
title: "Soft-Deletable Models",
},
{
path: "/advanced-development/data-models/searchable-property",
title: "Searchable Property",
},
],
},
{
@@ -1,196 +0,0 @@
import { Table } from "docs-ui"
export const metadata = {
title: `Segment Plugin`,
}
# {metadata.title}
## Features
[Segment](https://github.com/medusajs/medusa/tree/master/packages/medusa-plugin-segment) is a powerful Customer Data Platform that allows users to collect, transform, send and archive their customer data.
Through Segment, you can integrate other third-party services such as:
- Google Analytics
- Mailchimp
- Zendesk
- Data warehousing for advanced data analytics and segmentation through services like Metabase
![Diagram illustrating how data goes from Medusa through Segment to other destinations](https://res.cloudinary.com/dza7lstvk/image/upload/v1709135314/Medusa%20Resources/segment-diagram_mye5sc.jpg)
The Segment plugin in Medusa allows you to track ecommerce events and record them in Segment. Then, you can push these events to other destinations using Segment.
---
## Events That the Segment Plugin Tracks
The Segment plugin tracks the following events:
1. `order.placed`: Triggered when an order is placed.
2. `order.shipment_created`: Triggered when a shipment is created for an order.
3. `claim.created`: Triggered when a new claim is created.
4. `order.items_returned`: Triggered when an item in an order is returned.
5. `order.canceled`: Triggered when an order is canceled.
6. `swap.created`: Triggered when a swap is created.
7. `swap.shipment_created`: Triggered when a shipment is created for a swap.
8. `swap.payment_completed`: Triggered when payment for a swap is completed.
<Note title="Tip">
Check out the [Event Reference](../../../events-reference/page.mdx) to learn more about these events and their data payloads.
</Note>
---
## Preparations
<Note type="check">
- [Segment Account](https://app.segment.com/signup/)
</Note>
### Create a Segment Source
On your Segment dashboard:
1. Choose Catalog from the sidebar under Connections.
2. Search for "Node.js" or find "Node.js" under the Sources directory.
3. In the Node.js details page, click on Add Source.
4. This opens a new page to create a Node.js source. Enter the name of the source then click Add Source.
5. On the new source's dashboard, find a "Write Key". Youll use this key in the next section after you install the Segment plugin in your Medusa application.
### Optional: Add Destination
After you create the Segment source, you can add destinations. This is where the data is sent when you send them to Segment. You can add more than one destination.
To add a destination:
1. Choose Destinations from the sidebar under Connections.
2. Click on the "Add destination" button.
3. Choose the desired destination, such as Google Universal Analytics or Facebook Pixel.
The process of integrating each destination is different, so you must follow the steps detailed in Segment for each destination you choose.
---
## Install the Segment Plugin
To install the Segment plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-plugin-segment
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
export const highlights = [
["6", "write_key", "The Segment source's write key."]
]
```js title="medusa-config.js" highlights={highlights}
const plugins = [
// ...
{
resolve: `medusa-plugin-segment`,
options: {
write_key: process.env.SEGMENT_WRITE_KEY,
},
},
]
```
### Segment Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`write_key`
</Table.Cell>
<Table.Cell>
The Segment source's write key.
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### Environment Variables
Make sure to add the following environment variables:
```bash
SEGMENT_WRITE_KEY=<YOUR_SEGMENT_WRITE_KEY>
```
---
## Test the Plugin
To test the plugin, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, try triggering one of the [mentioned events earlier in this document](#events-that-the-segment-plugin-tracks). For example, you can place an order either using the [REST APIs](https://docs.medusajs.com/api/store) or using the [Next.js Starter](../../../nextjs-starter/page.mdx).
After you place an order, on the Segment source that you created, click on the Debugger tab. You should see at least one event triggered for each order you place. If you click on the event, you can see the order details are passed to the event.
If you added a destination, you can also check your destination to make sure the data is reflected there.
---
## Add Custom Tracking
The `SegmentService` allows you to track other Medusa events or custom events.
For example, create the file `src/subscribers/customer.ts` with the following content:
```ts title="src/subscribers/customer.ts"
import {
type SubscriberConfig,
type SubscriberArgs,
CustomerService,
} from "@medusajs/medusa"
export default async function handleCustomerCreated({
data,
container,
}: SubscriberArgs<Record<string, string>>) {
const segmentService = container.resolve("segmentService")
const customerData = data
delete customerData["password_hash"]
segmentService.track({
event: "Customer Created",
userId: data.id,
properties: customerData,
})
}
export const config: SubscriberConfig = {
event: CustomerService.Events.CREATED,
}
```
This creates a subscriber that listens to the `CustomerService.Events.CREATED` (`customer.created`) event and sends tracking information to Segment for every customer created.
The `SegmentService` has a `track` method used to send tracking data to Segment. It accepts an object of data, where the keys `event` and `userId` are required. Instead of `userId`, you can use `anonymousId` to pass an anonymous user ID.
To pass additional data to Segment, pass them under the `properties` object key.
The `SegmentService` also has the method `identify` to tie a user to their actions or specific traits.
@@ -1,686 +0,0 @@
import { Table } from "docs-ui"
export const metadata = {
title: `Contentful Plugin`,
}
# {metadata.title}
## Features
[Contentful](https://www.contentful.com/) is a headless CMS service that allows developers to integrate rich CMS functionalities into any platform.
By integrating Contentful to Medusa, you can benefit from powerful features in your ecommerce store such as:
- Rich CMS details for product.
- Easy-to-use interface to manage content for static content and pages.
- Localization for product and storefront content.
- Two-way sync between Contentful and Medusa.
---
## Install the Contentful Plugin
<Note type="check">
- [Contentful account with a space](https://www.contentful.com/sign-up/).
- An Event Module installed in the Medusa application, such as the [Redis Event Module](../../../architectural-modules/event/redis/page.mdx).
</Note>
To install the Contentful plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-plugin-contentful
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
export const highlights = [
["6", "space_id", "The Contentful space's ID."],
["7", "access_token", "The personal access token for content management."],
["8", "environment", "The Contentful environment."],
]
```js title="medusa-config.js" highlights={highlights}
const plugins = [
// ...
{
resolve: `medusa-plugin-contentful`,
options: {
space_id: process.env.CONTENTFUL_SPACE_ID,
access_token: process.env.CONTENTFUL_ACCESS_TOKEN,
environment: process.env.CONTENTFUL_ENV,
},
},
]
```
### Contentful Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell>Required</Table.HeaderCell>
<Table.HeaderCell>Default</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`space_id`
</Table.Cell>
<Table.Cell>
A string indicating the ID of your [Contentful space](https://www.contentful.com/help/find-space-id/).
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`access_token`
</Table.Cell>
<Table.Cell>
A string indicating [the personal access token for content management](https://www.contentful.com/help/personal-access-tokens/#how-to-get-a-personal-access-token-the-web-app).
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`environment`
</Table.Cell>
<Table.Cell>
A string indicating the [Contentful environment](https://www.contentful.com/developers/docs/concepts/multiple-environments/). Typically, its value should be `master`.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`ignore_threshold`
</Table.Cell>
<Table.Cell>
The number of seconds to wait before re-syncing a specific record.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`2`
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`custom_<TYPE>_fields`
</Table.Cell>
<Table.Cell>
An object that allows you to map fields in Medusa to [custom field names](#custom-field-mapping).
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### Environment Variables
Make sure to add the following environment variables.
```bash
CONTENTFUL_SPACE_ID=<YOUR_SPACE_ID>
CONTENTFUL_ACCESS_TOKEN=<YOUR_ACCESS_TOKEN>
CONTENTFUL_ENV=master
```
### Custom Field Mapping
When the plugin syncs data between Contentful and Medusa, it expects a set of fields to be defined in the respective content models in Contentful. If you use different names to define those fields in Contentful, specify them in the `custom_<TYPE>_fields` option mentioned earlier, where `<TYPE>` is the name of the content model.
For example, to change the name of the products `title` field, pass the following option to the plugin:
```js title="medusa-config.js" highlights={[["9"], ["10"], ["11"]]}
const plugins = [
// ...
{
resolve: `medusa-plugin-contentful`,
options: {
space_id: process.env.CONTENTFUL_SPACE_ID,
access_token: process.env.CONTENTFUL_ACCESS_TOKEN,
environment: process.env.CONTENTFUL_ENV,
custom_product_fields: {
title: "name",
},
},
},
]
```
The rest of this section includes the field names you can customize using this option for each content model type.
<Details summaryContent="product">
- `title`
- `subtitle`
- `description`
- `variants`
- `options`
- `medusaId`
- `type`
- `collection`
- `tags`
- `handle`
</Details>
<Details summaryContent="variant">
- `title`
- `sku`
- `prices`
- `options`
- `medusaId`
</Details>
<Details summaryContent="region">
- `name`
- `countries`
- `paymentProviders`
- `fulfillmentProviders`
- `medusaId`
</Details>
<Details summaryContent="collection">
- `title`
- `medusaId`
</Details>
<Details summaryContent="type">
- `name`
- `medusaId`
</Details>
### Migrate Content Models
In your Contentful space, you must have content models for Medusa entities such as products and regions.
You can either create the content models manually, or create a loader in the Medusa application that migrates these content models into Contentful.
This section includes migration scripts for Medusas data models that are relevant for Contentful.
Before creating the migration scripts, run the following command in the root of your Medusa backend to install Contentfuls migration SDK:
```bash npm2yarn
npm install --save-dev contentful-migration
```
<Details summaryContent="product Content Model">
Create the file `src/loaders/contentful-migrations/product.ts` with the following content:
```ts title="src/loaders/contentful-migrations/product.ts"
import Migration, {
MigrationContext,
} from "contentful-migration"
export function productMigration(
migration: Migration,
context?: MigrationContext
) {
const product = migration
.createContentType("product")
.name("Product")
.displayField("title")
product
.createField("title")
.name("Title")
.type("Symbol")
.required(true)
product
.createField("subtitle")
.name("Subtitle")
.type("Symbol")
product
.createField("handle")
.name("Handle")
.type("Symbol")
product
.createField("thumbnail")
.name("Thumbnail")
.type("Link")
.linkType("Asset")
product
.createField("description")
.name("Description")
.type("Text")
product
.createField("options")
.name("Options")
.type("Object")
product
.createField("tags")
.name("Tags")
.type("Object")
product
.createField("collection")
.name("Collection")
.type("Symbol")
product
.createField("type")
.name("Type")
.type("Symbol")
product
.createField("variants")
.name("Variants")
.type("Array")
.items({
type: "Link",
linkType: "Entry",
validations: [
{
linkContentType: ["productVariant"],
},
],
})
product
.createField("medusaId")
.name("Medusa ID")
.type("Symbol")
}
```
</Details>
<Details summaryContent="productVariant Content Model">
Create the file `src/loaders/contentful-migrations/product-variant.ts` with the following content:
```ts title="src/loaders/contentful-migrations/product-variant.ts"
import Migration, {
MigrationContext,
} from "contentful-migration"
export function productVariantMigration(
migration: Migration,
context?: MigrationContext
) {
const productVariant = migration
.createContentType("productVariant")
.name("Product Variant")
.displayField("title")
productVariant
.createField("title")
.name("Title")
.type("Symbol")
.required(true)
productVariant
.createField("sku")
.name("SKU")
.type("Symbol")
productVariant
.createField("options")
.name("Options")
.type("Object")
productVariant
.createField("prices")
.name("Prices")
.type("Object")
productVariant
.createField("medusaId")
.name("Medusa ID")
.type("Symbol")
}
```
</Details>
<Details summaryContent="collection Content Model">
Create the file `src/loaders/contentful-migrations/product-collection.ts` with the following content:
```ts title="src/loaders/contentful-migrations/product-collection.ts"
import Migration, {
MigrationContext,
} from "contentful-migration"
export function productCollectionMigration(
migration: Migration,
context?: MigrationContext
) {
const collection = migration
.createContentType("collection")
.name("Product Collection")
.displayField("title")
collection
.createField("title")
.name("Title")
.type("Symbol")
.required(true)
collection
.createField("medusaId")
.name("Medusa ID")
.type("Symbol")
}
```
</Details>
<Details summaryContent="productType Content Model">
Create the file `src/loaders/contentful-migrations/product-type.ts` with the following content:
```ts title="src/loaders/contentful-migrations/product-type.ts"
import Migration, {
MigrationContext,
} from "contentful-migration"
export function productTypeMigration(
migration: Migration,
context?: MigrationContext
) {
const collection = migration
.createContentType("productType")
.name("Product Type")
.displayField("title")
collection
.createField("title")
.name("Title")
.type("Symbol")
.required(true)
collection
.createField("medusaId")
.name("Medusa ID")
.type("Symbol")
}
```
</Details>
<Details summaryContent="region Content Model">
Create the file `src/loaders/contentful-migrations/region.ts` with the following content:
```ts title="src/loaders/contentful-migrations/region.ts"
import Migration, {
MigrationContext,
} from "contentful-migration"
export function regionMigration(
migration: Migration,
context?: MigrationContext
) {
const region = migration
.createContentType("region")
.name("Region")
.displayField("name")
region
.createField("name")
.name("Name")
.type("Symbol")
.required(true)
region
.createField("countries")
.name("Options")
.type("Object")
region
.createField("paymentProviders")
.name("Payment Providers")
.type("Object")
region
.createField("fulfillmentProviders")
.name("Fulfillment Providers")
.type("Object")
region
.createField("currencyCode")
.name("Currency Code")
.type("Symbol")
region
.createField("medusaId")
.name("Medusa ID")
.type("Symbol")
}
```
</Details>
Finally, create a loader at `src/loaders/index.ts` with the following content:
```ts title="src/loaders/index.ts"
import {
ConfigModule,
StoreService,
MedusaContainer,
} from "@medusajs/medusa"
import { runMigration } from "contentful-migration"
import {
productMigration,
} from "./contentful-migrations/product"
import {
productVariantMigration,
} from "./contentful-migrations/product-variant"
import {
productCollectionMigration,
} from "./contentful-migrations/product-collection"
import {
productTypeMigration,
} from "./contentful-migrations/product-type"
import {
regionMigration,
} from "./contentful-migrations/region"
type ContentfulPluginType = {
resolve: string
options: {
space_id: string
access_token: string
environment: string
}
}
export default async (
container: MedusaContainer,
config: ConfigModule
): Promise<void> => {
// ensure that migration only runs once
const storeService = container.resolve<StoreService>(
"storeService"
)
const store = await storeService.retrieve()
if (store.metadata?.ran_contentful_migrations) {
return
}
console.info("Running contentful migrations...")
// load Contentful options
const contentfulPlugin = config.plugins
.find((plugin) =>
typeof plugin === "object" &&
plugin.resolve === "medusa-plugin-contentful"
) as ContentfulPluginType
if (!contentfulPlugin) {
console.log(
"Didn't find Contentful plugin. Aborting migration..."
)
return
}
const options = {
spaceId: contentfulPlugin.options.space_id,
accessToken: contentfulPlugin.options.access_token,
environment: contentfulPlugin.options.environment,
yes: true,
}
const migrationFunctions = [
{
name: "Product",
function: productMigration,
},
{
name: "Product Variant",
function: productVariantMigration,
},
{
name: "Product Collection",
function: productCollectionMigration,
},
{
name: "Product Type",
function: productTypeMigration,
},
{
name: "Region",
function: regionMigration,
},
]
await Promise.all(
migrationFunctions.map(async (migrationFunction) => {
console.info(`Migrating ${
migrationFunction.name
} component...`)
try {
await runMigration({
...options,
migrationFunction: migrationFunction.function,
})
console.info(`Finished migrating ${
migrationFunction.name
} component`)
} catch (e) {
if (
typeof e === "object" && "errors" in e &&
Array.isArray(e.errors) &&
e.errors.length > 0 &&
e.errors[0].type === "Invalid Action" &&
e.errors[0].message.includes("already exists")
) {
console.info(`${
migrationFunction.name
} already exists. Skipping its migration.`)
} else {
throw new Error(e)
}
}
})
)
await storeService.update({
metadata: {
ran_contentful_migrations: true,
},
})
console.info("Finished contentful migrations")
}
```
Notice that in the script you store a flag in the default stores `metadata` attribute to ensure these migrations only run once.
### Setup Webhooks
As mentioned in the introduction, this plugin supports two-way sync. A subscriber in the plugin listens to changes in the data, such as adding a new product, and syncs the data with Contentful.
To update the Medusa application when changes occur in Contentful, you must configure webhooks settings in Contentful.
<Note>
For webhooks to work, your Medusa application must be deployed and accessible publicly.
</Note>
To do that:
1. On your Contentful Space Dashboard, click on Settings from the navigation bar, then choose Webhooks.
2. Click on the Add Webhook button.
3. In the form, enter a name for the webhook.
4. In the URL field, choose the method `POST` and in the input next to it enter the URL `<MEDUSA_URL>/hooks/contentful` where `<MEDUSA_URL>` is the URL of your deployed Medusa application.
5. Scroll down to find the Content Type select field. Choose `application/json` as its value.
6. You can leave the rest of the fields the same and click on the Save button.
---
## Test the Plugin
Run the following command to start your Medusa application and test the plugin:
```bash npm2yarn
npm run dev
```
If you created migration scripts, theyll run when the Medusa application starts and migrate your content models to Contentful. You can go to your spaces dashboard to confirm theyve been created.
After that, try the sync functionality by creating or updating products in the Medusa application. If youve also setup webhooks, you can test out the sync from Contentful to Medusa.
@@ -1,229 +0,0 @@
export const metadata = {
title: `Strapi Plugin`,
}
# {metadata.title}
<Note>
This plugin is a [community plugin](https://github.com/SGFGOV/medusa-strapi-repo) and is not managed by the official Medusa team. It supports v4 of Strapi. If you run into any issues, please refer to the [plugin's repository](https://github.com/SGFGOV/medusa-strapi-repo).
</Note>
## Features
[Strapi](https://strapi.io/) is an open source headless CMS service that allows developers to have complete control over their content models. It can be integrated into many other frameworks, including Medusa.
By integrating Strapi into Medusa, you can benefit from powerful features in your ecommerce store, such as:
- Rich CMS details for product.
- Easy-to-use interface to manage content for static content and pages.
- Localization for product and storefront content.
- Two-way sync between Strapi and Medusa.
---
## Preparations
<Note type="check">
- A [PostgreSQL database](https://www.postgresql.org/docs/current/sql-createdatabase.html) for Strapi.
</Note>
In this section, youll setup a Strapi project with a Medusa plugin installed. To do that:
1. Clone the Strapi project repository:
```bash
git clone https://github.com/SGFGOV/medusa-strapi-repo.git
```
2. Change to the `medusa-strapi-repo/packages/medusa-strapi` directory.
3. Copy the `.env.test` file to a new `.env` file.
### Change Strapi Environment Variables
In the `.env` file, change the following environment variables:
```bash
# IMPORTANT: Change supersecret with random and unique strings
APP_KEYS=supersecret
API_TOKEN_SALT=supersecret
ADMIN_JWT_SECRET=supersecret
JWT_SECRET=supersecret
MEDUSA_STRAPI_SECRET=supersecret
MEDUSA_BACKEND_URL=http://localhost:9000
MEDUSA_BACKEND_ADMIN=http://localhost:7001
SUPERUSER_EMAIL=support@medusa-commerce.com
SUPERUSER_USERNAME=SuperUser
SUPERUSER_PASSWORD=MedusaStrapi1
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_NAME=postgres_strapi
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=
DATABASE_SSL=false
DATABASE_SCHEMA=public
```
1. Change `APP_KEYS`, `API_TOKEN_SALT`, `JWT_SECRET`, and `ADMIN_JWT_SECRET` to a random and unique string. These keys are used by Strapi to sign session cookies, generate API tokens, and more.
2. Change `MEDUSA_STRAPI_SECRET` to a random unique string. The value of this environment variable is used later in your Medusa configurations.
3. Change `MEDUSA_BACKEND_URL` to the URL of your Medusa backend. If youre running it locally, it should be `http://localhost:9000`.
4. Change `MEDUSA_BACKEND_ADMIN` to the URL of your Medusa Admin. If youre running it locally, it should be `http://localhost:7001`.
5. Change the following environment variables to define the Strapi super user:
1. `SUPERUSER_EMAIL`: the super users email. By default, its `support@medusa-commerce.com`.
2. `SUPERUSER_USERNAME`: the super users username. By default, its `SuperUser`.
3. `SUPERUSER_PASSWORD`: the super users password. By default, its `MedusaStrapi1`.
4. `SUPERUSER_FIRSTNAME`: the super users first name. By default, its `Medusa`.
5. `SUPERUSER_LASTNAME`: the super users last name. By default, its `Commerce`.
6. Change the database environment variables based on your database configurations. All database environment variables start with `DATABASE_`.
7. You can optionally configure other services, such as S3 or MeiliSearch, as explained [here](https://github.com/SGFGOV/medusa-strapi-repo/tree/development/packages/medusa-strapi#media-bucket).
### Build Packages
Once youre done, install and build packages in the root `medusa-strapi-repo` directory:
```bash npm2yarn
# Install packages
npm install
# Build packages
npm run build
```
---
## Install the Strapi Plugin in Medusa
<Note type="check">
- An Event Module installed in the Medusa application, such as the [Redis Event Module](../../../architectural-modules/event/redis/page.mdx).
</Note>
To install the Strapi plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-plugin-strapi-ts
```
Next, add the plugin to the `plugins` array in `medusa-config.js`:
```js title="medusa-config.js"
const plugins = [
// ...
{
resolve: "medusa-plugin-strapi-ts",
options: {
strapi_protocol: process.env.STRAPI_PROTOCOL,
strapi_host: process.env.STRAPI_SERVER_HOSTNAME,
strapi_port: process.env.STRAPI_PORT,
strapi_secret: process.env.STRAPI_SECRET,
strapi_default_user: {
username: process.env.STRAPI_MEDUSA_USER,
password: process.env.STRAPI_MEDUSA_PASSWORD,
email: process.env.STRAPI_MEDUSA_EMAIL,
confirmed: true,
blocked: false,
provider: "local",
},
strapi_admin: {
username: process.env.STRAPI_SUPER_USERNAME,
password: process.env.STRAPI_SUPER_PASSWORD,
email: process.env.STRAPI_SUPER_USER_EMAIL,
},
auto_start: true,
},
},
]
```
### Strapi Plugin Options
1. `strapi_protocol`: The protocol of the Strapi server. If running locally, it should be `http`. Otherwise, it should be `https`.
2. `strapi_host`: the domain of the Strapi server. If running locally, use `127.0.0.1`.
3. `strapi_port`: the port that the Strapi server is running on, if any. If running locally, use `1337`.
4. `strapi_secret`: the same secret used for the `MEDUSA_STRAPI_SECRET` environment variable in the Strapi project.
5. `strapi_default_user`: The details of an existing user or a user to create in the Strapi backend that is used to update data in Strapi. Its an object accepting the following properties:
1. `username`: The users username.
2. `password`: The users password.
3. `email`: The users email.
4. `confirmed`: Whether the user is confirmed.
5. `blocked`: Whether the user is blocked.
6. `provider`: The name of the authentication provider.
6. `strapi_admin`: The details of the super admin. The super admin is only used to create the default user if it doesnt exist. Its an object accepting the following properties:
1. `username`: the super admins username. Its value is the same as that of the `SUPERUSER_USERNAME` environment variable in the Strapi project.
2. `password`: the super admins password. Its value is the same as that of the `SUPERUSER_PASSWORD` environment variable in the Strapi project.
3. `email`: the super admins email. Its value is the same as that of the `SUPERUSER_EMAIL` environment variable in the Strapi project.
7. `auto_start`: Whether to initialize the Strapi connection when Medusa starts. Disabling this may cause issues when syncing data from Medusa to Strapi.
Refer to the [plugins README](https://github.com/SGFGOV/medusa-strapi-repo/blob/development/packages/medusa-plugin-strapi-ts/README.md) for more options.
### Environment Variables
Make sure to add the necessary environment variables for the above options in `.env`:
```bash
STRAPI_PROTOCOL=http
STRAPI_SERVER_HOSTNAME=127.0.0.1
STRAPI_PORT=1337
STRAPI_SECRET=supersecret
STRAPI_MEDUSA_USER=medusa
STRAPI_MEDUSA_PASSWORD=supersecret
STRAPI_MEDUSA_EMAIL=admin@medusa-test.com
STRAPI_SUPER_USERNAME=SuperUser
STRAPI_SUPER_PASSWORD=MedusaStrapi1
STRAPI_SUPER_USER_EMAIL=support@medusa-commerce.com
```
---
## Test the Plugin
To test the integration between Medusa and Strapi, first, start the Strapi server by running the following command in the `medusa-strapi-repo/packages/medusa-strapi` directory:
```bash title="medusa-strapi-repo/packages/medusa-strapi" npm2yarn
npm run develop
```
Then, start the Medusa application:
```bash title="Medusa Backend" npm2yarn
npx medusa develop
```
If the connection to Strapi is successful, youll find the following message logged in your Medusa application with no errors:
```bash
info: Checking Strapi Health ,data:
debug: check-url: http://127.0.0.1:1337/_health ,data:
info: Strapi Subscriber Initialized
```
### Synced Entities
The Medusa and Strapi plugins support syncing the following Medusa data models:
- `Region`
- `Product`
- `ProductVariant`
- `ProductCollection`
- `ProductCategory`
### Two-Way Syncing
To test syncing data from Medusa to Strapi, try creating or updating a product either using the Medusa Admin or the [REST APIs](https://docs.medusajs.com/api/admin#products_postproducts). This triggers the associated event in Medusa, which makes the updates in Strapi.
<Note title="Tip">
Data is only synced to Strapi once you create or update them. So, if you have products in your Medusa application from before integrating Strapi, they wont be available by default in Strapi. Youll have to make updates to them, which triggers the update in Strapi.
</Note>
To test syncing data from Strapi to Medusa, try updating one of the products in the Strapi dashboard. If you check the products details in Medusa, theyre updated as expected.
@@ -1,487 +0,0 @@
import { Table } from "docs-ui"
export const metadata = {
title: `Brightpearl Plugin`,
}
# {metadata.title}
## Features
[Brightpearl](https://www.brightpearl.com/) is a Retail Operations Platform. It can be integrated to a business's different sales channels to provide features related to inventory management, automation, analytics and reporting, and more.
Medusa provides an official Brightpearl plugin with the following features:
- Send and sync orders with Brightpearl.
- Listen for inventory and stock movements in Brightpearl.
- Handle order returns through Brightpearl.
---
## Install the Brightpearl Plugin
<Note type="check">
- [Brightpearl account](https://www.brightpearl.com/)
</Note>
To install the Brightpearl plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-plugin-brightpearl
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
export const highlights = [
["6", "account", "The Brightpearl account ID."],
["7", "backend_url", "The URL of the Medusa application."],
["8", "channel_id", "The ID of the channel to map sales and credits to."],
["9", "event_owner", "The ID of the contact used when sending the Goods-Out Note Event."],
["10", "warehouse", "The ID of the warehouse to allocate order items' inventory from."],
]
```js title="medusa-config.js" highlights={highlights}
const plugins = [
// ...
{
resolve: `medusa-plugin-brightpearl`,
options: {
account: process.env.BRIGHTPEARL_ACCOUNT,
backend_url: process.env.BRIGHTPEARL_BACKEND_URL,
channel_id: process.env.BRIGHTPEARL_CHANNEL_ID,
event_owner: process.env.BRIGHTPEARL_EVENT_OWNER,
warehouse: process.env.BRIGHTPEARL_WAREHOUSE,
},
},
]
```
### Brightpearl Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell>Required</Table.HeaderCell>
<Table.HeaderCell>Default</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`account`
</Table.Cell>
<Table.Cell>
a string indicating your [Brightpearl account ID](https://help.brightpearl.com/s/article/360028541892#:~:text=Your%20account%20ID%20can%20be,your%20email%20address%20and%20password).
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`backend_url`
</Table.Cell>
<Table.Cell>
A string indicating the URL of your Medusa application. This is useful for webhooks.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`channel_id`
</Table.Cell>
<Table.Cell>
A string indicating the ID of the channel to map sales and credits to.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`event_owner`
</Table.Cell>
<Table.Cell>
A string indicating the ID of the contact used when sending the [Goods-Out Note Event](https://api-docs.brightpearl.com/warehouse/goods-out-note%20event/post.html).
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`warehouse`
</Table.Cell>
<Table.Cell>
A string indicating the ID of the warehouse to allocate order items' inventory from.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`default_status_id`
</Table.Cell>
<Table.Cell>
A string indicating the ID of the status to assign new orders. This value will also be used on
swaps or claims if their respective options, `swap_status_id` and `claim_status_id`, are not provided.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`3`
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`swap_status_id`
</Table.Cell>
<Table.Cell>
A string indicating the ID of the status to assign new swaps.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
Value of `default_status_id`.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`claim_status_id`
</Table.Cell>
<Table.Cell>
A string indicating the ID of the status to assign new claims.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
Value of `default_status_id`.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`payment_method_code`
</Table.Cell>
<Table.Cell>
A string indicating the payment method code to register payments with.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`1220`
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`sales_account_code`
</Table.Cell>
<Table.Cell>
A string indicating the nominal code to assign line items to.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`4000`
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`shipping_account_code`
</Table.Cell>
<Table.Cell>
A string indicating the nominal code to assign shipping lines to.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`4040`
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`discount_account_code`
</Table.Cell>
<Table.Cell>
A string indicating the nominal code to use for discount-type refunds.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`gift_card_account_code`
</Table.Cell>
<Table.Cell>
A string indicating the nominal code to use for gift card products and redeems.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`4000`
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`inventory_sync_cron`
</Table.Cell>
<Table.Cell>
A string indicating a cron pattern that should be used to create a scheduled job
for syncing inventory. If not provided, the scheduled job will not be created.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`cost_price_list`
</Table.Cell>
<Table.Cell>
A string indicating the ID of the price list to assign to created claims.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`1`
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`base_currency`
</Table.Cell>
<Table.Cell>
A string indicating the ISO 3 character code of the currency to assign to created claims.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`EUR`
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### Environment Variables
Make sure to add the necessary environment variables for the above options in `.env`:
```bash
BRIGHTPEARL_ACCOUNT=<YOUR_ACCOUNT>
BRIGHTPEARL_CHANNEL_ID=<YOUR_CHANNEL_ID>
BRIGHTPEARL_BACKEND_URL=<YOUR_BACKEND_URL>
BRIGHTPEARL_EVENT_OWNER=<YOUR_EVENT_OWNER>
BRIGHTPEARL_WAREHOUSE=<YOUR_WAREHOUSE>
BRIGHTPEARL_DEFAULT_STATUS_ID=<YOUR_DEFAULT_STATUS_ID>
BRIGHTPEARL_SWAP_STATUS_ID=<YOUR_SWAP_STATUS_ID>
BRIGHTPEARL_CLAIM_STATUS_ID=<YOUR_CLAIM_STATUS_ID>
BRIGHTPEARL_PAYMENT_METHOD_CODE=<YOUR_PAYMENT_METHOD_CODE>
BRIGHTPEARL_SALES_ACCOUNT_CODE=<YOUR_SALES_ACCOUNT_CODE>
BRIGHTPEARL_SHIPPING_ACCOUNT_CODE=<YOUR_SHIPPING_ACCOUNT_CODE>
BRIGHTPEARL_DISCOUNT_ACCOUNT_CODE=<YOUR_DISCOUNT_ACCOUNT_CODE>
BRIGHTPEARL_GIFT_CARD_ACCOUNT_CODE=<YOUR_GIFT_CARD_ACCOUNT_CODE>
BRIGHTPEARL_INVENTORY_SYNC_CRON=<YOUR_INVENTORY_SYNC_CRON>
BRIGHTPEARL_COST_PRICE_LIST=<YOUR_COST_PRICE_LIST>
BRIGHTPEARL_BASE_CURRENCY=<YOUR_BASE_CURRENCY>
```
---
## Test the Plugin
To test the plugin, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, place an order either using a [storefront](../../../nextjs-starter/page.mdx) or the [Store REST APIs](https://docs.medusajs.com/api/store). The order should appear on Brightpearl.
---
## How the Plugin Works
### OAuth
The plugin registers an OAuth app in Medusa allowing installation at `<MEDUSA_URL>/a/settings/apps`, where `<MEDUSA_URL>` is the URL of your Medusa application.
The OAuth tokens are refreshed every hour to prevent unauthorized requests.
### Orders and Fulfillments
When an order is created in the Medusa application, it'll automatically be sent to Brightpearl and allocated there. Once allocated, it is up to Brightpearl to figure out how the order is to be fulfilled.
The plugin listens for Goods-Out notes and tries to map each of these to a Medusa order. If the matching succeeds, the Medusa application sends the order to the fulfillment provider associated with the shipping method selected by the Customer.
### Order Returns
When line items in an order are returned, the plugin will generate a sales credit in Brightpearl.
### Products
The plugin doesn't automatically create products in Medusa, but listens for inventory changes in Brightpearl. Then, the plugin updates each product variant to reflect the inventory quantity listed in Brightpearl, thereby ensuring that the inventory levels in Medusa are always in sync with Brightpearl.
@@ -1,48 +0,0 @@
export const metadata = {
title: `Manual Fulfillment Plugin`,
}
# {metadata.title}
## Features
The manual fulfillment plugin is a minimal plugin that allows merchants to handle fulfillments manually. This plugin is installed by default in your Medusa application.
The manual fulfillment plugin is similar to a cash-on-delivery (COD) fulfillment plugin. While the merchant can use shipping and fulfillment functionalities, they only change data in the database. The merchant has to handle the actual fulfillment of the order manually.
---
## Install the Manual Fulfillment Plugin
To install the Manual Fulfillment plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-fulfillment-manual
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
```js title="medusa-config.js"
const plugins = [
// ...
{
resolve: `medusa-fulfillment-manual`,
},
]
```
---
## Test the Plugin
To test the plugin, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, you must enable the Manual Fulfillment Provider in at least one region to use it. You can do that using either the [Medusa Admin](!user-guide!/settings/regions/providers), or the [Admin API Routes](https://docs.medusajs.com/api/admin#regions_postregionsregionfulfillmentproviders).
After enabling the provider, you must add shipping options for that provider. You can also do that using either the [Medusa Admin](!user-guide!/settings/regions/shipping-options) or the [Admin API Routes](https://docs.medusajs.com/api/admin#shipping-options_postshippingoptions).
Finally, try to place an order using either a [storefront](../../../nextjs-starter/page.mdx) or the [Store API Routes](https://docs.medusajs.com/api/store). You can use the shipping options you created for the fulfillment provider.
@@ -1,333 +0,0 @@
import { Table } from "docs-ui"
export const metadata = {
title: `Webshipper Plugin`,
}
# {metadata.title}
## Features
[Webshipper](https://webshipper.com/) is a service that allows merchants to connect to multiple carriers through a single Webshipper account. Developers can then integrate webshipper with ecommerce store like Medusa to handle shipping and fulfillment.
Medusa provides an official plugin that allows you to integrate Webshipper in your store. When integrated, you can provide customers with Webshippers' shipping options on checkout, and process and handle fulfillment and shipments of orders through Webshipper.
---
## Install the Webshipper Plugin
<Note type="check">
- [Webshipper account](https://webshipper.com/)
</Note>
To install the Webshipper plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-fulfillment-webshipper
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
export const highlights = [
["6", "account", "The Webshipper account name."],
["7", "api_token", "The Webshipper API token."],
["8", "order_channel_id", "The ID of the order channel to retrieve shipping rates from."],
["10", "webhook_secret", "The secret used to sign the HMAC in webhooks."],
]
```js title="medusa-config.js" highlights={highlights}
const plugins = [
// ...
{
resolve: `medusa-fulfillment-webshipper`,
options: {
account: process.env.WEBSHIPPER_ACCOUNT,
api_token: process.env.WEBSHIPPER_API_TOKEN,
order_channel_id:
process.env.WEBSHIPPER_ORDER_CHANNEL_ID,
webhook_secret: process.env.WEBSHIPPER_WEBHOOK_SECRET,
},
},
]
```
### Webshipper Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell>Required</Table.HeaderCell>
<Table.HeaderCell>Default</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`account`
</Table.Cell>
<Table.Cell>
A string indicating your account name. It's in the first part of the URL you use when accessing the Webshipper UI which has the format `https://<account_name>.webshipper.io`.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`api_token`
</Table.Cell>
<Table.Cell>
A string indicating your API token. You can create it from the Webshipper UI under Settings > Access and tokens.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`order_channel_id`
</Table.Cell>
<Table.Cell>
A string indicating the ID of the order channel to retrieve shipping rates from.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`webhook_secret`
</Table.Cell>
<Table.Cell>
A string indicating the secret used to sign the HMAC in webhooks.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`return_address`
</Table.Cell>
<Table.Cell>
An object that indicates the return address to use when fulfilling an order return. Refer to [Webshipper's API reference](https://docs.webshipper.io/#shipping_addresses) for accepted properties in this object.
</Table.Cell>
<Table.Cell>
Yes for returns.
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`coo_countries`
</Table.Cell>
<Table.Cell>
A string or an array of strings, each being an ISO 3 character country codes used when attaching a Certificate of Origin. To support all countries you can set the value to `all`.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`all`
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`delete_on_cancel`
</Table.Cell>
<Table.Cell>
A boolean value that determines whether Webshipper orders should be deleted when its associated Medusa fulfillment is canceled.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`false`
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`document_size`
</Table.Cell>
<Table.Cell>
A string indicating the size used when retrieving documents, such as fulfillment documents. The accepted values, are `100X150`, `100X192`, or `A4`.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`A4`
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`return_portal`
</Table.Cell>
<Table.Cell>
An object that includes options related to order returns. It includes the following properties:
- `id`: is a string indicating the ID of the return portal to use when fulfilling an order return.
- `cause_id`: is a string indicating the ID of the return cause to use when fulfilling an order return.
- `refund_method_id` is a string indicating the ID of the refund method to use when fulfilling an order return.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### Environment Variables
Make sure to add the necessary environment variables for the above options in `.env`:
```bash
WEBSHIPPER_ACCOUNT=<YOUR_WEBSHIPPER_ACCOUNT>
WEBSHIPPER_API_TOKEN=<YOUR_WEBSHIPPER_API_TOKEN>
WEBSHIPPER_ORDER_CHANNEL_ID=<YOUR_WEBSHIPPER_ORDER_CHANNEL_ID>
WEBSHIPPER_WEBHOOK_SECRET=<YOUR_WEBSHIPPER_WEBHOOK_SECRET>
```
---
## Test the Plugin
To test the plugin, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, enable the Webshipper Fulfillment Provider in at least one region to use it. You can do that using either the [Medusa Admin](!user-guide!/settings/regions/providers) or the [Admin API Routes](https://docs.medusajs.com/api/admin#regions_postregionsregionfulfillmentproviders).
After enabling the provider, add shipping options for that provider using either the [Medusa Admin](!user-guide!/settings/regions/shipping-options) or the [Admin API Routes](https://docs.medusajs.com/api/admin#shipping-options_postshippingoptions).
Finally, try to place an order using either a [storefront](../../../nextjs-starter/page.mdx) or the [Store API Routes](https://docs.medusajs.com/api/store).
---
## Personal Customs Numbers
In countries like South Korea, a personal customs number is required to clear customs. The Webshipper plugin can pass this information to Webshipper given that the number is stored in `order.shipping_address.metadata.personal_customs_no`.
### Add Field in Checkout Flow
To allow the customer to pass their personal customs number along with the order, dynamically show an input field to the customer when they are shopping from a region that requires a personal customs number. Then, make sure that the `metadata` field includes the personal customs number when updating the cart's shipping address.
```ts
const onUpdateAddress = async () => {
const address = {
first_name: "John",
last_name: "Johnson",
// ...,
metadata: {
// TODO the value should be replaced with the
// value entered by the customer
personal_customs_no: "my-customs-number",
},
}
await medusaClient.carts
.update(cartId, {
shipping_address: address,
})
.then(() => {
console.log(
"Webshipper will pass along the customs number"
)
})
}
```
@@ -1,238 +0,0 @@
import { Table } from "docs-ui"
export const metadata = {
title: `Mailchimp Plugin`,
}
# {metadata.title}
## Features
[Mailchimp](https://mailchimp.com) is an email marketing service used to create newsletters and subscriptions.
By integrating Mailchimp with Medusa, customers can subscribe from Medusa to your Mailchimp newsletter and are automatically added to your Mailchimp subscribers list.
---
## Install the Mailchimp Plugin
<Note type="check">
- [Mailchimp account](https://mailchimp.com/signup)
- [Mailchimp API Key](https://mailchimp.com/help/about-api-keys/#Find_or_generate_your_API_key)
- [Mailchimp Audience ID](https://mailchimp.com/help/find-audience-id/)
</Note>
To install the Mailchimp plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-plugin-mailchimp
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
export const highlights = [
["6", "api_key", "The Mailchimp API Key."],
["7", "newsletter_list_id", "The Mailchimp Audience ID."],
]
```js title="medusa-config.js" highlights={highlights}
const plugins = [
// ...,
{
resolve: `medusa-plugin-mailchimp`,
options: {
api_key: process.env.MAILCHIMP_API_KEY,
newsletter_list_id:
process.env.MAILCHIMP_NEWSLETTER_LIST_ID,
},
},
]
```
### Mailchimp Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`api_key`
</Table.Cell>
<Table.Cell>
A string indicating the Mailchimp API Key.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`newsletter_list_id`
</Table.Cell>
<Table.Cell>
A string indicating the Mailchimp Audience ID.
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### Environment Variables
Make sure to add the necessary environment variables for the above options in `.env`:
```bash
MAILCHIMP_API_KEY=<YOUR_API_KEY>
MAILCHIMP_NEWSLETTER_LIST_ID=<YOUR_NEWSLETTER_LIST_ID>
```
---
## Test the Plugin
To test the plugin, start the Medusa application:
```bash npm2yarn
npm run dev
```
This plugin adds new `POST` and `PUT` API Routes at `/mailchimp/subscribe`. These API Routes require in the body of the request an `email` field. You can also optionally include a `data` object that holds any additional data you want to send to Mailchimp.
Check out [Mailchimps subscription documentation](https://mailchimp.com/developer/marketing/api/list-merges/) for more details on the data you can send.
### Without Additional Data
Try sending a `POST` or `PUT` request to `/mailchimp/subscribe`:
```bash noReport apiTesting testApiUrl="http://localhost:9000/mailchimp/subscribe" testApiMethod="POST" testBodyParams={{ "email": "example@gmail.com" }}
curl -X POST http://localhost:9000/mailchimp/subscribe \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "example@gmail.com"
}'
```
When the subscription is successful, a `200` response code is returned with `OK` message.
When the same email address is used again in the `POST`, a `400` response is returned. If this can occur in your usecase, use the `PUT` API Route instead.
Check your Mailchimp dashboard, you should find the email added to your Audience list.
### With Additional Data
For example, send in the `data` request body parameter a `tags` array:
```bash noReport apiTesting testApiUrl="http://localhost:9000/mailchimp/subscribe" testApiMethod="POST" testBodyParams={{ "email": "example@gmail.com" }}
curl -X POST http://localhost:9000/mailchimp/subscribe \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "example@gmail.com",
"data": {
"tags": ["customer"]
}
}'
```
All fields inside `data` is sent to Mailchimp along with the email.
---
## Use MailchimpService
Use the `MailchimpService` to subscribe users to the newsletter in other contexts. This service has a method `subscribeNewsletter` that subscribes a customer to the newsletter.
For example:
```ts title="src/subscribers/customer-created.ts"
import {
type SubscriberConfig,
type SubscriberArgs,
CustomerService,
} from "@medusajs/medusa"
export default async function handleCustomerCreated({
data,
container,
}: SubscriberArgs<Record<string, string>>) {
const mailchimpService = container.resolve("mailchimpService")
mailchimpService.subscribeNewsletter(
data.email,
{ tags: ["customer"] } // optional
)
}
export const config: SubscriberConfig = {
event: CustomerService.Events.CREATED,
}
```
This creates a subscriber that listens to the `CustomerService.Events.CREATED` (`customer.created`) event and subscribes the customer automatically using the `mailchimpService`.
---
## Add Subscription Form
This section provides a simple example of adding a subscription form in your storefront. The code is for React-based frameworks, but you can use the same logic for your storefronts regardless of the framework you are using.
You need to use [axios](https://github.com/axios/axios) to send API requests, so start by installing it in your storefront project:
```bash npm2yarn
npm install axios
```
Then, create the following component that uses the mailchimp plugin's API route to subscribe customers:
```tsx
import axios from "axios"
import { useState } from "react"
export default function NewsletterForm() {
const [email, setEmail] = useState("")
function subscribe(e) {
e.preventDefault()
if (!email) {
return
}
axios.post("http://localhost:9000/mailchimp/subscribe", {
email,
})
.then((e) => {
alert("Subscribed successfully!")
setEmail("")
})
.catch((e) => {
console.error(e)
alert("An error occurred")
})
}
return (
<form onSubmit={subscribe}>
<h2>Sign Up for our newsletter</h2>
<input
type="email"
name="email"
id="email"
placeholder="example@gmail.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Subscribe</button>
</form>
)
}
```
File diff suppressed because it is too large Load Diff
@@ -1,160 +0,0 @@
import { Table } from "docs-ui"
export const metadata = {
title: `Slack Plugin`,
}
# {metadata.title}
## Features
Slack is a communication platform used by teams and organizations for collaboration and messaging. This plugin sends merchants a slack message when a new order is placed.
The notification contains details about the order including:
- Customer's details and address.
- Items ordered, their quantity, and the price.
- Order totals including Tax amount.
- Promotion details if there are any (this is optional and can be turned off).
---
## Install the Slack Plugin
<Note type="check">
- [Slack account](https://slack.com)
- [A Slack app](https://api.slack.com/start/quickstart#creating)
- [Activate incoming webhooks in Slack and create a new webhook](https://api.slack.com/start/quickstart#webhooks)
</Note>
To install the Slack plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-plugin-slack-notification
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
export const highlights = [
["6", "slack_url", "The Slack webhook URL."],
["7", "show_discount_code", "Whether to show the discount code after creating the Slack app."],
["8", "admin_orders_url", "The prefix of the URL of the order detail pages on your admin panel."],
]
```js title="medusa-config.js" highlights={highlights}
const plugins = [
// ...
{
resolve: `medusa-plugin-slack-notification`,
options: {
slack_url: process.env.SLACK_WEBHOOK_URL,
show_discount_code: false,
admin_orders_url: `http://localhost:7001/a/orders`,
},
},
]
```
### Twilio SMS Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell>Required</Table.HeaderCell>
<Table.HeaderCell>Default</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`slack_url`
</Table.Cell>
<Table.Cell>
A string indicating the Slack webhook URL.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`show_discount_code`
</Table.Cell>
<Table.Cell>
A boolean whether to show the discount code after creating the Slack app.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`false`
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`admin_orders_url`
</Table.Cell>
<Table.Cell>
A string indicating the prefix of the URL of the order detail pages on your admin panel.
If youre using Medusa Admin locally, it should be `http://localhost:7001/a/orders`. This results in a URL like `http://localhost:7001/a/orders/order_01FYP7DM7PS43H9VQ1PK59ZR5G`.
</Table.Cell>
<Table.Cell>
No, but if not provided the order URL in the messages will be `/order_01FYP7DM7PS43H9VQ1PK59ZR5G`
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### Environment Variables
Make sure to add the necessary environment variables for the above options in `.env`:
```bash
SLACK_WEBHOOK_URL=<YOUR_WEBHOOK_URL>
```
---
## Test the Plugin
To test the plugin, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, place an order using either a [storefront](../../../nextjs-starter/page.mdx) or the [Store API Routes](https://docs.medusajs.com/api/store). A message is sent to the DM or slack channel you configured in the Slack webhook.
@@ -1,165 +0,0 @@
import { Table } from "docs-ui"
export const metadata = {
title: `Twilio SMS Plugin`,
}
# {metadata.title}
## Features
[Twilios SMS API](https://www.twilio.com/sms) is used to send users SMS messages instantly. It has a lot of additional features such as Whatsapp messaging and conversations.
By integrating Twilio SMS into Medusa, youll have easy access to Twilios SMS API to send SMS messages to your users and customers. You can use it to send order confirmations, verification codes, reset password messages, and more.
This plugin only gives you access to the Twilio SMS API but doesn't automate sending messages. Youll have to add this yourself where you need it. There's an [example later in this guide](#example-plugin-usage) on how to send an SMS for a new order.
---
## Install the Twilio SMS Plugin
<Note type="check">
- [Twilio account](https://www.twilio.com/sms)
- [Twilio account SID](https://help.twilio.com/articles/14726256820123-What-is-a-Twilio-Account-SID-and-where-can-I-find-it-)
- [Twilio auth token](https://help.twilio.com/articles/223136027-Auth-Tokens-and-How-to-Change-Them?_gl=1*qv22ht*_ga*OTY3NzYwMDAzLjE2OTE0MjA5MDI.*_ga_RRP8K4M4F3*MTcwOTIwMDA2Ny40LjAuMTcwOTIwMDA2Ny4wLjAuMA..)
- [Twilio phone number](https://help.twilio.com/articles/223135247)
</Note>
To install the Twilio SMS plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-plugin-twilio-sms
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
export const highlights = [
["6", "account_sid", "The Twilio account SID."],
["7", "auth_token", "The Twilio auth token."],
["8", "from_number", "The Twilio phone number."],
]
```js title="medusa-config.js" highlights={highlights}
const plugins = [
// ...
{
resolve: `medusa-plugin-twilio-sms`,
options: {
account_sid: process.env.TWILIO_SMS_ACCOUNT_SID,
auth_token: process.env.TWILIO_SMS_AUTH_TOKEN,
from_number: process.env.TWILIO_SMS_FROM_NUMBER,
},
},
]
```
### Twilio SMS Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`account_sid`
</Table.Cell>
<Table.Cell>
A string indicating the Twilio account SID.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`auth_token`
</Table.Cell>
<Table.Cell>
A string indicating the Twilio auth token.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`from_number`
</Table.Cell>
<Table.Cell>
A string indicating the Twilio phone number.
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### Environment Variables
Make sure to add the necessary environment variables for the above options in `.env`:
```bash
TWILIO_SMS_ACCOUNT_SID=<YOUR_ACCOUNT_SID>
TWILIO_SMS_AUTH_TOKEN=<YOUR_AUTH_TOKEN>
TWILIO_SMS_FROM_NUMBER=<YOUR_TWILIO_NUMBER>
```
---
## Example Plugin Usage
Resolve and use the `TwilioSmsService` to send SMS.
For example, create the file `src/subscriber/sms.ts` with the following content:
```ts title="src/subscriber/sms.ts"
import {
type SubscriberConfig,
type SubscriberArgs,
OrderService,
} from "@medusajs/medusa"
export default async function handleOrderPlaced({
data,
container,
}: SubscriberArgs<Record<string, string>>) {
const twilioSmsService = container.resolve("twilioSmsService")
const orderService: OrderService =
container.resolve("orderService")
const order = await orderService.retrieve(data.id, {
relations: ["shipping_address"],
})
if (order.shipping_address.phone) {
twilioSmsService.sendSms({
to: order.shipping_address.phone,
body: "We have received your order #" + data.id,
})
}
}
export const config: SubscriberConfig = {
event: OrderService.Events.PLACED,
}
```
This creates a subscriber that listens to the `OrderService.Events.PLACED` (`order.placed`) event and sends an SMS to the customer confirming their order.
The `sendSms` method of the `TwilioSmsService` accepts an object whose shape is as described in [Twilio's API reference](https://www.twilio.com/docs/sms/api/message-resource#create-a-message-resource).
<Note type="warning">
If youre on a Twilio trial make sure that the phone number you entered on checkout is a [verified Twilio number on your console](https://console.twilio.com/us1/develop/phone-numbers/manage/verified).
</Note>
@@ -1,96 +0,0 @@
export const metadata = {
title: `Discount Generator Plugin`,
}
# {metadata.title}
## Features
In Medusa, merchants can create dynamic discounts that act as a template for other discounts. With dynamic discounts, merchants don't have to repeat certain conditions every time they want to create a new discount.
The discount generator plugin allows merchants and developers to generate new discounts from a dynamic discount either using the `/discount-code` API Route or the `DiscountGeneratorService`.
---
## Install the Discount Generator Plugin
To install the Discount Generator plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-plugin-discount-generator
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
```js title="medusa-config.js"
const plugins = [
// ...
{
resolve: `medusa-plugin-discount-generator`,
},
]
```
---
## Test the Plugin
<Note type="check">
- Dynamic discount. Create it using either the [Medusa Admin](!user-guide!/discounts/create) or the [Admin API routes](https://docs.medusajs.com/api/admin#discounts_postdiscounts).
</Note>
To test the plugin, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, send a `POST` request to the `/discount-code` API Route:
```bash apiTesting testApiUrl="http://localhost:9000/discount-code/" testApiMethod="POST" testBodyParams={{"discount_code": "TEST"}}
curl -X POST http://localhost:9000/discount-code/ \
--header 'Content-Type: application/json' \
--data-raw '{
"discount_code": "TEST"
}'
```
The API Route accepts in the request body the parameter `discount_code` which is a string indicating the code of the dynamic discount to generate a new discount from.
The API Route then creates the new discount from the dynamic discount and returns it in the response.
---
## Use DiscountGeneratorService
Use the `DiscountGeneratorService` to generate a discount in other resources.
For example:
```ts title="src/api/store/generate-discount-code/route.ts"
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
export const POST = async (
req: MedusaRequest,
res: MedusaResponse
) => {
// skipping validation for simplicity
const { dynamicCode } = req.body
const discountGenerator = req.scope.resolve(
"discountGeneratorService"
)
const code =
await discountGenerator.generateDiscount(dynamicCode)
res.json({
code,
})
}
```
The `DiscountGeneratorService` has the method `generateDiscount`. It accepts the code of a dynamic discount as a parameter and creates a new discount having the same attributes as the dynamic discount, but with a different, random code.
@@ -1,154 +0,0 @@
import { Table } from "docs-ui"
export const metadata = {
title: `IP Lookup (ipstack) Plugin`,
}
# {metadata.title}
## Features
Location detection in a commerce store is essential for multi-region support.
Medusa provides an IP Lookup plugin that integrates the application with [ipstack](https://ipstack.com/) to detect a customers location and region.
---
## Install the IP Lookup Plugin
<Note type="check">
- [ipstack account](https://ipstack.com/)
</Note>
To install the IP Lookup plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-plugin-ip-lookup
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
export const highlights = [
["6", "access_token", "The ipstack accounts access key"],
]
```js title="medusa-config.js" highlights={highlights}
const plugins = [
// other plugins...
{
resolve: `medusa-plugin-ip-lookup`,
options: {
access_token: process.env.IPSTACK_ACCESS_KEY,
},
},
]
```
### IP Lookup Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`access_token`
</Table.Cell>
<Table.Cell>
A string indicating the ipstack accounts access key.
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### Environment Variables
Make sure to add the necessary environment variables for the above options in `.env`:
```bash
IPSTACK_ACCESS_KEY=<YOUR_ACCESS_KEY>
```
---
## Test the Plugin
The plugin provides two resources: the `IpLookupService` and the `preCartCreation` middleware.
<Note>
Due to how Express resolves the current IP when accessing your website from `localhost`, you wont be able to test the plugin locally. You can either use tools like ngrok to expose the `9000` port to be accessed publicly, or you have to test it on a deployed Medusa application.
</Note>
### IpLookupService
The `IpLookupService` has a method `lookupIp` that accepts the IP address as a parameter, sends a request to ipstacks API, and returns the retrieved result.
For example, you can use it in a custom API route:
```ts title="src/api/store/customer-region/route.ts"
import type {
MedusaRequest,
MedusaResponse,
RegionService,
} from "@medusajs/medusa"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
const ipLookupService = req.scope.resolve("ipLookupService")
const regionService =
req.scope.resolve<RegionService>("regionService")
const ip =
req.headers["x-forwarded-for"] || req.socket.remoteAddress
const { data } = await ipLookupService.lookupIp(ip)
if (!data.country_code) {
throw new Error("Couldn't detect country code.")
}
const region = await regionService.retrieveByCountryCode(
data.country_code
)
res.json({
region,
})
}
```
### preCartCreation
The `preCartCreation` middleware can be added as a middleware to any route to attach the region ID to that route based on the users location.
For example, you can attach it to all `/store` routes to ensure the customers region is always detected:
```ts title="src/api/middlewares.ts"
import type { MiddlewaresConfig } from "@medusajs/medusa"
const { preCartCreation } = require(
"medusa-plugin-ip-lookup/api/medusa-middleware"
).default
export const config: MiddlewaresConfig = {
routes: [
{
matcher: "/store/*",
middlewares: [preCartCreation],
},
],
}
```
@@ -1,189 +0,0 @@
import { Table } from "docs-ui"
export const metadata = {
title: `Restock Notifications Plugin`,
}
# {metadata.title}
## Features
Customers browsing your products may find something that they need, but it's out of stock. In this scenario, you can keep them interested in your product by notifying them when the product is back in stock.
The Restock Notifications plugin provides new API Routes to subscribe customers to restock notifications of a specific product variant. It also triggers the `restock-notification.restocked` event whenever a product variant's stock quantity is above a specified threshold.
However, this plugin doesn't actually implement the sending of the notification, only the required implementation to trigger restock events and allow customers to subscribe to product variants' stock status. To send the notification, use a Notification plugin.
---
## Install the Restock Notifications Plugin
To install the Restock Notifications plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-plugin-restock-notification
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
```js title="medusa-config.js"
const plugins = [
// other plugins...
{
resolve: `medusa-plugin-restock-notification`,
options: {
// optional options
},
},
]
```
### Restock Notifications Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell>Default</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`trigger_delay`
</Table.Cell>
<Table.Cell>
A number indicating the time in milliseconds to delay the triggering of the `restock-notification.restocked` event.
</Table.Cell>
<Table.Cell>
`0`
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`inventory_required`
</Table.Cell>
<Table.Cell>
A number indicating the minimum inventory quantity to consider a product variant as restocked.
</Table.Cell>
<Table.Cell>
`0`
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### Run Migrations
The plugin requires changes in the database. So, before using it, run the `migrations` command:
```bash
npx medusa migrations run
```
---
## Test the Plugin
<Note type="check">
- An out-of-stock product variant. You can edit a variant's stock quantity for testing either using the [Medusa Admin](!user-guide!/products/manage) or the [Admin API Routes](https://docs.medusajs.com/api/admin#products_postproductsproductvariantsvariant).
</Note>
To test the plugin, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, send a `POST` request to the API Route `/restock-notifications/variants/{variant_id}` to subscribe to restock notifications of a product variant ID:
```bash apiTesting testApiUrl="http://localhost:9000/restock-notifications/variants/{variant_id}" testApiMethod="POST" testPathParams={{"variant_id": "variant_01G1G5V2MRX2V3PVSR2WXYPFB6"}} testBodyParams={{"email": "example@gmail.com"}}
curl -X POST http://localhost:9000/restock-notifications/variants/variant_01G1G5V2MRX2V3PVSR2WXYPFB6 \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "example@gmail.com"
}'
```
The API Route accepts the following request body parameters:
1. `email`: a string indicating the email that is subscribing to the product variant's restock notification.
2. `sales_channel_id`: an optional string indicating the ID of the sales channel to check the stock quantity in when subscribing.
After subscribing to the out-of-stock variant, change its stock quantity to the minimum inventory required to test the event trigger. The new stock quantity should be any value above `0` if you didn't set the `inventory_required` option.
{/* [Medusa Admin](../../user-guide/products/manage.mdx#manage-product-variants) */}
You can use the Medusa Admin or the [Admin API Routes](https://docs.medusajs.com/api/admin#products_postproductsproductvariantsvariant) to update the quantity.
After you update the quantity, the `restock-notification.restocked` is emitted.
---
## Example: Implement Notification Sending with SendGrid
<Note title="Tip">
The SendGrid plugin already listens to and handles the `restock-notification.restocked` event. So, if you install it, you don't need to manually create a subscriber that handles this event as explained here. This example is only provided for reference on how to send a notification to the customer using a Notification plugin.
</Note>
Here's an example of a subscriber that listens to the `restock-notification.restocked` event and uses the [SendGrid plugin](../../notifications/sendgrid/page.mdx) to send the subscribed customers an email:
```ts title="src/subscribers/restock-notification.ts"
import {
type SubscriberConfig,
type SubscriberArgs,
ProductVariantService,
} from "@medusajs/medusa"
export default async function handleRestockNotification({
data,
container,
}: SubscriberArgs<Record<string, string>>) {
const sendgridService = container.resolve("sendgridService")
const productVariantService: ProductVariantService =
container.resolve("productVariantService")
// retrieve variant
const variant = await productVariantService.retrieve(
data.variant_id
)
sendgridService.sendEmail({
templateId: "restock-notification",
from: "hello@medusajs.com",
to: data.emails,
dynamic_template_data: {
// any data necessary for your template...
variant,
},
})
}
export const config: SubscriberConfig = {
event: "restock-notification.restocked",
}
```
The handler function receives in the `data` property of the first parameter the following properties:
- `variant_id`: The ID of the variant that has been restocked.
- `emails`: An array of strings indicating the email addresses subscribed to the restocked variant.
In the handler function, you retrieve the variant by its ID using the `ProductVariantService`, then send the email using the `SendGridService`.
@@ -1,110 +0,0 @@
import { Table } from "docs-ui"
export const metadata = {
title: `Wishlist Plugin`,
}
# {metadata.title}
## Features
A wishlist allows customers to save items they like so they can browse and purchase them later.
Medusa's Wishlist plugin provides the following features:
- Allow a customer to manage their wishlist, including adding or deleting items.
- Allow a customer to share their wishlist with others using a token.
Items in the wishlist are added as line items. This allows you to implement functionalities like moving an item from the wishlist to the cart.
---
## Install the Wishlist Plugin
To install the Wishlist plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-plugin-wishlist
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
```js title="medusa-config.js"
const plugins = [
// ...
{
resolve: `medusa-plugin-wishlist`,
},
]
```
---
## Test the Plugin
To test the plugin, start the Medusa application:
```bash npm2yarn
npm run dev
```
The plugin exposes four API Routes.
### Add Item to Wishlist API Route
The `POST` API Route at `/store/customers/{customer_id}/wishlist` allows customers to add items to their existing or new wishlist:
```bash apiTesting testApiUrl="http://localhost:9000/store/customers/{customer_id}/wishlist" testApiMethod="POST" testPathParams={{"customer_id": "cus_01G2SG30J8C85S4A5CHM2S1NS2"}} testBodyParams={{"variant_id": "variant_01G1G5V2MRX2V3PVSR2WXYPFB6"}}
curl -X POST http://localhost:9000/store/customers/cus_01G2SG30J8C85S4A5CHM2S1NS2/wishlist \
--header 'Content-Type: application/json' \
--data-raw '{
"variant_id": "variant_01G1G5V2MRX2V3PVSR2WXYPFB6"
}'
```
It accepts the following body parameters:
- `variant_id`: a string indicating the ID of the product variant to add to the wishlist.
- `quantity`: (optional) a number indicating the quantity of the product variant.
- `metadata`: (optional) any metadata to attach to the wishlist item.
The request returns the full customer object. The wishlist is available in the `customer.metadata.wishlist` property, where its value is an array of items.
### Delete Item from Wishlist API Route
The `DELETE` API Route at `/store/customers/{customer_id}/wishlist` allows customers to delete items from their wishlist:
```bash apiTesting testApiUrl="http://localhost:9000/store/customers/{customer_id}/wishlist" testApiMethod="DELETE" testPathParams={{"customer_id": "cus_01G2SG30J8C85S4A5CHM2S1NS2"}} testBodyParams={{"index": 1}}
curl -X DELETE http://localhost:9000/store/customers/cus_01G2SG30J8C85S4A5CHM2S1NS2/wishlist \
--header 'Content-Type: application/json' \
--data-raw '{
"index": 1
}'
```
The API Route accepts one request body parameter `index`, which indicates the index of the item in the `customer.metadata.wishlist` array.
The request returns the full customer object. The wishlist is available in the `customer.metadata.wishlist` property, where its value is an array of items.
#### Generate Share Token API Route
The `POST` API Route at `/store/customers/{customer_id}/wishlist/share-token` allows customers to retrieve a token that can be used to share the wishlist:
```bash apiTesting testApiUrl="http://localhost:9000/store/customers/{customer_id}/wishlist/share-token" testApiMethod="POST" testPathParams={{"customer_id": "cus_01G2SG30J8C85S4A5CHM2S1NS2"}}
curl -X POST http://localhost:9000/store/customers/cus_01G2SG30J8C85S4A5CHM2S1NS2/wishlist/share-token
```
The request returns an object in the response having the property `share_token`, being the token that can be used to access the wishlist.
#### Access Wishlist with Token API Route
The `GET` API Route at `/wishlists/{token}` allows anyone to access the wishlist using its token, where `{token}` is the token retrieved from the [Generate Share Token API Route](#generate-share-token-api-token):
```bash apiTesting testApiUrl="http://localhost:9000/wishlists/{token}" testApiMethod="GET" testPathParams={{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"}}
curl http://localhost:9000/wishlists/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
```
The request returns an object in the response having the following properties:
- `items`: an array of objects, each being an item in the wishlist.
- `first_name`: a string indicating the first name of the customer that this wishlist belongs to.
-11
View File
@@ -1,11 +0,0 @@
import { ChildDocs } from "docs-ui"
export const metadata = {
title: `Plugins`,
}
# {metadata.title}
This section includes documentation for official Medusa plugins. You can find community plugins in the [Plugins Library](https://medusajs.com/plugins/)
<ChildDocs />
@@ -1,271 +0,0 @@
import { Table } from "docs-ui"
export const metadata = {
title: `Klarna Plugin`,
}
# {metadata.title}
## Features
[Klarna](https://www.klarna.com/) is a payment provider that allows customers to pay in different ways including direct payment, installment payments, payment after delivery, and more.
---
## Install the Klarna Plugin
<Note type="check">
- [Klarna business account](https://slack.com)
</Note>
To install the Klarna plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-payment-klarna
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
export const highlights = [
["6", "backend_url", "The Klarna URL."],
["7", "url", "The base Klarna URL based on your environment."],
["8", "user", "The Klarna Merchant ID (MID)."],
["9", "password", "The string associated with the Klarna Merchant ID (MID) used for authorization."],
["10", "merchant_urls", "The merchant URLs to use for orders."],
["15", "payment_collection_urls", "The merchant URLs to use for payment collections."],
]
```js title="medusa-config.js" highlights={highlights}
const plugins = [
// ...
{
resolve: `medusa-payment-klarna`,
options: {
backend_url: process.env.KLARNA_BACKEND_URL,
url: process.env.KLARNA_URL,
user: process.env.KLARNA_USER,
password: process.env.KLARNA_PASSWORD,
merchant_urls: {
terms: process.env.KLARNA_MERCHANT_TERMS_URL,
checkout: process.env.KLARNA_MERCHANT_CHECKOUT_URL,
confirmation: process.env.KLARNA_MERCHANT_CONFIRMATION_URL,
},
payment_collection_urls: {
terms: process.env.KLARNA_PAYCOL_TERMS_URL,
checkout: process.env.KLARNA_PAYCOL_CHECKOUT_URL,
confirmation: process.env.KLARNA_PAYCOL_CONFIRMATION_URL,
},
},
},
]
```
### Klarna Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell>Required</Table.HeaderCell>
<Table.HeaderCell>Default</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`backend_url`
</Table.Cell>
<Table.Cell>
A string indicating the Klarna URL.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`url`
</Table.Cell>
<Table.Cell>
A string indicating the [base Klarna URL based on your environment](https://docs.klarna.com/api/api-urls/).
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`user`
</Table.Cell>
<Table.Cell>
The [Klarna Merchant ID (MID)](https://www.klarna.com/us/business/merchant-support/what-is-a-merchant-id/).
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`password`
</Table.Cell>
<Table.Cell>
The string associated with the Klarna Merchant ID (MID) used for [API authorization](https://docs.klarna.com/api/authentication/).
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`merchant_urls`
</Table.Cell>
<Table.Cell>
An object of merchant URLs passed to [Klarna's APIs](https://docs.klarna.com/api/payments/#operation/createCreditSession) for orders. It accepts the following keys:
- `terms`: The terms URL.
- `checkout`: The checkout URL.
- `confirmation`: The confirmation URL.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`payment_collection_urls`
</Table.Cell>
<Table.Cell>
An object of merchant URLs passed to [Klarna's APIs](https://docs.klarna.com/api/payments/#operation/createCreditSession) for payment collections. It accepts the following keys:
- `terms`: The terms URL.
- `checkout`: The checkout URL.
- `confirmation`: The confirmation URL.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`language`
</Table.Cell>
<Table.Cell>
A string indicating [Klarna's locale](https://docs.klarna.com/klarna-payments/in-depth-knowledge/puchase-countries-currencies-locales/#data-mapping).
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`en-US`
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### Environment Variables
Make sure to add the necessary environment variables for the above options in `.env`:
```bash
KLARNA_BACKEND_URL=<YOUR_KLARNA_BACKEND_URL>
KLARNA_URL=<YOUR_KLARNA_URL>
KLARNA_USER=<YOUR_KLARNA_USER>
KLARNA_PASSWORD=<YOUR_KLARNA_PASSWORD>
KLARNA_MERCHANT_TERMS_URL=<YOUR_KLARNA_MERCHANT_TERMS_URL>
KLARNA_MERCHANT_CHECKOUT_URL=<YOUR_KLARNA_MERCHANT_CHECKOUT_URL>
KLARNA_MERCHANT_CONFIRMATION_URL=<YOUR_KLARNA_MERCHANT_CONFIRMATION_URL>
KLARNA_PAYCOL_TERMS_URL=<YOUR_KLARNA_PAYCOL_TERMS_URL>
KLARNA_PAYCOL_CHECKOUT_URL=<YOUR_KLARNA_PAYCOL_CHECKOUT_URL>
KLARNA_PAYCOL_CONFIRMATION_URL=<YOUR_KLARNA_PAYCOL_CONFIRMATION_URL>
```
---
## Test the Plugin
To test the plugin, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, you must enable the Klarna Payment Provider in at least one region to use it. You can do that using either the [Medusa Admin](!user-guide!/settings/regions/providers), or the [Admin API Routes](https://docs.medusajs.com/api/admin#regions_postregionsregionpaymentproviders).
Finally, try to place an order using either a [storefront](../../../nextjs-starter/page.mdx) or the [Store API Routes](https://docs.medusajs.com/api/store). You can use Klarna during checkout and to process the order's payment.
@@ -1,354 +0,0 @@
import { Table } from "docs-ui"
export const metadata = {
title: `PayPal Plugin`,
}
# {metadata.title}
## Features
[PayPal](https://www.paypal.com) is a payment processor used by millions around the world. It allows customers to purchase orders from your website using their PayPal account rather than the need to enter their card details.
As a developer, you can use PayPals SDKs and APIs to integrate PayPal as a payment method into your ecommerce store. You can test out the payment method in sandbox mode before going live with it as a payment method.
---
## Install the PayPal Plugin
<Note type="check">
- [PayPal account](https://www.paypal.com).
- [PayPal developer account](https://developer.paypal.com).
- [PayPal client ID and secret](https://developer.paypal.com/api/rest/).
- For deployed Medusa applications, a [PayPal webhook ID](https://developer.paypal.com/api/rest/webhooks/rest/). When creating the Webhook, set the value to `{medusa_url}/paypal/hooks`, where `{medusa_url}` with the URL to your deployed Medusa application.
</Note>
To install the PayPal plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-payment-paypal
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
export const highlights = [
["6", "clientId", "The PayPal client ID."],
["7", "clientSecret", "The PayPal client secret."],
["8", "sandbox", "Whether to use sandbox mode."],
]
```js title="medusa-config.js" highlights={highlights}
const plugins = [
// ...
{
resolve: `medusa-payment-paypal`,
options: {
clientId: process.env.PAYPAL_CLIENT_ID,
clientSecret: process.env.PAYPAL_CLIENT_SECRET,
sandbox: process.env.PAYPAL_SANDBOX,
},
},
]
```
### Klarna Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell>Required</Table.HeaderCell>
<Table.HeaderCell>Default</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`clientId`
</Table.Cell>
<Table.Cell>
A string indicating the [PayPal client ID](https://developer.paypal.com/api/rest/).
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`clientSecret`
</Table.Cell>
<Table.Cell>
A string indicating the [PayPal client secret](https://developer.paypal.com/api/rest/).
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`sandbox`
</Table.Cell>
<Table.Cell>
A boolean indicating whether to use sandbox mode. Enabling this is useful for testing.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`false`
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`authWebhookId`
</Table.Cell>
<Table.Cell>
A string indicating the PayPal webhook ID. This is only useful for deployed Medusa applications.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`capture`
</Table.Cell>
<Table.Cell>
A boolean indicating whether to automatically capture payments when an order is placed.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`false`. Payments are authorized when an order is placed and the admin user captures the payment manually.
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### Environment Variables
Make sure to add the necessary environment variables for the above options in `.env`:
```bash
PAYPAL_SANDBOX=true
PAYPAL_CLIENT_ID=<CLIENT_ID>
PAYPAL_CLIENT_SECRET=<CLIENT_SECRET>
```
---
## Test the PayPal Plugin
To test the plugin, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, you must enable the PayPal Payment Provider in at least one region to use it. You can do that using either the [Medusa Admin](!user-guide!/settings/regions/providers), or the [Admin API Routes](https://docs.medusajs.com/api/admin#regions_postregionsregionpaymentproviders).
Finally, try to place an order using either a [storefront](../../../nextjs-starter/page.mdx) or the [Store API Routes](https://docs.medusajs.com/api/store). You can use PayPal during checkout and to process the order's payment.
---
## Storefront Setup
This section provides an example of how to add PayPal as a payment method in custom storefronts. For the Next.js storefront, refer to [this guide](../../../nextjs-starter/page.mdx#paypal-integration)
### Integration Steps Overview
1. Show PayPals button if the PayPal processor is available for the current cart.
2. When the button is clicked, open PayPals payment portal and wait for the customer to authorize the payment.
3. If the payment is authorized successfully, set PayPals Payment Sessionas the session used to perform the payment for the current cart, then update the Payment Session on the backend with the data received from PayPals payment portal. This data is essential to the backend to verify the authorization and perform additional payment processing later such as capturing payment.
4. Complete the cart to create the order.
### Add to Custom Storefront
<Note>
This example assumes your storefront uses React. If not, the steps generally clarify how to implement it in your storefront.
</Note>
In your storefront, install the [PayPal React components library](https://www.npmjs.com/package/@paypal/react-paypal-js) and the [Medusa JS Client library](https://www.npmjs.com/package/@medusajs/medusa-js):
```bash npm2yarn
npm install @paypal/react-paypal-js @medusajs/medusa-js
```
Then, add the Client ID as an environment variable based on the framework youre using.
Next, create the file that holds the PayPal component with the following content:
export const storefrontHighlights = [
["10", "", "Initialize the Medusa JS Client."],
["16", "", "Retrieve the cart."],
["18", "handlePayment", "Initialize the payment authorization using `actions.order.authorize()` and takes the customer to authorize the payment with PayPal in another page."],
["28", "setPaymentSession", "Select the PayPal provider's payment session for the cart."],
["40", "updatePaymentSession", "Update the payment session's data with the authorization data from PayPal."],
["52", "complete", "Complete the cart and place the order."],
["71", `"<CLIENT_ID>"`, "The PayPal client ID."],
["81", "PayPalButtons", "Render a PayPal button that, when clicked, initializes the payment using PayPal."]
]
```tsx highlights={storefrontHighlights}
import {
PayPalButtons,
PayPalScriptProcessor,
} from "@paypal/react-paypal-js"
import { useEffect, useState } from "react"
import Medusa from "@medusajs/medusa-js"
function Paypal() {
const client = new Medusa({
baseUrl: "http://localhost:9000",
maxRetries: 3,
})
const [errorMessage, setErrorMessage] = useState(undefined)
const [processing, setProcessing] = useState(false)
const cart = "..." // TODO retrieve the cart here
const handlePayment = (data, actions) => {
actions.order.authorize().then(async (authorization) => {
if (authorization.status !== "COMPLETED") {
setErrorMessage(
`An error occurred, status: ${authorization.status}`
)
setProcessing(false)
return
}
const response = await client.carts.setPaymentSession(
cart.id,
{
provider_id: "paypal",
}
)
if (!response.cart) {
setProcessing(false)
return
}
await client.carts.updatePaymentSession(
cart.id,
"paypal",
{
data: {
data: {
...authorization,
},
},
}
)
const { data, type } = await client.carts.complete(
cart.id
)
if (!data || type !== "order") {
setProcessing(false)
return
}
// order successful
alert("success")
})
}
return (
<div style={{ marginTop: "10px", marginLeft: "10px" }}>
{cart !== undefined && (
<PayPalScriptProcessor
options={{
"client-id": "<CLIENT_ID>",
currency: "EUR",
intent: "authorize",
}}
>
{errorMessage && (
<span className="text-rose-500 mt-4">
{errorMessage}
</span>
)}
<PayPalButtons
style={{ layout: "horizontal" }}
onApprove={handlePayment}
disabled={processing}
/>
</PayPalScriptProcessor>
)}
</div>
)
}
export default Paypal
```
A brief overview of what this component does:
1. You initialize the Medusa JS Client.
2. You retrieve the cart. Ideally, the cart should be managed through a context. So, every time the cart has been updated the cart should be updated in the context to be accessed from all components.
3. You render a PayPal button that, when clicked, initializes the payment using PayPal. You use the components from the PayPal React components library to render the button and you pass the `PayPalScriptProcessor` component the Client ID. Make sure to replace `<CLIENT_ID>` with the environment variable you added.
4. When the button is clicked, the `handlePayment` function is executed. In this method, you initialize the payment authorization using `actions.order.authorize()`. It takes the customer to another page to log in with PayPal and authorize the payment.
5. After the payment is authorized successfully on PayPals portal, the fulfillment function passed to `actions.order.authorize().then` is executed.
6. In the fulfillment function, you select the PayPal provider's [payment session in the cart](https://docs.medusajs.com/api/store#carts_postcartscartpaymentsession). Then, you [update the payment session](https://docs.medusajs.com/api/store#carts_postcartscartpaymentsessionupdate)'s data in the Medusa application with the authorization data received from PayPal.
7. You [complete the cart and place the order](https://docs.medusajs.com/api/store#carts_postcartscartcomplete). If successful, you just show a success alert.
You can then import this component where you want to show it in your storefront.
If you run the Medusa application and the storefront, you can use the PayPal button during checkout.
@@ -1,447 +0,0 @@
import { Table } from "docs-ui"
export const metadata = {
title: `Stripe Plugin`,
}
# {metadata.title}
## Features
[Stripe](https://stripe.com/) is a battle-tested and unified platform for transaction handling. Stripe supplies you with the technical components needed to handle transactions safely and all the analytical features necessary to gain insight into your sales. These features are also available in a safe test environment which allows for a concern-free development process.
---
## Install the PayPal Plugin
<Note type="check">
- [Stripe account](https://stripe.com).
- [Stripe API Key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard)
- For deployed Medusa applications, a [Stripe webhook secret](https://docs.stripe.com/webhooks#add-a-webhook-endpoint). When creating the Webhook, set the endpoint URL to `{medusa_url}/stripe/hooks`, where `{medusa_url}` with the URL to your deployed Medusa application.
</Note>
To install the Stripe plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-payment-stripe
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
export const highlights = [
["6", "api_key", "The Stripe API key."],
]
```js title="medusa-config.js" highlights={highlights}
const plugins = [
// ...
{
resolve: `medusa-payment-stripe`,
options: {
api_key: process.env.STRIPE_API_KEY,
},
},
]
```
### Stripe Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell>Required</Table.HeaderCell>
<Table.HeaderCell>Default</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`api_key`
</Table.Cell>
<Table.Cell>
A string indicating the [Stripe API key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard).
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`webhook_secret`
</Table.Cell>
<Table.Cell>
A string indicating the [Stripe webhook secret](https://docs.stripe.com/webhooks#add-a-webhook-endpoint). This is only useful for deployed Medusa applications.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`capture`
</Table.Cell>
<Table.Cell>
A boolean indicating whether to automatically capture payments when an order is placed.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`false`. Payments are authorized when an order is placed and the admin user captures the payment manually.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`automatic_payment_methods`
</Table.Cell>
<Table.Cell>
A boolean value indicating whether to enable Stripe's automatic payment methods. This is useful if you're integrating services like Apple pay or Google pay.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`false`
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`payment_description`
</Table.Cell>
<Table.Cell>
A string used as the default description of a payment if none is available in `cart.context.payment_description`.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`webhook_delay`
</Table.Cell>
<Table.Cell>
A number indicating the delay in milliseconds before processing the webhook event.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`5000` (five seconds)
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`webhook_retries`
</Table.Cell>
<Table.Cell>
A number of times to retry the webhook event processing in case of an error.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`3`
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### Environment Variables
Make sure to add the necessary environment variables for the above options in `.env`:
```bash
STRIPE_API_KEY=<YOUR_STRIPE_API_KEY>
```
---
## Test the Stripe Plugin
To test the plugin, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, you must enable the Stripe Payment Provider in at least one region to use it. You can do that using either the [Medusa Admin](!user-guide!/settings/regions/providers), or the [Admin API Routes](https://docs.medusajs.com/api/admin#regions_postregionsregionpaymentproviders).
Finally, try to place an order using either a [storefront](../../../nextjs-starter/page.mdx) or the [Store API Routes](https://docs.medusajs.com/api/store). You can use Stripe during checkout and to process the order's payment.
---
## Webhook Events
This plugin handles the following Stripe webhook events:
- `payment_intent.succeeded`: If the payment is associated with a payment collection, the plugin captures the payments within the webhook listener of this event. Otherwise, it checks first if an order is created and, if not, completes the cart which creates the order. It also captures the payment of the order associated with the cart if it's not captured already.
- `payment_intent.amount_capturable_updated`: If no order is created for the cart associated with the payment, the webhook listener completes the cart and creates the order.
- `payment_intent.payment_failed`: the webhook listener prints the error message received from Stripe into the logs.
---
## Storefront Setup
This section provides an example of how to add Stripe as a payment method in custom storefronts. For the Next.js storefront, refer to [this guide](../../../nextjs-starter/page.mdx#stripe-integration)
### Integration Steps Overview
1. When the user reaches the payment section during checkout, [create payment sessions](https://docs.medusajs.com/api/store#carts_postcartscartpaymentsessions).
2. If the user chooses Stripe, select the Stripe provider's [the payment session](https://docs.medusajs.com/api/store#carts_postcartscartpaymentsession) in the cart.
3. After the user enters their card details and submits the form, confirm the payment with Stripe.
4. If successful, [complete the cart](https://docs.medusajs.com/api/store#carts_postcartscartcomplete) in Medusa.
### Add to Custom Storefront
<Note>
This example assumes your storefront uses React. If not, the steps generally clarify how to implement it in your storefront.
</Note>
In your storefront, install [Stripe's React and JavaScript libraries](https://docs.stripe.com/stripe-js/react) and the [Medusa JS Client library](https://www.npmjs.com/package/@medusajs/medusa-js):
```bash npm2yarn
npm install --save @stripe/react-stripe-js @stripe/stripe-js @medusajs/medusa-js
```
Then, add [Stripe's publishable key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard) as an environment variable based on the framework youre using.
After that, create a container component that holds the payment card component:
export const containerHighlights = [
["6", `"<STRIPE_PUB_KEY>"`, "Stripe's publishable key."]
]
```tsx
import { useState } from "react"
import { Elements } from "@stripe/react-stripe-js"
import Form from "./Form"
import { loadStripe } from "@stripe/stripe-js"
const stripePromise = loadStripe("<STRIPE_PUB_KEY>")
export default function Container() {
const [clientSecret, setClientSecret] = useState()
// TODO set clientSecret
return (
<div>
{clientSecret && (
<Elements
stripe={stripePromise}
options={{
clientSecret,
}}
>
<Form clientSecret={clientSecret} cartId={cartId} />
</Elements>
)}
</div>
)
}
```
In this component, you use Stripes `loadStripe` function outside of the components implementation to ensure that Stripe doesnt re-load with every change. The function accepts Stripe's publishable key.
Then, inside the components implementation, you add a state variable `clientSecret` which youll retrieve in the next section.
The `Elements` Stripe component wraps a `Form` component that youll create next. The `Elements` component allows child elements to get access to the cards inputs and their data using Stripes `useElements` hook.
Next, create a new file for the `Form` component with the following content:
```tsx
import {
CardElement,
useElements,
useStripe,
} from "@stripe/react-stripe-js"
export default function Form({ clientSecret, cartId }) {
const stripe = useStripe()
const elements = useElements()
async function handlePayment(e) {
e.preventDefault()
// TODO handle payment
}
return (
<form>
<CardElement />
<button onClick={handlePayment}>Submit</button>
</form>
)
}
```
The `useStripe` hook gives you access to the stripe instance to confirm the payment later. The `useElements` hook gives you access to the card element to retrieve the entered card details safely.
Youll now implement the integration steps explained earlier in the `Container` component.
Start by initializing the Medusa client:
```tsx
import Medusa from "@medusajs/medusa-js"
export default function Container() {
const client = new Medusa({
baseUrl: "http://localhost:9000",
maxRetries: 3,
})
// ...
}
```
Then, in the place of the `//TODO`, initialize the payment sessions and create a payment session if Stripe is available:
```tsx
client.carts.createPaymentSessions(cart.id).then(({ cart }) => {
// check if stripe is selected
const isStripeAvailable = cart.payment_sessions?.some(
(session) => session.provider_id === "stripe"
)
if (!isStripeAvailable) {
return
}
// select stripe payment session
client.carts
.setPaymentSession(cart.id, {
provider_id: "stripe",
})
.then(({ cart }) => {
setClientSecret(cart.payment_session.data.client_secret)
})
})
```
<Note>
Its assumed you have access to the `cart` object throughout your storefront. Ideally, the `cart` should be managed through a context. In that case, you probably wouldnt need a `clientSecret` state variable as you can use the client secret directly from the `cart` object.
</Note>
Once the client secret is set, the form is shown to the user.
The last step in the integration step is confirming the payment with Stripe and if its done successfully, completing the customer's order.
In the `Form` component, initialize the Medusa client or re-use the same client in the `Container` element
```tsx
import Medusa from "@medusajs/medusa-js"
export default function Form() {
const client = new Medusa({
baseUrl: "http://localhost:9000",
maxRetries: 3,
})
// ...
}
```
Then, replace the `//TODO` in the `handlePayment` function with the following content:
```jsx
return stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: elements.getElement(CardElement),
billing_details: {
name,
email,
phone,
address: {
city,
country,
line1,
line2,
postal_code,
},
},
},
}).then(({ error, paymentIntent }) => {
// TODO handle errors
client.carts.complete(cartId).then(
(resp) => console.log(resp)
)
})
```
You use the `confirmCardPayment` method in the `stripe` object passing it the client secret, which you can access in the cart object if its available through a context.
This method also requires as a second parameter an object of the customers information including `name`, `email`, and their address.
Once the promise resolves you handle any errors that could've occurred. If no errors occurred, you complete the customers order.
If you run the Medusa application and the storefront, you can use Stripe during checkout.
@@ -1,232 +0,0 @@
import { Table } from "docs-ui"
export const metadata = {
title: `Algolia Plugin`,
}
# {metadata.title}
## Features
[Algolia](https://www.algolia.com/) is a search engine service that allows developers to integrate advanced search functionalities into their websites including typo tolerance, recommended results, and quick responses.
Algolia is used for a wide range of use cases, including commerce stores. By integrating Algolia into your commerce application, you provide your customers with a better user experience and help them find what theyre looking for swifltly.
---
## Install the Algolia Plugin
<Note type="check">
- [Algolia account](https://www.algolia.com/users/sign_up)
- [Algolia app ID](https://support.algolia.com/hc/en-us/articles/11040113398673-Where-can-I-find-my-application-ID-and-the-index-name)
- [Algolia API key](https://support.algolia.com/hc/en-us/articles/11972559809681-How-do-I-find-my-Admin-API-key)
</Note>
To install the Algolia plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-plugin-algolia
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
export const highlights = [
["6", "applicationId", "The Algolia app ID."],
["7", "adminApiKey", "The Algolia API key."],
["8", "settings", "Settings of indices created in Algolia."],
["9", "products", "The name of an index to create. In this example, it's `products`."],
["10", "indexSettings", "The settings of the index."],
["11", "searchableAttributes", "The attributes that can be searched in the index."],
["12", "attributesToRetrieve", "The attributes to retrieve in the search results."],
["26", "transformer", "A function that shapes the object to be indexed."]
]
```js title="medusa-config.js" highlights={highlights}
const plugins = [
// ...
{
resolve: `medusa-plugin-algolia`,
options: {
applicationId: process.env.ALGOLIA_APP_ID,
adminApiKey: process.env.ALGOLIA_ADMIN_API_KEY,
settings: {
products: {
indexSettings: {
searchableAttributes: ["title", "description"],
attributesToRetrieve: [
"id",
"title",
"description",
"handle",
"thumbnail",
"variants",
"variant_sku",
"options",
"collection_title",
"collection_handle",
"images",
],
},
transformer: (product) => ({
objectID: product.id,
// other attributes...
}),
},
},
},
},
]
```
### Algolia Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell>Required</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`applicationId`
</Table.Cell>
<Table.Cell>
A string indicating the Algolia app ID.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`adminApiKey`
</Table.Cell>
<Table.Cell>
A string indicating the Algolia API key.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`settings`
</Table.Cell>
<Table.Cell>
An object of settings. Its keys are names of indices to create in Algolia (in the example above, `products`), and values are an object of the index's settings.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`settings.[indexName].indexSettings`
</Table.Cell>
<Table.Cell>
An object of index settings. It accepts two properties:
- `searchableAttributes`: An array of field names that can be searched.
- `attributesToRetrieve`: An array of field names retrieved in search results.
</Table.Cell>
<Table.Cell>
If `settings` is provided, this property is required.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`settings.[indexName].transformer`
</Table.Cell>
<Table.Cell>
A function used to change the shape of the indexed records. For example, you can add details related to variants or custom relations, or filter out certain products.
The function accepts as a parameter that data model object to index, such as a product, and returns an object to be indexed.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### Environment Variables
Make sure to add the necessary environment variables for the above options in `.env`:
```bash
ALGOLIA_APP_ID=<YOUR_APP_ID>
ALGOLIA_ADMIN_API_KEY=<YOUR_ADMIN_API_KEY>
```
---
## Test the Plugin
To test the plugin, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, send a `POST` request to the `/store/products/search`:
```bash apiTesting testApiUrl="http://localhost:9000/store/products/search" testApiMethod="POST" testBodyParams={{"q": "shirt"}}
curl -X POST http://localhost:9000/store/products/search \
--header 'Content-Type: application/json' \
--data-raw '{
"q": "shirt"
}'
```
The response contains a `hits` array with the results from the Algolia search engine.
### Add or Update Products
If you add or update products in your Medusa application, it'll be reflected in the Algolia indices.
---
## Add Search to your Storefront
### Next.js Starter
Refer to the [Next.js Starter guide](../../../nextjs-starter/page.mdx#configure-algolia) to learn how to configure Algolia.
### Custom Storefront
To integrate Algolia's search functionalities in your custom storefront, refer to [Algolia's InstantSearch.js documentation](https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/js/).
@@ -1,248 +0,0 @@
import { Table } from "docs-ui"
export const metadata = {
title: `MeiliSearch Plugin`,
}
# {metadata.title}
## Features
[MeiliSearch](https://www.meilisearch.com/) is a super-fast, open source search engine built in Rust. It comes with a wide range of features including typo-tolerance, filtering, and sorting.
MeiliSearch also provides a pleasant developer experience, as it is extremely intuitive and newcomer-friendly. So, even if you're new to the search engine ecosystem, [their documentation](https://docs.meilisearch.com/) is resourceful enough for everyone to go through and understand.
---
## Install the MeiliSearch Plugin
<Note type="check">
- [MeiliSearch installed](https://docs.meilisearch.com/learn/getting_started/quick_start.html#setup-and-installation)
- [MeiliSearch master key](https://www.meilisearch.com/docs/learn/security/master_api_keys#protecting-a-meilisearch-instance)
</Note>
To install the Algolia plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-plugin-meilisearch
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
export const highlights = [
["6", "config", "The MeiliSearch connection configuration object."],
["7", "host", "The MeiliSearch host."],
["8", "apiKey", "The MeiliSearch master key."],
["10", "settings", "Settings of indices created in MeiliSearch."],
["11", "products", "The name of an index to create. In this example, it's `products`."],
["12", "indexSettings", "The settings of the index."],
["13", "searchableAttributes", "The attributes that can be searched in the index."],
["18", "displayedAttributes", "The attributes to retrieve in the search results."],
["27", "transformer", "A function that shapes the object to be indexed."]
]
```js title="medusa-config.js" highlights={highlights}
const plugins = [
// ...
{
resolve: `medusa-plugin-meilisearch`,
options: {
config: {
host: process.env.MEILISEARCH_HOST,
apiKey: process.env.MEILISEARCH_API_KEY,
},
settings: {
products: {
indexSettings: {
searchableAttributes: [
"title",
"description",
"variant_sku",
],
displayedAttributes: [
"id",
"title",
"description",
"variant_sku",
"thumbnail",
"handle",
],
},
transformer: (product) => ({
id: product.id,
// other attributes...
}),
},
},
},
},
]
```
### MeiliSearch Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell>Required</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`config`
</Table.Cell>
<Table.Cell>
An object of MeiliSearch connection configurations. It accepts two properties:
- `host`: A string indicating the MeiliSearch host. For example, `http://127.0.0.1:7700`.
- `apiKey`: A string indicating the [MeiliSearch master key](https://www.meilisearch.com/docs/learn/security/master_api_keys#protecting-a-meilisearch-instance).
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`settings`
</Table.Cell>
<Table.Cell>
An object of settings. Its keys are names of indices to create in MeiliSearch (in the example above, `products`), and values are an object of the index's settings.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`settings.[indexName].indexSettings`
</Table.Cell>
<Table.Cell>
An object of index settings. It accepts two properties:
- `searchableAttributes`: An array of field names that can be searched.
- `displayedAttributes`: An array of field names retrieved in search results.
</Table.Cell>
<Table.Cell>
If `settings` is provided, this property is required.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`settings.[indexName].transformer`
</Table.Cell>
<Table.Cell>
A function used to change the shape of the indexed records. For example, you can add details related to variants or custom relations, or filter out certain products.
The function accepts as a parameter that data model object to index, such as a product, and returns an object to be indexed.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`settings.[indexName].primaryKey`
</Table.Cell>
<Table.Cell>
A string indicating which field in the data model acts as a primary key of a document. It's used to enforce unique documents in an index. Learn more in [MeiliSearch's documentation](https://docs.meilisearch.com/learn/core_concepts/primary_key.html#primary-field).
</Table.Cell>
<Table.Cell>
`id`
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### Environment Variables
Make sure to add the necessary environment variables for the above options in `.env`:
```bash
MEILISEARCH_HOST=<YOUR_MEILISEARCH_HOST>
MEILISEARCH_API_KEY=<YOUR_MASTER_KEY>
```
---
## Test the Plugin
<Note type="check">
- [MeiliSearch running in the background](https://www.meilisearch.com/docs/learn/getting_started/quick_start#running-meilisearch).
</Note>
To test the plugin, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, send a `POST` request to the `/store/products/search`:
```bash apiTesting testApiUrl="http://localhost:9000/store/products/search" testApiMethod="POST" testBodyParams={{"q": "shirt"}}
curl -X POST http://localhost:9000/store/products/search \
--header 'Content-Type: application/json' \
--data-raw '{
"q": "shirt"
}'
```
The response contains a `hits` array with the results from the MeiliSearch search engine.
### Add or Update Products
If you add or update products in your Medusa application, it'll be reflected in the MeiliSearch indices.
---
## Add Search to your Storefront
<Note type="check">
- [MeiliSearch API key](https://www.meilisearch.com/docs/learn/security/master_api_keys#creating-an-api-key).
</Note>
### Next.js Starter
Refer to the [Next.js Starter guide](../../../nextjs-starter/page.mdx#configure-meilisearch) to learn how to configure MeiliSearch.
### Custom Storefront
To integrate MeiliSearch's search functionalities in your custom storefront, refer to [MeiliSearch's documentation](https://docs.meilisearch.com/learn/what_is_meilisearch/sdks.html#front-end-tools).
@@ -1,153 +0,0 @@
import { Table } from "docs-ui"
export const metadata = {
title: `Shopify Source Plugin`,
}
# {metadata.title}
## Features
If you're migrating from Shopify to Medusa, this plugin facilitates the process. It migrates data related to your products on Shopify to Medusa.
It also registers a scheduled job that runs periodically and ensures your data is synced between Shopify and Medusa.
---
## Install the Shopify Source Plugin
<Note type="check">
- [Shopify account](https://accounts.shopify.com/lookup?rid=8ea3d7a5-0bd6-4645-8e1c-ebf19bd750c7)
- [Shopify custom app](https://help.shopify.com/en/manual/apps/app-types/custom-apps) with `read_products` admin API access scope.
- [Shopify API secret key](https://help.shopify.com/en/manual/apps/app-types/custom-apps#get-the-api-credentials-for-a-custom-app)
</Note>
To install the Shopify Source plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-source-shopify
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
export const highlights = [
["6", "domain", "The Shopify store's subdomain."],
["7", "password", "The Shopify API secret key."],
]
```js title="medusa-config.js" highlights={highlights}
const plugins = [
// ...,
{
resolve: "medusa-source-shopify",
options: {
domain: process.env.SHOPIFY_DOMAIN,
password: process.env.SHOPIFY_PASSWORD,
},
},
]
```
### Shopify Source Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell>Required</Table.HeaderCell>
<Table.HeaderCell>Default</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`domain`
</Table.Cell>
<Table.Cell>
A string indicating the Shopify store's subdomain. Your store's domain is of the format `<DOMAIN>.myshopify.com`. The `<DOMAIN>` is the value of this option.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`password`
</Table.Cell>
<Table.Cell>
A string indicating the [Shopify API secret key](https://help.shopify.com/en/manual/apps/app-types/custom-apps#get-the-api-credentials-for-a-custom-app).
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`ignore_threshold`
</Table.Cell>
<Table.Cell>
The products retrieved from Shopify are added to the cache. This option sets the number of seconds an item can live in the cache before its removed.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`2`
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### Environment Variables
Make sure to add the necessary environment variables for the above options in `.env`:
```bash
SHOPIFY_DOMAIN=<YOUR_SHOPIFY_DOMAIN>
SHOPIFY_PASSWORD=<YOUR_SHOPIFY_PASSWORD>
```
---
## Test the Plugin
To test the plugin, start the Medusa application:
```bash npm2yarn
npm run dev
```
This runs the migration script. The products are migrated from Shopify into Medusa.
@@ -1,127 +0,0 @@
import { Table } from "docs-ui"
export const metadata = {
title: `Local File Storage Plugin`,
}
# {metadata.title}
## Features
The Local File Storage plugin allows you to upload media assets, such as product images, to a local directory. This is useful during development.
<Note title="Tip">
For production, it's recommended to use a storage plugin that hosts your images on a third-party service. This storage plugin doesn't handle advanced features such as presigned URLs.
</Note>
---
## Install the Local File Storage Plugin
To install the Local File Storage plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install @medusajs/file-local
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
```js title="medusa-config.js"
const plugins = [
// ...
{
resolve: `@medusajs/file-local`,
options: {
// optional
},
},
]
```
### Local File Storage Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell>Default</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`upload_dir`
</Table.Cell>
<Table.Cell>
A string indicating the relative path to upload the files to.
</Table.Cell>
<Table.Cell>
`uploads/images`
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`backend_url`
</Table.Cell>
<Table.Cell>
A string indicating the URL of your Medusa application. This is helpful if you deploy your application or change the port used.
</Table.Cell>
<Table.Cell>
`http://localhost:9000`
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
---
## Test the Plugin
To test the plugin, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, upload a product image either using the [Medusa Admin](!user-guide!/products/manage#manage-product-media) or the [Admin API routes](https://docs.medusajs.com/api/admin#uploads_postuploads).
---
## Next.js Starter Configuration
If youre using the [Next.js Starter storefront](../../../nextjs-starter/page.mdx), add the following option to the exported object in `next.config.js`:
```js title="next.config.js"
const { withStoreConfig } = require("./store-config")
// ...
module.exports = withStoreConfig({
// ...
images: {
domains: [
// ...
"{medusa_domain}",
],
},
})
```
This adds the Medusa application's domain name into the configured images domain names. If you don't add the configuration, youll receive the error ["next/image Un-configured Host”](https://nextjs.org/docs/messages/next-image-unconfigured-host).
Make sure to replace `{medusa_domain}` with the domain of your Medusa application. For example, `localhost`.
@@ -1,296 +0,0 @@
import { Table } from "docs-ui"
export const metadata = {
title: `MinIO Plugin`,
}
# {metadata.title}
## Features
[MinIO](https://min.io/) is an open-source object storage server compatible with the Amazon S3 API. It allows users to store photos, videos, backups, and more.
With the MinIO plugin, you'll benefit from basic and advanced storage functionalities, including public and private uploads, and presigned URLs.
---
## Install the MinIO Plugin
<Note type="check">
- [Install MinIO](https://min.io/docs/minio/linux/index.html).
- Change port to `9001` using the [console address](https://min.io/docs/minio/linux/reference/minio-server/minio-server.html#minio.server.-console-address) and [address](https://min.io/docs/minio/linux/reference/minio-server/minio-server.html#minio.server.-address) CLI options.
- [MinIO bucket with public access policy](https://min.io/docs/minio/linux/administration/console/managing-objects.html#creating-buckets).
- [MinIO access and secret access key](https://min.io/docs/minio/linux/administration/console/security-and-access.html#id1)
</Note>
To install the MinIO plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-file-minio
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
export const highlights = [
["6", "bucket", "The bucket to upload files to."],
["7", "access_key_id", "The MinIO access key."],
["8", "secret_access_key", "The MinIO secret access key."],
["9", "endpoint", "The URL of your MinIO server."],
]
```js title="medusa-config.js" highlights={highlights}
const plugins = [
// ...
{
resolve: `medusa-file-minio`,
options: {
bucket: process.env.MINIO_BUCKET,
access_key_id: process.env.MINIO_ACCESS_KEY,
secret_access_key: process.env.MINIO_SECRET_KEY,
endpoint: process.env.MINIO_ENDPOINT,
},
},
]
```
### MinIO Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell>Required</Table.HeaderCell>
<Table.HeaderCell>Default</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`bucket`
</Table.Cell>
<Table.Cell>
A string indicating the bucket to upload files to.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`access_key_id`
</Table.Cell>
<Table.Cell>
A string indicating the [MinIO access key](https://min.io/docs/minio/linux/administration/console/security-and-access.html#id1).
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`secret_access_key`
</Table.Cell>
<Table.Cell>
A string indicating the [MinIO secret access key](https://min.io/docs/minio/linux/administration/console/security-and-access.html#id1).
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`endpoint`
</Table.Cell>
<Table.Cell>
A string indicating the URL of your MinIO server.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`private_bucket`
</Table.Cell>
<Table.Cell>
A string indicating the bucket to use for private media, such as the CSV file of exported products.
</Table.Cell>
<Table.Cell>
Required if you're using import/export features that require a private bucket.
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`private_access_key_id`
</Table.Cell>
<Table.Cell>
A string indicating the MinIO access key to use for private uploads.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
The value of `access_key_id`.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`private_secret_access_key`
</Table.Cell>
<Table.Cell>
A string indicating the MinIO secret access key to use for private uploads.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
The value of `secret_access_key`.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`download_url_duration`
</Table.Cell>
<Table.Cell>
A number indicating the expiry time of presigned URLs in seconds.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`60`
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### Environment Variables
Make sure to add the necessary environment variables for the above options in `.env`:
```bash
MINIO_BUCKET=<BUCKET>
MINIO_ACCESS_KEY=<ACCESS_KEY>
MINIO_SECRET_KEY=<SECRET_KEY>
MINIO_ENDPOINT=<ENDPOINT>
```
---
## Test the Plugin
To test the plugin, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, upload a product image either using the [Medusa Admin](!user-guide!/products/manage#manage-product-media) or the [Admin API routes](https://docs.medusajs.com/api/admin#uploads_postuploads).
---
## Next.js Starter Template Configuration
If youre using the [Next.js Starter storefront](../../../nextjs-starter/page.mdx), add the following option to the exported object in `next.config.js`:
```jsx title="next.config.js"
const { withStoreConfig } = require("./store-config")
// ...
module.exports = withStoreConfig({
// ...
images: {
domains: [
// ...
"{minio_domain}",
],
},
})
```
This adds the MinIO domain into the configured images domain names. If you don't add the configuration, youll receive the error ["next/image Un-configured Host”](https://nextjs.org/docs/messages/next-image-unconfigured-host).
Make sure to replace `{minio_domain}` with the MinIO domain. For example, `127.0.0.1`.
@@ -1,363 +0,0 @@
import { Table, DetailsList } from "docs-ui"
import AclErrorSection from "../../../troubleshooting/_sections/other/s3-acl.mdx"
export const metadata = {
title: `S3 Plugin`,
}
# {metadata.title}
## Features
[Amazon S3](https://aws.amazon.com/s3/) is a cloud storage service that offers scalable object storage. It allows users to store and retrieve images, videos, and other media types.
With the S3 plugin, you'll benefit from basic and advanced storage functionalities, including public and private uploads, and presigned URLs.
---
## Preparations
<Note type="check">
- [AWS account](https://console.aws.amazon.com/console/home?nc2=h_ct&src=header-signin).
- [S3 bucket](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-overview.html) with the "Public Access setting" enabled.
- [AWS user with AmazonS3FullAccess permissions](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-and-attach-iam-policy.html).
- [AWS user access key ID and secret access key](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey)
</Note>
### Bucket Policies
Change your [bucket's policy](https://docs.aws.amazon.com/AmazonS3/latest/userguide/add-bucket-policy.html) to the following:
```json
{
"Version": "2012-10-17",
"Id": "Policy1397632521960",
"Statement": [
{
"Sid": "Stmt1397633323327",
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::{bucket_name}/*"
}
]
}
```
Make sure to replace `{bucket_name}` with the name of the bucket you created.
---
## Install the S3 Plugin
To install the MinIO plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-file-s3
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
export const highlights = [
["6", "bucket", "The bucket to upload files to."],
["7", "s3_url", "The URL to the bucket."],
["8", "access_key_id", "The AWS user's access key ID."],
["9", "secret_access_key", "The AWS user's secret access key."],
["10", "region", "The bucket's region code."],
]
```js title="medusa-config.js" highlights={highlights}
const plugins = [
// ...
{
resolve: `medusa-file-s3`,
options: {
bucket: process.env.S3_BUCKET,
s3_url: process.env.S3_URL,
access_key_id: process.env.S3_ACCESS_KEY_ID,
secret_access_key: process.env.S3_SECRET_ACCESS_KEY,
region: process.env.S3_REGION,
},
},
]
```
### S3 Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell>Required</Table.HeaderCell>
<Table.HeaderCell>Default</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`bucket`
</Table.Cell>
<Table.Cell>
A string indicating the bucket to upload files to.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`s3_url`
</Table.Cell>
<Table.Cell>
A string indicating the URL to your bucket. Its in the form `https://<BUCKET_NAME>.s3.<REGION>.amazonaws.com`, where `<BUCKET_NAME>` is the name of the bucket and the `<REGION>` is the region the bucket is created in.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`access_key_id`
</Table.Cell>
<Table.Cell>
A string indicating the AWS user's access key ID.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`secret_access_key`
</Table.Cell>
<Table.Cell>
A string indicating the AWS user's secret access key.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`region`
</Table.Cell>
<Table.Cell>
A string indicating the region code of your bucket. For example, `us-east-1`.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`prefix`
</Table.Cell>
<Table.Cell>
A string indicating a prefix to apply on stored file names. If supplied, a `/` is added at the end of the prefix.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`download_file_duration`
</Table.Cell>
<Table.Cell>
A number indicating the expiry time of presigned URLs in seconds.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
[S3's default expiration time](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html#PresignedUrl-Expiration)
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`cache_control`
</Table.Cell>
<Table.Cell>
A string indicating [how long objects remain in the CloudFront's cache](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Expiration.html).
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`max-age=31536000` (a year)
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`aws_config_object`
</Table.Cell>
<Table.Cell>
An object of [AWS Configurations](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html) passed to all requests.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### Environment Variables
Make sure to add the necessary environment variables for the above options in `.env`:
```bash
S3_BUCKET=<YOUR_BUCKET_NAME>
S3_URL=<YOUR_BUCKET_URL>
S3_ACCESS_KEY_ID=<YOUR_ACCESS_KEY_ID>
S3_SECRET_ACCESS_KEY=<YOUR_SECRET_ACCESS_KEY>
S3_REGION=<YOUR_BUCKET_REGION>
```
---
## Test the S3 Plugin
To test the plugin, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, upload a product image either using the [Medusa Admin](!user-guide!/products/manage#manage-product-media) or the [Admin API routes](https://docs.medusajs.com/api/admin#uploads_postuploads).
---
## Next.js Starter Template Configuration
If youre using the [Next.js Starter storefront](../../../nextjs-starter/page.mdx), add the following option to the exported object in `next.config.js`:
```jsx title="next.config.js"
const { withStoreConfig } = require("./store-config")
// ...
module.exports = withStoreConfig({
// ...
images: {
domains: [
// ...
"{s3_domain}",
],
},
})
```
This adds the S3's domain into the configured images domain names. If you don't add the configuration, youll receive the error ["next/image Un-configured Host”](https://nextjs.org/docs/messages/next-image-unconfigured-host).
Make sure to replace `{s3_domain}` with the S3 domain which is of the format `<BUCKET_NAME>.s3.<REGION>.amazonaws.com`.
---
## Troubleshooting
<DetailsList
sections={[
{
title: 'Error: AccessControlListNotSupported: The bucket does not allow ACLs',
content: <AclErrorSection />
}
]}
/>
@@ -1,282 +0,0 @@
import { Table, DetailsList } from "docs-ui"
import AclErrorSection from "../../../troubleshooting/_sections/other/s3-acl.mdx"
export const metadata = {
title: `Spaces Plugin`,
}
# {metadata.title}
## Features
[DigitalOcean Spaces](https://www.digitalocean.com/products/spaces) is an object storage service provided by DigitalOcean. Spaces is designed to make it easy and cost-effective to store medias such as images, videos, and more.
With the DigitalOcean plugin, you'll benefit from basic and advanced storage functionalities, including public and private uploads, and presigned URLs.
---
## Install the Spaces Plugin
<Note type="check">
- [DigitalOcean account](https://cloud.digitalocean.com/registrations/new)
- [DigitalOcean Spaces bucket](https://docs.digitalocean.com/products/spaces/how-to/create/)
- [DigitalOcean Spaces access and secret access keys](https://docs.digitalocean.com/products/spaces/how-to/manage-access/#access-keys)
</Note>
To install the Spaces plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-file-spaces
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
export const highlights = [
["6", "bucket", "The bucket to upload files to."],
["7", "spaces_url", "Either the Origin Endpoint or the CDN endpoint of your Spaces Object Storage bucket."],
["8", "access_key_id", "The Spaces access key."],
["9", "secret_access_key", "The Spaces secret access key."],
["10", "region", "The region your Spaces Object Storage bucket is in."],
["11", "endpoint", "The Spaces Origin Endpoint."],
]
Then, add the following environment variables:
```js title="medusa-config.js" highlights={highlights}
const plugins = [
// ...
{
resolve: `medusa-file-spaces`,
options: {
bucket: process.env.SPACE_BUCKET,
spaces_url: process.env.SPACE_URL,
access_key_id: process.env.SPACE_ACCESS_KEY_ID,
secret_access_key: process.env.SPACE_SECRET_ACCESS_KEY,
region: process.env.SPACE_REGION,
endpoint: process.env.SPACE_ENDPOINT,
},
},
]
```
### Spaces Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell>Required</Table.HeaderCell>
<Table.HeaderCell>Default</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`bucket`
</Table.Cell>
<Table.Cell>
A string indicating the bucket to upload files to.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`spaces_url`
</Table.Cell>
<Table.Cell>
A string indicating either the Origin Endpoint or the CDN endpoint of your Spaces Object Storage bucket.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`access_key_id`
</Table.Cell>
<Table.Cell>
A string indicating the [Spaces access key](https://docs.digitalocean.com/products/spaces/how-to/manage-access/#access-keys).
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`secret_access_key`
</Table.Cell>
<Table.Cell>
A string indicating the [Spaces secret access key](https://docs.digitalocean.com/products/spaces/how-to/manage-access/#access-keys).
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`region`
</Table.Cell>
<Table.Cell>
A string indicating the region your Spaces Object Storage bucket is in. If you're unsure, you can find it in the Origin Endpoint whose format is `https://<bucket-name>.<region>.digitaloceanspaces.com`. For example, `nyc3`.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`endpoint`
</Table.Cell>
<Table.Cell>
A string indicating the Spaces Origin Endpoint.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`download_url_duration`
</Table.Cell>
<Table.Cell>
A number indicating the expiry time of presigned URLs in seconds.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`60`
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### Environment Variables
Make sure to add the necessary environment variables for the above options in `.env`:
```bash
SPACE_URL=<YOUR_SPACE_URL>
SPACE_BUCKET=<YOUR_SPACE_NAME>
SPACE_REGION=<YOUR_SPACE_REGION>
SPACE_ENDPOINT=<YOUR_SPACE_ENDPOINT>
SPACE_ACCESS_KEY_ID=<YOUR_ACCESS_KEY_ID>
SPACE_SECRET_ACCESS_KEY=<YOUR_SECRET_ACCESS_KEY>
```
---
## Test the Plugin
To test the plugin, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, upload a product image either using the [Medusa Admin](!user-guide!/products/manage#manage-product-media) or the [Admin API routes](https://docs.medusajs.com/api/admin#uploads_postuploads).
---
## Next.js Starter Template Configuration
If youre using the [Next.js Starter storefront](../../../nextjs-starter/page.mdx), add the following option to the exported object in `next.config.js`:
```jsx title="next.config.js"
const { withStoreConfig } = require("./store-config")
// ...
module.exports = withStoreConfig({
// ...
images: {
domains: [
// ...
"{spaces_domain}",
],
},
})
```
This adds the Spaces domain into the configured images domain names. If you don't add the configuration, youll receive the error ["next/image Un-configured Host”](https://nextjs.org/docs/messages/next-image-unconfigured-host).
Make sure to replace `{spaces_domain}` with the Spaces domain. It's of the format `<bucket-name>.<region>.digitaloceanspaces.com` or `<bucket-name>.<region>.cdn.digitaloceanspaces.com`
@@ -12,7 +12,7 @@ There are two types of API keys:
- `publishable`: A public key used in client applications, such as a storefront.
- `secret`: A secret key used for authentication and verification purposes, such as an admin users authentication token or a password reset token.
The API keys type is stored in the `type` field of the [ApiKey data model](/references/api-key/models/ApiKey).
The API keys type is stored in the `type` property of the [ApiKey data model](/references/api-key/models/ApiKey).
---
@@ -25,7 +25,7 @@ In this guide, youll find common examples of how you can use the API Key Modu
const apiKeyModuleService: IApiKeyModuleService =
request.scope.resolve(ModuleRegistrationName.API_KEY)
const apiKey = await apiKeyModuleService.create({
const apiKey = await apiKeyModuleService.createApiKeys({
title: "Publishable API key",
type: "publishable",
created_by: "user_123",
@@ -50,7 +50,7 @@ In this guide, youll find common examples of how you can use the API Key Modu
export async function POST(request: Request) {
const apiKeyModuleService = await initializeApiKeyModule()
const apiKey = await apiKeyModuleService.create({
const apiKey = await apiKeyModuleService.createApiKeys({
title: "Publishable API key",
type: "publishable",
created_by: "user_123",
@@ -85,7 +85,7 @@ In this guide, youll find common examples of how you can use the API Key Modu
request.scope.resolve(ModuleRegistrationName.API_KEY)
res.json({
api_keys: await apiKeyModuleService.list(),
api_keys: await apiKeyModuleService.listApiKeys(),
})
}
```
@@ -104,7 +104,7 @@ In this guide, youll find common examples of how you can use the API Key Modu
const apiKeyModuleService = await initializeApiKeyModule()
return NextResponse.json({
api_keys: await apiKeyModuleService.list(),
api_keys: await apiKeyModuleService.listApiKeys(),
})
}
```
@@ -279,7 +279,7 @@ In this guide, youll find common examples of how you can use the API Key Modu
}
)
const newKey = await apiKeyModuleService.create({
const newKey = await apiKeyModuleService.createApiKeys({
title: revokedKey.title,
type: revokedKey.type,
created_by: revokedKey.created_by,
@@ -318,7 +318,7 @@ In this guide, youll find common examples of how you can use the API Key Modu
revoked_by: params.user_id,
})
const newKey = await apiKeyModuleService.create({
const newKey = await apiKeyModuleService.createApiKeys({
title: revokedKey.title,
type: revokedKey.type,
created_by: revokedKey.created_by,
@@ -19,13 +19,13 @@ Store and manage API keys in your store. You can create both publishable and sec
- Password reset tokens when a user or customer requests to reset their password.
```ts
const pubApiKey = await apiKeyModuleService.create({
const pubApiKey = await apiKeyModuleService.createApiKeys({
title: "Publishable API key",
type: "publishable",
created_by: "user_123",
})
const secretApiKey = await apiKeyModuleService.create({
const secretApiKey = await apiKeyModuleService.createApiKeys({
title: "Authentication Key",
type: "secret",
created_by: "user_123",
@@ -66,7 +66,7 @@ const revokedKey = await apiKeyModuleService.revoke("apk_1", {
revoked_by: "user_123",
})
const newKey = await apiKeyModuleService.create({
const newKey = await apiKeyModuleService.createApiKeys({
title: revokedKey.title,
type: revokedKey.type,
created_by: revokedKey.created_by,
@@ -97,7 +97,7 @@ For example:
request.scope.resolve(ModuleRegistrationName.API_KEY)
res.json({
api_keys: await apiKeyModuleService.list(),
api_keys: await apiKeyModuleService.listApiKeys(),
})
}
```
@@ -116,7 +116,7 @@ For example:
const apiKeyModuleService: IApiKeyModuleService =
container.resolve(ModuleRegistrationName.API_KEY)
const apiKeys = await apiKeyModuleService.list()
const apiKeys = await apiKeyModuleService.listApiKeys()
}
```
@@ -136,7 +136,7 @@ For example:
ModuleRegistrationName.API_KEY
)
const apiKeys = await apiKeyModuleService.list()
const apiKeys = await apiKeyModuleService.listApiKeys()
})
```
@@ -45,7 +45,7 @@ Then, the user is authenticated successfully, and their authentication details a
<Note>
Check out the [AuthIdentity](/references/auth/models/AuthIdentity) reference for the expected fields in `authIdentity`.
Check out the [AuthIdentity](/references/auth/models/AuthIdentity) reference for the expected properties in `authIdentity`.
</Note>
@@ -13,31 +13,18 @@ Before creating an actor type, you must define a data model the actor type belon
The rest of this guide uses this `Manager` data model as an example:
```ts title="src/modules/manager/models/manager.ts"
import { BaseEntity } from "@medusajs/utils"
import {
Entity,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { model } from "@medusajs/utils"
@Entity()
export class Manager extends BaseEntity {
@PrimaryKey({ columnType: "text" })
id!: string
const Manager = model.define("manager", {
id: model.id(),
firstName: model.text(),
lastName: model.text(),
email: model.text(),
})
@Property({ columnType: "text" })
first_name: string
@Property({ columnType: "text" })
last_name: string
@Property({ columnType: "text" })
email: string
}
export default Manager
```
The modules main service must also have a `create` method to create a record of the `Manager` data model.
---
## 1. Create Workflow
@@ -45,7 +32,7 @@ The modules main service must also have a `create` method to create a record
Start by creating a workflow that does two things:
- Create a record of the `Manager` data model.
- Sets the `app_metadata` field of the associated `AuthIdentity` record based on the new actor type.
- Sets the `app_metadata` property of the associated `AuthIdentity` record based on the new actor type.
For example, create the file `src/workflows/create-manager.ts`. with the following content:
@@ -92,7 +79,7 @@ const createManagerStep = createStep(
const managerModuleService: ManagerModuleService =
container.resolve("managerModuleService")
const manager = await managerModuleService.create(
const manager = await managerModuleService.createManager(
managerData
)
@@ -127,7 +114,7 @@ This workflow accepts the managers data and the associated auth identitys
The workflow has two steps:
1. Create the manager using the `createManagerStep`.
2. Set the `app_metadata` field of the associated auth identity using the `setAuthAppMetadataStep` step imported from `@medusajs/core-flows`. You specify the actor type `manager` in the `actorType` property of the steps input.
2. Set the `app_metadata` property of the associated auth identity using the `setAuthAppMetadataStep` step imported from `@medusajs/core-flows`. You specify the actor type `manager` in the `actorType` property of the steps input.
---
@@ -250,13 +237,12 @@ export async function GET(
const managerModuleService: ManagerModuleService =
req.scope.resolve("managerModuleService")
const manager = await managerModuleService.retrieve(
const manager = await managerModuleService.retrieveManager(
req.auth_context.actor_id
)
res.json({ manager })
}
```
This route is only accessible by authenticated managers. You access the managers ID using `req.auth_context.actor_id`.
@@ -235,11 +235,12 @@ This example uses the [jsonwebtoken NPM package](https://www.npmjs.com/package/j
const authModuleService: IAuthModuleService =
req.scope.resolve(ModuleRegistrationName.AUTH)
const authIdentity = await authModuleService.create({
provider: "emailpass",
entity_id: "user@example.com",
scope: "admin",
})
const authIdentity = await authModuleService
.createAuthIdentities({
provider: "emailpass",
entity_id: "user@example.com",
scope: "admin",
})
res.json({ auth_identity: authIdentity })
}
@@ -258,11 +259,12 @@ This example uses the [jsonwebtoken NPM package](https://www.npmjs.com/package/j
export async function POST(request: Request) {
const authModuleService = await initializeAuthModule()
const authIdentity = await authModuleService.create({
provider: "emailpass",
entity_id: "user@example.com",
scope: "admin",
})
const authIdentity = await authModuleService
.createAuthIdentities({
provider: "emailpass",
entity_id: "user@example.com",
scope: "admin",
})
return NextResponse.json({
auth_identity: authIdentity,
@@ -293,7 +295,8 @@ This example uses the [jsonwebtoken NPM package](https://www.npmjs.com/package/j
req.scope.resolve(ModuleRegistrationName.AUTH)
res.json({
auth_identitys: await authModuleService.list(),
auth_identitys:
await authModuleService.listAuthIdentities(),
})
}
```
@@ -312,7 +315,8 @@ This example uses the [jsonwebtoken NPM package](https://www.npmjs.com/package/j
const authModuleService = await initializeAuthModule()
return NextResponse.json({
auth_identities: await authModuleService.list(),
auth_identities:
await authModuleService.listAuthIdentities(),
})
}
```
@@ -339,12 +343,13 @@ This example uses the [jsonwebtoken NPM package](https://www.npmjs.com/package/j
const authModuleService: IAuthModuleService =
req.scope.resolve(ModuleRegistrationName.AUTH)
const authIdentity = await authModuleService.update({
id: "authusr_123",
provider_metadata: {
test: true,
},
})
const authIdentity = await authModuleService
.updateAuthIdentites({
id: "authusr_123",
provider_metadata: {
test: true,
},
})
res.json({
auth_identity: authIdentity,
@@ -374,15 +379,16 @@ This example uses the [jsonwebtoken NPM package](https://www.npmjs.com/package/j
) {
const authModuleService = await initializeAuthModule()
const authIdentity = await authModuleService.update({
id: "authusr_123",
provider_metadata: {
test: true,
},
})
const authIdentity = await authModuleService
.updateAuthIdentites({
id: "authusr_123",
provider_metadata: {
test: true,
},
})
return NextResponse.json({
auth_identitys: await authModuleService.list(),
auth_identity: authIdentity,
})
}
```
@@ -409,7 +415,9 @@ This example uses the [jsonwebtoken NPM package](https://www.npmjs.com/package/j
const authModuleService: IAuthModuleService =
req.scope.resolve(ModuleRegistrationName.AUTH)
await authModuleService.delete(["authusr_123"])
await authModuleService.deleteAuthIdentities([
"authusr_123",
])
res.status(200)
}
@@ -437,7 +445,9 @@ This example uses the [jsonwebtoken NPM package](https://www.npmjs.com/package/j
) {
const authModuleService = await initializeAuthModule()
await authModuleService.delete(["authusr_123"])
await authModuleService.deleteAuthIdentities([
"authusr_123",
])
}
```
@@ -99,7 +99,8 @@ For example:
req.scope.resolve(ModuleRegistrationName.AUTH)
res.json({
authIdentitys: authModuleService.list(),
authIdentitys:
await authModuleService.listAuthIdentities(),
})
}
```
@@ -120,7 +121,8 @@ For example:
const authModuleService: IAuthModuleService =
container.resolve(ModuleRegistrationName.AUTH)
const authIdentitys = await authModuleService.list()
const authIdentitys = await authModuleService
.listAuthIdentities()
}
```
@@ -141,7 +143,8 @@ For example:
container.resolve(
ModuleRegistrationName.AUTH
)
const authIdentitys = await authModuleService.list()
const authIdentitys = await authModuleService
.listAuthIdentities()
})
```
@@ -18,7 +18,7 @@ A cart has a shipping and billing address. Both of these addresses are represent
A line item, represented by the `LineItem` data model, is a product variant added to the cart. A cart has multiple line items.
A line item stores some of the product variants fields, such as the `product_title` and `product_description`. It also stores data related to the items quantity and price.
A line item stores some of the product variants properties, such as the `product_title` and `product_description`. It also stores data related to the items quantity and price.
<Note>
@@ -36,8 +36,8 @@ If the shipping method is created from a shipping option, available through the
A shipping method can also be a custom method associated with this cart only.
### data Field
### data Property
After an order is placed, you may use a third-party fulfillment provider to fulfill its shipments. If the fulfillment provider requires additional custom data to be passed along from the checkout process, you can add this data in the `ShippingMethod`'s `data` field.
After an order is placed, you may use a third-party fulfillment provider to fulfill its shipments. If the fulfillment provider requires additional custom data to be passed along from the checkout process, you can add this data in the `ShippingMethod`'s `data` property.
The `data` field is an object used to store custom data relevant later for fulfillment.
The `data` property is an object used to store custom data relevant later for fulfillment.
@@ -25,7 +25,7 @@ In this guide, youll find common examples of how you can use the Cart Module
const cartModuleService: ICartModuleService =
req.scope.resolve(ModuleRegistrationName.CART)
const cart = await cartModuleService.create({
const cart = await cartModuleService.createCarts({
currency_code: "usd",
shipping_address: {
address_1: "1512 Barataria Blvd",
@@ -57,7 +57,7 @@ In this guide, youll find common examples of how you can use the Cart Module
export async function POST(request: Request) {
const cartModuleService = await initializeCartModule()
const cart = await cartModuleService.create({
const cart = await cartModuleService.createCarts({
currency_code: "usd",
shipping_address: {
address_1: "1512 Barataria Blvd",
@@ -100,7 +100,9 @@ In this guide, youll find common examples of how you can use the Cart Module
const cartModuleService: ICartModuleService =
req.scope.resolve(ModuleRegistrationName.CART)
const cart = await cartModuleService.retrieve("cart_123")
const cart = await cartModuleService.retrieveCart(
"cart_123"
)
res.json({ cart })
}
@@ -119,7 +121,9 @@ In this guide, youll find common examples of how you can use the Cart Module
export async function GET(request: Request) {
const cartModuleService = await initializeCartModule()
const cart = await cartModuleService.retrieve("cart_123")
const cart = await cartModuleService.retrieveCart(
"cart_123"
)
return NextResponse.json({
cart,
@@ -15,7 +15,7 @@ The Cart Module is the `@medusajs/cart` NPM package that provides cart-related f
Store and manage carts, including their addresses, line items, shipping methods, and more.
```ts
const cart = await cartModuleService.create({
const cart = await cartModuleService.createCarts({
currency_code: "usd",
shipping_address: {
address_1: "1512 Barataria Blvd",
@@ -76,7 +76,6 @@ For example:
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
import { ICartModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { res } from "express"
export async function GET(
req: MedusaRequest,
@@ -86,7 +85,7 @@ For example:
req.scope.resolve(ModuleRegistrationName.CART)
res.json({
carts: await cartModuleService.list(),
carts: await cartModuleService.listCarts(),
})
}
```
@@ -105,7 +104,7 @@ For example:
const cartModuleService: ICartModuleService =
container.resolve(ModuleRegistrationName.CART)
const carts = await cartModuleService.list()
const carts = await cartModuleService.listCarts()
}
```
@@ -125,7 +124,7 @@ For example:
ModuleRegistrationName.CART
)
const carts = await cartModuleService.list()
const carts = await cartModuleService.listCarts()
})
```
@@ -18,13 +18,13 @@ The `LineItemAdjustment` data model represents changes on a line item, and the `
![A diagram showcasing the relations between other data models and adjustment line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534248/Medusa%20Resources/cart-adjustments_k4sttb.jpg)
The `amount` field of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion is stored in the `promotion_id` field of the adjustment line.
The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion is stored in the `promotion_id` property of the adjustment line.
---
## Discountable Option
The `LineItem` data model has an `is_discountable` field that indicates whether promotions can be applied to the line item. Its enabled by default.
The `LineItem` data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. Its enabled by default.
When disabled, a promotion cant be applied to a line item. In the context of the Promotion Module, the promotion isnt applied to the line item even if it matches its rules.
@@ -50,7 +50,7 @@ import {
} from "@medusajs/types"
// retrieve the cart
const cart = await cartModuleService.retrieve("cart_123", {
const cart = await cartModuleService.retrieveCart("cart_123", {
relations: [
"items.adjustments",
"shipping_methods.adjustments",
@@ -18,7 +18,7 @@ A tax line indicates the tax rate of a line item or a shipping method. The [Line
By default, the tax amount is calculated by taking the tax rate from the line item or shipping methods amount, and then added to the item/methods subtotal.
However, line items and shipping methods have an `is_tax_inclusive` field that, when enabled, indicates that the item or methods price already includes taxes.
However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or methods price already includes taxes.
So, instead of calculating the tax rate and adding it to the item/methods subtotal, its calculated as part of the subtotal.
@@ -38,7 +38,7 @@ When using the Cart and Tax modules together, you can use the `getTaxLines` meth
```ts
// retrieve the cart
const cart = await cartModuleService.retrieve("cart_123", {
const cart = await cartModuleService.retrieveCart("cart_123", {
relations: [
"items.tax_lines",
"shipping_methods.tax_lines",
@@ -26,7 +26,7 @@ In this guide, youll find common examples of how you can use the Currency Mod
req.scope.resolve(ModuleRegistrationName.CURRENCY)
res.json({
currencies: await currencyModuleService.list(),
currencies: await currencyModuleService.listCurrencies(),
})
}
```
@@ -42,10 +42,11 @@ In this guide, youll find common examples of how you can use the Currency Mod
} from "@medusajs/currency"
export async function GET(request: Request) {
const currencyModuleService = await initializeCurrencyModule()
const currencyModuleService =
await initializeCurrencyModule()
return NextResponse.json({
currencies: await currencyModuleService.list(),
currencies: await currencyModuleService.listCurrencies(),
})
}
```
@@ -72,7 +73,8 @@ In this guide, youll find common examples of how you can use the Currency Mod
const currencyModuleService: ICurrencyModuleService =
req.scope.resolve(ModuleRegistrationName.CURRENCY)
const currency = await currencyModuleService.retrieve("usd")
const currency = await currencyModuleService
.retrieveCurrency("usd")
res.json({
currency,
@@ -95,7 +97,8 @@ In this guide, youll find common examples of how you can use the Currency Mod
) {
const currencyModuleService = await initializeCurrencyModule()
const currency = await currencyModuleService.retrieve("usd")
const currency = await currencyModuleService
.retrieveCurrency("usd")
return NextResponse.json({ currency })
}
@@ -15,7 +15,9 @@ The Currency Module is the `@medusajs/currency` NPM package that provides curren
List and retrieve currencies stored in your application.
```ts
const currency = await currencyModuleService.retrieve("usd")
const currency = await currencyModuleService.retrieveCurrency(
"usd"
)
```
### Support Currencies in Modules
@@ -25,8 +27,10 @@ Other commerce modules use currency codes in their data models or operations. Yo
An example with the Region Module:
```ts
const region = await regionModuleService.retrieve("reg_123")
const currency = await currencyModuleService.retrieve(
const region = await regionModuleService.retrieveCurrency(
"reg_123"
)
const currency = await currencyModuleService.retrieveCurrency(
region.currency_code
)
```
@@ -55,7 +59,7 @@ For example:
req.scope.resolve(ModuleRegistrationName.CURRENCY)
res.json({
currencies: await currencyModuleService.list(),
currencies: await currencyModuleService.listCurrencies(),
})
}
```
@@ -74,7 +78,8 @@ For example:
const currencyModuleService: ICurrencyModuleService =
container.resolve(ModuleRegistrationName.CURRENCY)
const currencies = await currencyModuleService.list()
const currencies = await currencyModuleService
.listCurrencies()
}
```
@@ -94,7 +99,8 @@ For example:
ModuleRegistrationName.CURRENCY
)
const currencies = await currencyModuleService.list()
const currencies = await currencyModuleService
.listCurrencies()
})
```
@@ -6,9 +6,9 @@ export const metadata = {
In this document, youll learn how registered and unregistered accounts are distinguished in the Medusa application.
## `has_account` field
## `has_account` Property
The [Customer data model](/references/customer/models/Customer) has a `has_account` field, which is a boolean that indicates whether a customer is registerd.
The [Customer data model](/references/customer/models/Customer) has a `has_account` property, which is a boolean that indicates whether a customer is registerd.
When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`.
@@ -18,6 +18,6 @@ When this or another guest customer registers an account with the same email, a
## Email Uniqueness
The above behavior means that two `Customer` records may exist of the same email. However, the main difference is the `has_account` fields value.
The above behavior means that two `Customer` records may exist of the same email. However, the main difference is the `has_account` property's value.
So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email.
@@ -25,7 +25,7 @@ In this guide, youll find common examples of how you can use the Customer Mod
const customerModuleService: ICustomerModuleService =
request.scope.resolve(ModuleRegistrationName.CUSTOMER)
const customer = await customerModuleService.create({
const customer = await customerModuleService.createCustomers({
first_name: "Peter",
last_name: "Hayes",
email: "peter.hayes@example.com",
@@ -50,7 +50,7 @@ In this guide, youll find common examples of how you can use the Customer Mod
export async function POST(request: Request) {
const customerModuleService = await initializeCustomerModule()
const customer = await customerModuleService.create({
const customer = await customerModuleService.createCustomers({
first_name: "Peter",
last_name: "Hayes",
email: "peter.hayes@example.com",
@@ -85,7 +85,7 @@ In this guide, youll find common examples of how you can use the Customer Mod
request.scope.resolve(ModuleRegistrationName.CUSTOMER)
const customerGroup =
await customerModuleService.createCustomerGroup({
await customerModuleService.createCustomerGroups({
name: "VIP",
})
@@ -109,7 +109,7 @@ In this guide, youll find common examples of how you can use the Customer Mod
const customerModuleService = await initializeCustomerModule()
const customerGroup =
await customerModuleService.createCustomerGroup({
await customerModuleService.createCustomerGroups({
name: "VIP",
})
@@ -15,7 +15,7 @@ The Customer Module is the `@medusajs/customer` NPM package that provides custom
With the Customer Module, store and manage customers in your store.
```ts
const customer = await customerModuleService.create({
const customer = await customerModuleService.createCustomers({
first_name: "Peter",
last_name: "Hayes",
email: "peter.hayes@example.com",
@@ -28,7 +28,7 @@ You can organize customers into groups. This has a lot of benefits and supports
```ts
const customerGroup =
await customerModuleService.createCustomerGroup({
await customerModuleService.createCustomerGroups({
name: "VIP",
})
@@ -62,7 +62,7 @@ For example:
request.scope.resolve(ModuleRegistrationName.CUSTOMER)
res.json({
customers: await customerModuleService.list(),
customers: await customerModuleService.listCustomers(),
})
}
```
@@ -81,7 +81,7 @@ For example:
const customerModuleService: ICustomerModuleService =
container.resolve(ModuleRegistrationName.CUSTOMER)
const customers = await customerModuleService.list()
const customers = await customerModuleService.listCustomers()
}
```
@@ -99,7 +99,7 @@ For example:
const customerModuleService: ICustomerModuleService =
container.resolve(ModuleRegistrationName.CUSTOMER)
const customers = await customerModuleService.list()
const customers = await customerModuleService.listCustomers()
})
```
@@ -22,11 +22,11 @@ The fulfillment is also associated with a shipping option of that provider, whic
---
## data Field
## data Property
The `Fulfillment` data model has a `data` field that holds any necessary data for the third-party fulfillment provider to process the fulfillment.
The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment.
For example, the `data` field can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillments details.
For example, the `data` property can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillments details.
---
@@ -48,7 +48,7 @@ Once a shipment is created for the fulfillment, you can store its tracking numbe
## Fulfillment Status
The `Fulfillment` data model has three fields to keep track of the current status of the fulfillment:
The `Fulfillment` data model has three properties to keep track of the current status of the fulfillment:
- `packed_at`: The date the fulfillment was packed. If set, then the fulfillment has been packed.
- `shipped_at`: The date the fulfillment was shipped. If set, then the fulfillment has been shipped.
@@ -34,9 +34,9 @@ Service zones can be more restrictive, such as restricting to certain cities or
You can restrict shipping options by custom rules, such as the items weight or the customers group.
These rules are represented by the [ShippingOptionRule data model](/references/fulfillment/models/ShippingOptionRule). Its fields define the custom rule:
These rules are represented by the [ShippingOptionRule data model](/references/fulfillment/models/ShippingOptionRule). Its properties define the custom rule:
- `attribute`: The name of a field or table that the rule applies to. For example, `customer_group`.
- `attribute`: The name of a property or table that the rule applies to. For example, `customer_group`.
- `operator`: The operator used in the condition. For example:
- To allow multiple values, use the operator `in`, which validates that the provided values are in the rules values.
- To create a negation condition that considers `value` against the rule, use `nin`, which validates that the provided values arent in the rules values.
@@ -59,8 +59,8 @@ A shipping option also belongs to a shipping profile, as each shipping profile d
---
## data Field
## data Property
When fulfilling an item, you might use a third-party fulfillment provider that requires additional custom data to be passed along from the checkout or order-creation process.
The `ShippingOption` data model has a `data` field. It's an object that stores custom data relevant later when creating and processing a fulfillment.
The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment.
@@ -20,15 +20,15 @@ The `InventoryItem` data model mainly holds details related to the underlying st
An inventory level, represented by the [InventoryLevel data model](/references/inventory/models/InventoryLevel), holds the inventory and quantity details of an inventory item in a specific location.
It has three quantity-related fields:
It has three quantity-related properties:
- `stocked_quantity`: The available stock quantity of an item in the associated location.
- `reserved_quantity`: The quantity reserved from the available `stocked_quantity`. It indicates the quantity that's still not removed from stock, but considered as unavailable when checking whether an item is in stock.
- `incoming_quantity`: The incoming stock quantity of an item into the associated location. This field doesn't play into the `stocked_quantity` or when checking whether an item is in stock.
- `incoming_quantity`: The incoming stock quantity of an item into the associated location. This property doesn't play into the `stocked_quantity` or when checking whether an item is in stock.
### Associated Location
The inventory level's location is determined by the `location_id` field. Medusa links the `InventoryLevel` data model with the `StockLocation` data model from the Stock Location Module.
The inventory level's location is determined by the `location_id` property. Medusa links the `InventoryLevel` data model with the `StockLocation` data model from the Stock Location Module.
---
@@ -26,7 +26,7 @@ In this document, youll find common examples of how you can use the Inventory
request.scope.resolve(ModuleRegistrationName.INVENTORY)
const inventoryItem =
await inventoryModuleService.create({
await inventoryModuleService.createInventoryItems({
sku: request.body.sku,
title: request.body.title,
requires_shipping: request.body.requires_shipping,
@@ -52,7 +52,7 @@ In this document, youll find common examples of how you can use the Inventory
const body = await request.json()
const inventoryItem =
await inventoryModuleService.create({
await inventoryModuleService.createInventoryItems({
sku: body.sku,
title: body.title,
requires_shipping: body.requires_shipping,
@@ -85,7 +85,7 @@ In this document, youll find common examples of how you can use the Inventory
request.scope.resolve(ModuleRegistrationName.INVENTORY)
const inventoryItems =
await inventoryModuleService.list({})
await inventoryModuleService.listInventoryItems({})
res.json({ inventory_items: inventoryItems })
}
@@ -106,7 +106,7 @@ In this document, youll find common examples of how you can use the Inventory
await initializeInventoryModule({})
const inventoryItems =
await inventoryModuleService.list({})
await inventoryModuleService.listInventoryItems({})
return NextResponse.json({ inventory_items: inventoryItems })
}
@@ -135,7 +135,7 @@ In this document, youll find common examples of how you can use the Inventory
request.scope.resolve(ModuleRegistrationName.INVENTORY)
const inventoryItem =
await inventoryModuleService.retrieve(
await inventoryModuleService.retrieveInventoryItem(
request.params.id
)
@@ -167,7 +167,7 @@ In this document, youll find common examples of how you can use the Inventory
await initializeInventoryModule({})
const inventoryItem =
await inventoryModuleService.retrieve(
await inventoryModuleService.retrieveInventoryItem(
params.id
)
@@ -8,7 +8,7 @@ This document explains how the Inventory Module is used within the Medusa applic
## Product Variant Creation
When a product variant is created and its `manage_inventory` field's value is `true`, the Medusa application creates an inventory item associated with that product variant.
When a product variant is created and its `manage_inventory` property's value is `true`, the Medusa application creates an inventory item associated with that product variant.
![A diagram showcasing how the Inventory Module is used in the product variant creation form](https://res.cloudinary.com/dza7lstvk/image/upload/v1709661511/Medusa%20Resources/inventory-product-create_khz2hk.jpg)
@@ -32,7 +32,7 @@ When an order is placed, the Medusa application creates a reservation item for e
## Order Fulfillment
When an item in an order is fulfilled and the associated variant has its `manage_inventory` field set to `true`, the Medusa application:
When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application:
- Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item.
- Resets the `reserved_quantity` to `0`.
@@ -44,6 +44,6 @@ When an item in an order is fulfilled and the associated variant has its `manage
## Order Return
When an item in an order is returned and the associated variant has its `manage_inventory` field set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity.
When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity.
![A diagram showcasing how the Inventory Module is used in the order return flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712457/Medusa%20Resources/inventory-order-return_ihftyk.jpg)
@@ -18,7 +18,7 @@ Inventory items hold details of the underlying stock-kept item, as well as inven
```ts
const inventoryItem =
await inventoryModuleService.create({
await inventoryModuleService.createInventoryItems({
sku: "SHIRT",
title: "Green Medusa Shirt",
requires_shipping: true,
@@ -89,7 +89,7 @@ For example:
res.json({
inventory_items:
await inventoryModuleService.list({}),
await inventoryModuleService.listInventoryItems({}),
})
}
```
@@ -109,7 +109,7 @@ For example:
container.resolve(ModuleRegistrationName.INVENTORY)
const inventoryItems =
await inventoryModuleService.list({})
await inventoryModuleService.listInventoryItems({})
}
```
@@ -128,7 +128,7 @@ For example:
container.resolve(ModuleRegistrationName.INVENTORY)
const inventoryItems =
await inventoryModuleService.list({})
await inventoryModuleService.listInventoryItems({})
})
```
@@ -12,7 +12,7 @@ Each product variant has different inventory details. Medusa defines a link modu
![A diagram showcasing an example of how data models from the Inventory and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658720/Medusa%20Resources/inventory-product_ejnray.jpg)
A product variant, whose `manage_inventory` field is enabled, has an associated inventory item. Through that inventory's items relations in the Inventory Module, you can manage and check the variant's inventory quantity.
A product variant, whose `manage_inventory` property is enabled, has an associated inventory item. Through that inventory's items relations in the Inventory Module, you can manage and check the variant's inventory quantity.
---
@@ -22,13 +22,13 @@ The details of the purchased products are represented by the [LineItem data mode
An order has one or more shipping methods used to handle item shipment. Each shipping method is represented by the [ShippingMethod data model](/references/order/models/ShippingMethod) that holds its details.
### data Field
### data Property
When fulfilling the order, you may use a third-party fulfillment provider that requires additional custom data to be passed along from the order creation process.
The `ShippingMethod` data model has a `data` field. Its an object used to store custom data relevant later for fulfillment.
The `ShippingMethod` data model has a `data` property. Its an object used to store custom data relevant later for fulfillment.
The Medusa application passes the `data` field to the Fulfillment Module when fulfilling items.
The Medusa application passes the `data` property to the Fulfillment Module when fulfilling items.
---
@@ -35,7 +35,7 @@ Each of these actions is represented by the [OrderChangeAction data model](/refe
### Action Name
The `action` field of the `OrderChangeAction` holds the name of the action to perform. Based on the action, additional details are stored in the `details` object field of the data model.
The `action` property of the `OrderChangeAction` holds the name of the action to perform. Based on the action, additional details are stored in the `details` object property of the data model.
<Table>
<Table.Header>
@@ -162,7 +162,7 @@ The `action` field of the `OrderChangeAction` holds the name of the action to pe
Mark as received an item whose return was previously requested, but consider the items damaged.
This changes the order items `return_dismissed_quantity` field rather than its `return_received_quantity` field.
This changes the order items `return_dismissed_quantity` property rather than its `return_received_quantity` property.
</Table.Cell>
<Table.Cell>
@@ -223,7 +223,7 @@ The `action` field of the `OrderChangeAction` holds the name of the action to pe
</Table.Cell>
<Table.Cell>
Mark a quantity of an item as shipped. This modifies the shipped_quantity field of an order item.
Mark a quantity of an item as shipped. This modifies the `shipped_quantity` property of an order item.
</Table.Cell>
<Table.Cell>
@@ -243,7 +243,7 @@ The `action` field of the `OrderChangeAction` holds the name of the action to pe
</Table.Cell>
<Table.Cell>
Mark a quantity of an item as fulfilled. This modifies the fulfilled_quantity field of an order item.
Mark a quantity of an item as fulfilled. This modifies the `fulfilled_quantity` property of an order item.
</Table.Cell>
<Table.Cell>
@@ -277,7 +277,7 @@ The `action` field of the `OrderChangeAction` holds the name of the action to pe
## Order Change Confirmation
The `OrderChange` data model has a `status` field that indicates its current status. By default, its pending. At this point, the order changes actions arent applied to the order yet.
The `OrderChange` data model has a `status` property that indicates its current status. By default, its pending. At this point, the order changes actions arent applied to the order yet.
To apply these changes to the order, you confirm the order change. When the order change is confirmed:
@@ -12,17 +12,17 @@ Versioning means assigning a version number to a record, such as an order and it
---
## version Field
## version Property
The `Order` and `OrderSummary` data models have a `version` field that indicates the current order version. By default, its value is `1`.
The `Order` and `OrderSummary` data models have a `version` property that indicates the current order version. By default, its value is `1`.
The `OrderItem` data model also has a `version` field, but it indicates the version it belongs to. For example, original items in the order have version `1`. Then, when a new item is added, its version is `2`. If an existing item is modified, such as its quantity has been changed, its version is updated.
The `OrderItem` data model also has a `version` property, but it indicates the version it belongs to. For example, original items in the order have version `1`. Then, when a new item is added, its version is `2`. If an existing item is modified, such as its quantity has been changed, its version is updated.
---
## Order Change Versioning
The `OrderChange` and `OrderChangeAction` data models also have a `version` field. When an order change is created, the `version` fields value is the associated orders version incremented.
The `OrderChange` and `OrderChangeAction` data models also have a `version` property. When an order change is created, the `version` propertys value is the associated orders version incremented.
So, if the orders `version` is `1`, the order changes version is `2`.
@@ -15,7 +15,7 @@ The Order Module is the `@medusajs/order` NPM package that provides order-relate
Store and manage your orders to retriev, create, cancel, and perform other operations.
```ts
const order = await orderModuleService.create({
const order = await orderModuleService.createOrders({
currency_code: "usd",
items: [
{
@@ -38,7 +38,7 @@ const order = await orderModuleService.create({
Allow merchants to create orders on behalf of their customers as draft orders that later are transformed to regular orders.
```ts
const draftOrder = await orderModuleService.create({
const draftOrder = await orderModuleService.createOrders({
currency_code: "usd",
// other details...
status: "draft",
@@ -122,7 +122,7 @@ For example:
req.scope.resolve(ModuleRegistrationName.ORDER)
res.json({
orders: await orderModuleService.list(),
orders: await orderModuleService.listOrders(),
})
}
```
@@ -141,7 +141,7 @@ For example:
const orderModuleService: IOrderModuleService =
container.resolve(ModuleRegistrationName.ORDER)
const orders = await orderModuleService.list()
const orders = await orderModuleService.listOrders()
}
```
@@ -160,7 +160,7 @@ For example:
container.resolve(
ModuleRegistrationName.ORDER
)
const orders = await orderModuleService.list()
const orders = await orderModuleService.listOrders()
})
```
@@ -18,13 +18,13 @@ The [LineItemAdjustment data model](/references/order/models/LineItemAdjustment)
![A diagram showcasing the relation between an order, its items and shipping methods, and their adjustment lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712306017/Medusa%20Resources/order-adjustments_myflir.jpg)
The `amount` field of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion can be stored in the `promotion_id` field of the adjustment line.
The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion can be stored in the `promotion_id` property of the adjustment line.
---
## Discountable Option
The `LineItem` data model has an `is_discountable` field that indicates whether promotions can be applied to the line item. Its enabled by default.
The `LineItem` data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. Its enabled by default.
When disabled, a promotion cant be applied to a line item. In the context of the Promotion Module, the promotion isnt applied to the line item even if it matches its rules.
@@ -52,7 +52,7 @@ import {
// ...
// retrieve the order
const order = await orderModuleService.retrieve("ord_123", {
const order = await orderModuleService.retrieveOrder("ord_123", {
relations: [
"items.item.adjustments",
"shipping_methods.adjustments",
@@ -22,7 +22,7 @@ A tax line indicates the tax rate of a line item or a shipping method. The [Line
By default, the tax amount is calculated by taking the tax rate from the line item or shipping methods amount and then adding it to the item/methods subtotal.
However, line items and shipping methods have an `is_tax_inclusive` field that, when enabled, indicates that the item or methods price already includes taxes.
However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or methods price already includes taxes.
So, instead of calculating the tax rate and adding it to the item/methods subtotal, its calculated as part of the subtotal.
@@ -18,7 +18,7 @@ The transactions main purpose is to ensure a correct balance between paid and
## Checking Outstanding Amount
The orders total is stored in the `OrderSummary`'s `total` field. To check the outstanding amount of the order, its transaction amounts are summed. Then, the following conditions are checked:
The orders total is stored in the `OrderSummary`'s `total` property. To check the outstanding amount of the order, its transaction amounts are summed. Then, the following conditions are checked:
<Table>
<Table.Header>
@@ -73,7 +73,7 @@ The orders total is stored in the `OrderSummary`'s `total` field. To check th
The Order Module doesnt provide payment processing functionalities, so it doesnt store payments that can be processed. For that, use the Payment Module or custom logic.
The `Transaction` data model has two fields that determine which data model and record holds the actual payments details:
The `Transaction` data model has two properties that determine which data model and record holds the actual payments details:
- `reference`: indicates the tables name in the database. For example, `payment` if youre using the Payment Module.
- `reference_id`: indicates the ID of the record in the table. For example, `pay_123`.
@@ -16,17 +16,17 @@ A payment collection can have multiple payment sessions. For example, during che
---
## data field
## data Property
Payment providers may need additional data to process the payment later. The `PaymentSession` data model has a `data` field used to store that data.
Payment providers may need additional data to process the payment later. The `PaymentSession` data model has a `data` property used to store that data.
For example, the customer's ID in Stripe is stored in the `data` field.
For example, the customer's ID in Stripe is stored in the `data` property.
---
## Payment Session Status
The `status` field of a payment session indicates its current status. Its value can be:
The `status` property of a payment session indicates its current status. Its value can be:
- `pending`: The payment session is awaiting authorization.
- `requires_more`: The payment session requires an action before its authorized. For example, to enter a 3DS code.
@@ -14,7 +14,7 @@ A payment carries along many of the data and relations of a payment session:
- It belongs to the same payment collection.
- Its associated with the same payment provider, which handles further payment processing.
- It stores the payment sessions `data` field in its `data` field, as its still useful for the payment providers processing.
- It stores the payment sessions `data` property in its `data` property, as its still useful for the payment providers processing.
---
@@ -18,7 +18,7 @@ A [PriceSet](/references/pricing/models/PriceSet) represents a collection of pri
Each price within a price set can be applied for different conditions. These conditions are represented as rule types.
A [RuleType](/references/pricing/models/RuleType) defines custom conditions. A rule type has a unique `rule_attribute` which indicates the field this rule applies on. For example, `region_id`.
A [RuleType](/references/pricing/models/RuleType) defines custom conditions. A rule type has a unique `rule_attribute` which indicates the property this rule applies on. For example, `region_id`.
This is referenced when setting a rule of a price. For example:
@@ -46,6 +46,6 @@ const priceSet = await pricingModuleService.addPrices({
## Price List
A [PriceList](/references/pricing/models/PriceList) is a group of prices only enabled if their conditions and rules are satisfied. A price list has optional `start_date` and `end_date` fields, which indicate the date range in which a price list can be applied.
A [PriceList](/references/pricing/models/PriceList) is a group of prices only enabled if their conditions and rules are satisfied. A price list has optional `start_date` and `end_date` properties, which indicate the date range in which a price list can be applied.
Its associated prices are represented by the `Price` data model.
@@ -27,7 +27,7 @@ In this document, youll find common examples of how you can use the Pricing M
// A rule type with \`rule_field=region_id\` should
// already be present in the database
const priceSet = await pricingModuleService.create([
const priceSet = await pricingModuleService.createPriceSets([
{
rules: [{ rule_field: "region_id" }],
prices: [
@@ -62,7 +62,7 @@ In this document, youll find common examples of how you can use the Pricing M
// A rule type with \`rule_field=region_i\` should
// already be present in the database
const priceSet = await pricingModuleService.create([
const priceSet = await pricingModuleService.createPriceSets([
{
rules: [{ rule_field: "region_id" }],
prices: [
@@ -103,7 +103,7 @@ In this document, youll find common examples of how you can use the Pricing M
const pricingModuleService: IPricingModuleService =
request.scope.resolve(ModuleRegistrationName.PRICING)
const priceSets = await pricingModuleService.list()
const priceSets = await pricingModuleService.listPriceSets()
res.json({ price_sets: priceSets })
}
@@ -122,7 +122,7 @@ In this document, youll find common examples of how you can use the Pricing M
export async function GET(request: Request) {
const pricingModuleService = await initializePricingModule()
const priceSets = await pricingModuleService.list()
const priceSets = await pricingModuleService.listPriceSets()
return NextResponse.json({ price_sets: priceSets })
}
@@ -150,7 +150,7 @@ In this document, youll find common examples of how you can use the Pricing M
const pricingModuleService: IPricingModuleService =
request.scope.resolve(ModuleRegistrationName.PRICING)
const priceSet = await pricingModuleService.retrieve(
const priceSet = await pricingModuleService.retrievePriceSet(
request.params.id
)
@@ -180,7 +180,7 @@ In this document, youll find common examples of how you can use the Pricing M
) {
const pricingModuleService = await initializePricingModule()
const priceSet = await pricingModuleService.retrieve(
const priceSet = await pricingModuleService.retrievePriceSet(
params.id
)
@@ -17,7 +17,7 @@ With the Pricing Module, store the prices of a resource and manage them through
Prices are grouped in a price set, allowing you to add more than one price for a resource based on different conditions, such as currency code.
```ts
const priceSet = await pricingModuleService.create({
const priceSet = await pricingModuleService.createPriceSets({
rules: [],
prices: [
{
@@ -124,7 +124,7 @@ For example:
request.scope.resolve(ModuleRegistrationName.PRICING)
res.json({
price_sets: await pricingModuleService.list(),
price_sets: await pricingModuleService.listPriceSets(),
})
}
```
@@ -143,7 +143,7 @@ For example:
const pricingModuleService: IPricingModuleService =
container.resolve(ModuleRegistrationName.PRICING)
const priceSets = await pricingModuleService.list()
const priceSets = await pricingModuleService.listPriceSets()
}
```
@@ -161,7 +161,7 @@ For example:
const pricingModuleService: IPricingModuleService =
container.resolve(ModuleRegistrationName.PRICING)
const priceSets = await pricingModuleService.list()
const priceSets = await pricingModuleService.listPriceSets()
})
```
@@ -178,7 +178,7 @@ const ruleTypes = await pricingModuleService.createRuleTypes([
},
])
const priceSet = await pricingModuleService.create({
const priceSet = await pricingModuleService.createPriceSets({
rules: [
{ rule_attribute: "region_id" },
{ rule_attribute: "city" },
@@ -422,7 +422,7 @@ const priceSet = await pricingModuleService.create({
<TabsContent value="code">
```ts
const priceList = pricingModuleService.createPriceList({
const priceList = pricingModuleService.createPriceLists({
name: "Test Price List",
starts_at: Date.parse("01/10/2023"),
ends_at: Date.parse("31/10/2023"),
@@ -8,7 +8,7 @@ In this document, you'll learn about price rules for price sets and price lists.
## Price Rule
Each rule of a price within a price set is represented by the [PriceRule data model](/references/pricing/models/PriceRule), which holds the value of a rule type. The `Price` data model has a `rules_count` field, which indicates how many rules, represented by `PriceRule`, are applied to the price.
Each rule of a price within a price set is represented by the [PriceRule data model](/references/pricing/models/PriceRule), which holds the value of a rule type. The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price.
![A diagram showcasing the relation between the PriceRule, PriceSet, Price, and RuleType.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648772/Medusa%20Resources/price-rule-1_vy8bn9.jpg)
@@ -40,7 +40,7 @@ For example, to use the `zip_code` rule type on a price in a price set, the rule
## Price List Rules
Rules that can be applied to a price list are represented by the [PriceListRule data model](/references/pricing/models/PriceListRule). The `rules_count` field of a `PriceList` indicates how many rules are applied to it.
Rules that can be applied to a price list are represented by the [PriceListRule data model](/references/pricing/models/PriceListRule). The `rules_count` property of a `PriceList` indicates how many rules are applied to it.
Each rule of a price list can have more than one value, representing its values by the [PriceListRuleValue data model](/references/pricing/models/PriceListRuleValue).
@@ -25,7 +25,7 @@ In this guide, youll find common examples of how you can use the Product Modu
const productModuleService: IProductModuleService =
request.scope.resolve(ModuleRegistrationName.PRODUCT)
const products = await productModuleService.create([
const products = await productModuleService.createProducts([
{
title: "Medusa Shirt",
options: [
@@ -63,7 +63,7 @@ In this guide, youll find common examples of how you can use the Product Modu
export async function POST(request: Request) {
const productModuleService = await initializeProductModule()
const products = await productModuleService.create([
const products = await productModuleService.createProducts([
{
title: "Medusa Shirt",
options: [
@@ -110,7 +110,7 @@ In this guide, youll find common examples of how you can use the Product Modu
const productModuleService: IProductModuleService =
request.scope.resolve(ModuleRegistrationName.PRODUCT)
const products = await productModuleService.list()
const products = await productModuleService.listProducts()
res.json({ products })
}
@@ -129,7 +129,7 @@ In this guide, youll find common examples of how you can use the Product Modu
export async function GET(request: Request) {
const productModuleService = await initializeProductModule()
const products = await productModuleService.list()
const products = await productModuleService.listProducts()
return NextResponse.json({ products })
}
@@ -157,7 +157,7 @@ In this guide, youll find common examples of how you can use the Product Modu
const productModuleService: IProductModuleService =
request.scope.resolve(ModuleRegistrationName.PRODUCT)
const product = await productModuleService.retrieve(
const product = await productModuleService.retrieveProduct(
request.params.id
)
@@ -182,7 +182,7 @@ In this guide, youll find common examples of how you can use the Product Modu
const { id } = params
const productModuleService = await initializeProductModule()
const product = await productModuleService.retrieve(
const product = await productModuleService.retrieveProduct(
id
)
@@ -212,7 +212,7 @@ In this guide, youll find common examples of how you can use the Product Modu
const productModuleService: IProductModuleService =
request.scope.resolve(ModuleRegistrationName.PRODUCT)
const data = await productModuleService.list({
const data = await productModuleService.listProducts({
handle: "shirt",
})
@@ -233,7 +233,7 @@ In this guide, youll find common examples of how you can use the Product Modu
export async function GET(request: Request) {
const productModuleService = await initializeProductModule()
const data = await productModuleService.list({
const data = await productModuleService.listProducts({
handle: "shirt",
})
@@ -263,7 +263,8 @@ In this guide, youll find common examples of how you can use the Product Modu
const productModuleService: IProductModuleService =
request.scope.resolve(ModuleRegistrationName.PRODUCT)
const categories = await productModuleService.listCategories()
const categories = await productModuleService
.listProductCategories()
res.json({ categories })
}
@@ -282,7 +283,8 @@ In this guide, youll find common examples of how you can use the Product Modu
export async function GET(request: Request) {
const productModuleService = await initializeProductModule()
const categories = await productModuleService.listCategories()
const categories = await productModuleService
.listProductCategories()
return NextResponse.json({ categories })
}
@@ -310,9 +312,10 @@ In this guide, youll find common examples of how you can use the Product Modu
const productModuleService: IProductModuleService =
request.scope.resolve(ModuleRegistrationName.PRODUCT)
const data = await productModuleService.listCategories({
handle: request.params.handle,
})
const data = await productModuleService
.listProductCategories({
handle: request.params.handle,
})
res.json({ category: data[0] })
}
@@ -335,9 +338,10 @@ In this guide, youll find common examples of how you can use the Product Modu
const { handle } = params
const productModuleService = await initializeProductModule()
const data = await productModuleService.listCategories({
handle,
})
const data = await productModuleService
.listProductCategories({
handle,
})
return NextResponse.json({ category: data[0] })
}
@@ -15,7 +15,7 @@ The Product Module is the `@medusajs/product` NPM package that provides product-
Store and manage products. Products have custom options, such as color or size, and each variant in the product sets the value for these options.
```ts
const products = await productService.create([
const products = await productService.createProducts([
{
title: "Medusa Shirt",
options: [
@@ -42,11 +42,11 @@ const products = await productService.create([
The Product Module provides different data models used to organize products, including categories, collections, tags, and more.
```ts
const category = await productService.createCategory({
const category = await productService.createProductCategories({
name: "Shirts",
})
const products = await productService.update([
const products = await productService.updateProducts([
{
id: product.id,
categories: [
@@ -81,7 +81,9 @@ For example:
const productModuleService: IProductModuleService =
request.scope.resolve(ModuleRegistrationName.PRODUCT)
res.json({ products: await productModuleService.list() })
res.json({
products: await productModuleService.listProducts(),
})
}
```
@@ -99,7 +101,7 @@ For example:
const productModuleService: IProductModuleService =
container.resolve(ModuleRegistrationName.PRODUCT)
const products = await productModuleService.list()
const products = await productModuleService.listProducts()
}
```
@@ -117,7 +119,7 @@ For example:
const productModuleService: IProductModuleService =
container.resolve(ModuleRegistrationName.PRODUCT)
const products = await productModuleService.list()
const products = await productModuleService.listProducts()
})
```
@@ -48,6 +48,6 @@ Each product variant has different inventory details. Medusa defines a link modu
![A diagram showcasing an example of how data models from the Product and Inventory modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg)
When the `manage_inventory` field of a product variant is enabled, you can manage the variant's inventory in different locations through this relation.
When the `manage_inventory` property of a product variant is enabled, you can manage the variant's inventory in different locations through this relation.
Learn more about the `InventoryItem` data model in the [Inventory Concepts](../../inventory/concepts/page.mdx#inventoryitem)
@@ -10,7 +10,7 @@ In this document, youll learn about promotion actions and how theyre compu
The Promotion Module's main service has a [computeActions method](/references/promotion/computeActions) that returns an array of actions to perform on a cart when one or more promotions are applied.
Actions inform you what adjustment must be made to a cart item or shipping method. Each action is an object having the `action` field indicating the type of action.
Actions inform you what adjustment must be made to a cart item or shipping method. Each action is an object having the `action` property indicating the type of action.
---
@@ -17,7 +17,7 @@ The [ApplicationMethod data model](/references/promotion/models/ApplicationMetho
<Table.Row>
<Table.HeaderCell>
Field
Property
</Table.HeaderCell>
<Table.HeaderCell>
@@ -71,7 +71,7 @@ The [ApplicationMethod data model](/references/promotion/models/ApplicationMetho
When the promotion is applied to a cart item or a shipping method, you can restrict which items/shipping methods the promotion is applied to.
The `ApplicationMethod` data model has a collection of `PromotionRule` records to restrict which items or shipping methods the promotion applies to. The `target_rules` field represents this relation.
The `ApplicationMethod` data model has a collection of `PromotionRule` records to restrict which items or shipping methods the promotion applies to. The `target_rules` property represents this relation.
![A diagram showcasing the target_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898273/Medusa%20Resources/application-method-target-rules_hqaymz.jpg)
@@ -83,7 +83,7 @@ In this example, the promotion is only applied on products in the cart having th
When the promotions type is `buyget`, you must specify the “buy X” condition. For example, a cart must have two shirts before the promotion can be applied.
The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` field represents this relation.
The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` property represents this relation.
![A diagram showcasing the buy_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898453/Medusa%20Resources/application-method-buy-rules_djjuhw.jpg)
@@ -78,19 +78,19 @@ A promotion can be restricted by a set of rules, each rule is represented by the
![A diagram showcasing the relation between Promotion and PromotionRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1709833196/Medusa%20Resources/promotion-promotion-rule_msbx0w.jpg)
A `PromotionRule`'s `attribute` field indicates the field's name to which this rule is applied. For example, `customer_group_id`. Its value is stored in the `PromotionRuleValue` data model. So, a rule can have multiple values.
A `PromotionRule`'s `attribute` property indicates the property's name to which this rule is applied. For example, `customer_group_id`. Its value is stored in the `PromotionRuleValue` data model. So, a rule can have multiple values.
When testing whether a promotion can be applied to a cart, the rule's `attribute` field and its values are tested on the cart itself. For example, the cart's customer must be part of the customer group(s) indicated in the promotion rule's value.
When testing whether a promotion can be applied to a cart, the rule's `attribute` property and its values are tested on the cart itself. For example, the cart's customer must be part of the customer group(s) indicated in the promotion rule's value.
---
## Flexible Rules
The `PromotionRule`'s `operator` field adds more flexibility to the rules condition rather than simple equality (`eq`).
The `PromotionRule`'s `operator` property adds more flexibility to the rules condition rather than simple equality (`eq`).
For example, to restrict the promotion to only `VIP` and `B2B` customer groups:
- Add a `PromotionRule` with its `attribute` field set to `customer_group_id` and `operator` field to `in`.
- Add a `PromotionRule` with its `attribute` property set to `customer_group_id` and `operator` property to `in`.
- Add two `PromotionRuleValue` associated with the rule: one with the value `VIP` and the other `B2B`.
![A diagram showcasing the relation between PromotionRule and PromotionRuleValue when a rule has multiple values](https://res.cloudinary.com/dza7lstvk/image/upload/v1709897383/Medusa%20Resources/promotion-promotion-rule-multiple_hctpmt.jpg)
@@ -25,7 +25,7 @@ In this document, youll find common examples of how you can use the Promotion
const promotionModuleService: IPromotionModuleService =
request.scope.resolve(ModuleRegistrationName.PROMOTION)
const promotion = await promotionModuleService.create({
const promotion = await promotionModuleService.createPromotions({
code: "10%OFF",
type: "standard",
application_method: {
@@ -55,7 +55,7 @@ In this document, youll find common examples of how you can use the Promotion
await initializePromotionModule()
const body = await request.json()
const promotion = await promotionModuleService.create({
const promotion = await promotionModuleService.createPromotions({
code: "10%OFF",
type: "standard",
application_method: {
@@ -155,7 +155,7 @@ In this document, youll find common examples of how you can use the Promotion
const promotionModuleService: IPromotionModuleService =
request.scope.resolve(ModuleRegistrationName.PROMOTION)
const promotion = await promotionModuleService.create({
const promotion = await promotionModuleService.createPromotions({
code: "10%OFF",
type: "standard",
application_method: {
@@ -191,7 +191,7 @@ In this document, youll find common examples of how you can use the Promotion
const promotionModuleService =
await initializePromotionModule()
const promotion = await promotionModuleService.create({
const promotion = await promotionModuleService.createPromotions({
code: "10%OFF",
type: "standard",
application_method: {
@@ -236,7 +236,7 @@ In this document, youll find common examples of how you can use the Promotion
request.scope.resolve(ModuleRegistrationName.PROMOTION)
res.json({
promotions: await promotionModuleService.list(),
promotions: await promotionModuleService.listPromotions(),
})
}
```
@@ -256,7 +256,7 @@ In this document, youll find common examples of how you can use the Promotion
await initializePromotionModule()
return NextResponse.json({
promotions: await promotionModuleService.list(),
promotions: await promotionModuleService.listPromotions(),
})
}
```
@@ -17,7 +17,7 @@ A promotion discounts an amount or percentage of a cart's items, shipping method
The Promotion Module allows you to store and manage promotions.
```ts
const promotion = await promotionModuleService.create({
const promotion = await promotionModuleService.createPromotions({
code: "10%OFF",
type: "standard",
application_method: {
@@ -34,7 +34,7 @@ const promotion = await promotionModuleService.create({
A promotion has rules that restricts when it's applied. For example, you can create a promotion that's only applied to VIP customers.
```ts
const promotion = await promotionModuleService.create({
const promotion = await promotionModuleService.createPromotions({
code: "10%OFF",
type: "standard",
application_method: {
@@ -94,7 +94,7 @@ For example:
request.scope.resolve(ModuleRegistrationName.PROMOTION)
res.json({
promotions: await promotionModuleService.list(),
promotions: await promotionModuleService.listPromotions(),
})
}
```
@@ -113,7 +113,7 @@ For example:
const promotionModuleService: IPromotionModuleService =
container.resolve(ModuleRegistrationName.PROMOTION)
const promotions = await promotionModuleService.list()
const promotions = await promotionModuleService.listPromotions()
}
```
@@ -131,7 +131,7 @@ For example:
const promotionModuleService: IPromotionModuleService =
container.resolve(ModuleRegistrationName.PROMOTION)
const promotions = await promotionModuleService.list()
const promotions = await promotionModuleService.listPromotions()
})
```
@@ -25,7 +25,7 @@ In this guide, youll find common examples of how you can use the Region Modul
const regionModuleService: IRegionModuleService =
req.scope.resolve(ModuleRegistrationName.REGION)
const region = await regionModuleService.create({
const region = await regionModuleService.createRegions({
name: "Europe",
currency_code: "eur",
})
@@ -49,7 +49,7 @@ In this guide, youll find common examples of how you can use the Region Modul
export async function POST(request: Request) {
const regionModuleService = await initializeRegionModule()
const region = await regionModuleService.create({
const region = await regionModuleService.createRegions({
name: "Europe",
currency_code: "eur",
})
@@ -83,7 +83,7 @@ In this guide, youll find common examples of how you can use the Region Modul
req.scope.resolve(ModuleRegistrationName.REGION)
res.json({
regions: await regionModuleService.list(),
regions: await regionModuleService.listRegions(),
})
}
```
@@ -102,7 +102,7 @@ In this guide, youll find common examples of how you can use the Region Modul
const regionModuleService = await initializeRegionModule()
return NextResponse.json({
regions: await regionModuleService.list(),
regions: await regionModuleService.listRegions(),
})
}
```
@@ -129,7 +129,8 @@ In this guide, youll find common examples of how you can use the Region Modul
const regionModuleService: IRegionModuleService =
req.scope.resolve(ModuleRegistrationName.REGION)
const region = await regionModuleService.retrieve("reg_123")
const region = await regionModuleService
.retrieveRegion("reg_123")
res.json({ region })
}
@@ -150,7 +151,8 @@ In this guide, youll find common examples of how you can use the Region Modul
) {
const regionModuleService = await initializeRegionModule()
const region = await regionModuleService.retrieve("reg_123")
const region = await regionModuleService
.retrieveRegion("reg_123")
return NextResponse.json({ region })
}
@@ -178,9 +180,10 @@ In this guide, youll find common examples of how you can use the Region Modul
const regionModuleService: IRegionModuleService =
req.scope.resolve(ModuleRegistrationName.REGION)
const region = await regionModuleService.update("reg_123", {
automatic_taxes: false,
})
const region = await regionModuleService
.updateRegions("reg_123", {
automatic_taxes: false,
})
res.json({ region })
}
@@ -201,9 +204,10 @@ In this guide, youll find common examples of how you can use the Region Modul
) {
const regionModuleService = await initializeRegionModule()
const region = await regionModuleService.update("reg_123", {
automatic_taxes: false,
})
const region = await regionModuleService
.updateRegions("reg_123", {
automatic_taxes: false,
})
return NextResponse.json({ region })
}
@@ -231,7 +235,7 @@ In this guide, youll find common examples of how you can use the Region Modul
const regionModuleService: IRegionModuleService =
req.scope.resolve(ModuleRegistrationName.REGION)
await regionModuleService.delete("reg_123")
await regionModuleService.deleteRegions("reg_123")
res.status(200)
}
@@ -252,7 +256,7 @@ In this guide, youll find common examples of how you can use the Region Modul
) {
const regionModuleService = await initializeRegionModule()
await regionModuleService.delete("reg_123")
await regionModuleService.deleteRegions("reg_123")
}
```
@@ -21,7 +21,7 @@ A region represents the area you sell products in. Each region can cover multipl
You can manage your regions to create, update, retrieve, or delete them.
```ts
const region = await regionModuleService.create({
const region = await regionModuleService.createRegions({
name: "Europe",
currency_code: "eur",
})
@@ -32,7 +32,7 @@ const region = await regionModuleService.create({
As each region has a currency, you can support multiple currencies in your store by creating multiple regions.
```ts
const regions = await regionModuleService.create([
const regions = await regionModuleService.createRegions([
{
name: "Europe",
currency_code: "eur",
@@ -49,7 +49,7 @@ const regions = await regionModuleService.create([
Each region has its own settings, such as what countries belong to a region or its tax settings. Each region has different tax rates, payment providers, and more provided by other commerce modules.
```ts
const regions = await regionModuleService.create([
const regions = await regionModuleService.createRegions([
{
name: "Europe",
currency_code: "eur",
@@ -89,7 +89,7 @@ For example:
req.scope.resolve(ModuleRegistrationName.REGION)
res.json({
regions: await regionModuleService.list(),
regions: await regionModuleService.listRegions(),
})
}
```
@@ -108,7 +108,7 @@ For example:
const regionModuleService: IRegionModuleService =
container.resolve(ModuleRegistrationName.REGION)
const regions = await regionModuleService.list()
const regions = await regionModuleService.listRegions()
}
```
@@ -128,7 +128,7 @@ For example:
ModuleRegistrationName.REGION
)
const regions = await regionModuleService.list()
const regions = await regionModuleService.listRegions()
})
```
@@ -25,9 +25,10 @@ In this guide, youll find common examples of how you can use the Sales Channe
const salesChannelModuleService: ISalesChannelModuleService =
request.scope.resolve(ModuleRegistrationName.SALES_CHANNEL)
const salesChannel = await salesChannelModuleService.create({
name: "B2B",
})
const salesChannel = await salesChannelModuleService
.createSalesChannels({
name: "B2B",
})
res.json({
sales_channel: salesChannel,
@@ -49,9 +50,10 @@ In this guide, youll find common examples of how you can use the Sales Channe
const salesChannelModuleService =
await initializeSalesChannelModule()
const salesChannel = await salesChannelModuleService.create({
name: "B2B",
})
const salesChannel = await salesChannelModuleService
.createSalesChannels({
name: "B2B",
})
return NextResponse.json({ sales_channel: salesChannel })
}
@@ -80,7 +82,8 @@ In this guide, youll find common examples of how you can use the Sales Channe
request.scope.resolve(ModuleRegistrationName.SALES_CHANNEL)
res.json({
sales_channels: salesChannelModuleService.list(),
sales_channels: salesChannelModuleService
.listSalesChannels(),
})
}
```
@@ -99,7 +102,8 @@ In this guide, youll find common examples of how you can use the Sales Channe
const salesChannelModuleService =
await initializeSalesChannelModule()
const salesChannels = await salesChannelModuleService.list()
const salesChannels = await salesChannelModuleService
.listSalesChannels()
return NextResponse.json({ sales_channels: salesChannels })
}
@@ -127,9 +131,10 @@ In this guide, youll find common examples of how you can use the Sales Channe
const salesChannelModuleService: ISalesChannelModuleService =
request.scope.resolve(ModuleRegistrationName.SALES_CHANNEL)
const salesChannel = await salesChannelModuleService.retrieve(
"sc_123"
)
const salesChannel = await salesChannelModuleService
.retrieveSalesChannel(
"sc_123"
)
res.json({
sales_channel: salesChannel,
@@ -153,9 +158,10 @@ In this guide, youll find common examples of how you can use the Sales Channe
const salesChannelModuleService =
await initializeSalesChannelModule()
const salesChannel = await salesChannelModuleService.retrieve(
"sc_123"
)
const salesChannel = await salesChannelModuleService
.retrieveSalesChannel(
"sc_123"
)
return NextResponse.json({ sales_channel: salesChannel })
}
@@ -183,10 +189,11 @@ In this guide, youll find common examples of how you can use the Sales Channe
const salesChannelModuleService: ISalesChannelModuleService =
request.scope.resolve(ModuleRegistrationName.SALES_CHANNEL)
const salesChannel = await salesChannelModuleService.update({
id: "sc_123",
description: "Sales channel for B2B customers",
})
const salesChannel = await salesChannelModuleService
.updateSalesChannels({
id: "sc_123",
description: "Sales channel for B2B customers",
})
res.json({
sales_channel: salesChannel,
@@ -210,10 +217,11 @@ In this guide, youll find common examples of how you can use the Sales Channe
const salesChannelModuleService =
await initializeSalesChannelModule()
const salesChannel = await salesChannelModuleService.update({
id: "sc_123",
description: "Sales channel for B2B customers",
})
const salesChannel = await salesChannelModuleService
.updateSalesChannels({
id: "sc_123",
description: "Sales channel for B2B customers",
})
return NextResponse.json({ sales_channel: salesChannel })
}
@@ -241,7 +249,7 @@ In this guide, youll find common examples of how you can use the Sales Channe
const salesChannelModuleService: ISalesChannelModuleService =
request.scope.resolve(ModuleRegistrationName.SALES_CHANNEL)
await salesChannelModuleService.delete("sc_123")
await salesChannelModuleService.deleteSalesChannels("sc_123")
res.status(200)
}
@@ -263,7 +271,7 @@ In this guide, youll find common examples of how you can use the Sales Channe
const salesChannelModuleService =
await initializeSalesChannelModule()
await salesChannelModuleService.delete("sc_123")
await salesChannelModuleService.deleteSalesChannels("sc_123")
}
```

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