docs: add guide on how to seed data (#9449)
Add a guide on how to seed data using a custom CLI script, with an example to seed products Closes DOCS-953
This commit is contained in:
@@ -0,0 +1,200 @@
|
|||||||
|
export const metadata = {
|
||||||
|
title: `${pageNumber} Seed Data with Custom CLI Script`,
|
||||||
|
}
|
||||||
|
|
||||||
|
# {metadata.title}
|
||||||
|
|
||||||
|
In this chapter, you'll learn how to seed data using a custom CLI script.
|
||||||
|
|
||||||
|
## How to Seed Data
|
||||||
|
|
||||||
|
To seed dummy data for development or demo purposes, use a custom CLI script.
|
||||||
|
|
||||||
|
In the CLI script, use your custom workflows or Medusa's existing workflows, which you can browse in [this reference](!resources!/medusa-workflows-reference), to seed data.
|
||||||
|
|
||||||
|
### Example: Seed Dummy Products
|
||||||
|
|
||||||
|
In this section, you'll follow an example of creating a custom CLI script that seeds fifty dummy products.
|
||||||
|
|
||||||
|
First, install the [Faker](https://fakerjs.dev/) library to generate random data in your script:
|
||||||
|
|
||||||
|
```bash npm2yarn
|
||||||
|
npm install --save-dev @faker-js/faker
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, create the file `src/scripts/demo-products.ts` with the following content:
|
||||||
|
|
||||||
|
export const highlights = [
|
||||||
|
["16", "salesChannelModuleService", "Resolve the Sales Chanel Module's main service"],
|
||||||
|
["19", "logger", "Resolve the logger to log messages in the terminal."],
|
||||||
|
["22", "query", "Resolve Query to retrieve data later."],
|
||||||
|
["26", "defaultSalesChannel", "Retrieve the default sales channel to associate products with."],
|
||||||
|
["31", "sizeOptions", "Declare the size options to be used in the products' variants."],
|
||||||
|
["32", "colorOptions", "Declare the color options to be used in the products' variants."],
|
||||||
|
["33", "currency_code", "Declare the currency code to use in products' prices."],
|
||||||
|
["34", "productsNum", "The number of products to seed."]
|
||||||
|
]
|
||||||
|
|
||||||
|
```ts title="src/scripts/demo-products.ts" highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports"
|
||||||
|
import { ExecArgs } from "@medusajs/framework/types"
|
||||||
|
import { faker } from "@faker-js/faker"
|
||||||
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
Modules,
|
||||||
|
ProductStatus,
|
||||||
|
} from "@medusajs/framework/utils"
|
||||||
|
import {
|
||||||
|
createInventoryLevelsWorkflow,
|
||||||
|
createProductsWorkflow,
|
||||||
|
} from "@medusajs/medusa/core-flows"
|
||||||
|
|
||||||
|
export default async function seedDummyProducts({
|
||||||
|
container,
|
||||||
|
}: ExecArgs) {
|
||||||
|
const salesChannelModuleService = container.resolve(
|
||||||
|
Modules.SALES_CHANNEL
|
||||||
|
)
|
||||||
|
const logger = container.resolve(
|
||||||
|
ContainerRegistrationKeys.LOGGER
|
||||||
|
)
|
||||||
|
const query = container.resolve(
|
||||||
|
ContainerRegistrationKeys.QUERY
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultSalesChannel = await salesChannelModuleService
|
||||||
|
.listSalesChannels({
|
||||||
|
name: "Default Sales Channel",
|
||||||
|
})
|
||||||
|
|
||||||
|
const sizeOptions = ["S", "M", "L", "XL"]
|
||||||
|
const colorOptions = ["Black", "White"]
|
||||||
|
const currency_code = "eur"
|
||||||
|
const productsNum = 50
|
||||||
|
|
||||||
|
// TODO seed products
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
So far, in the script, you:
|
||||||
|
|
||||||
|
- Resolve the Sales Channel Module's main service to retrieve the application's default sales channel. This is the sales channel the dummy products will be available in.
|
||||||
|
- Resolve the Logger to log messages in the terminal, and Query to later retrieve data useful for the seeded products.
|
||||||
|
- Initialize some default data to use when seeding the products next.
|
||||||
|
|
||||||
|
Next, replace the `TODO` with the following:
|
||||||
|
|
||||||
|
```ts title="src/scripts/demo-products.ts"
|
||||||
|
const productsData = new Array(productsNum).fill(0).map((_, index) => {
|
||||||
|
const title = faker.commerce.product() + "_" + index
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
is_giftcard: true,
|
||||||
|
description: faker.commerce.productDescription(),
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
title: "Size",
|
||||||
|
values: sizeOptions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Color",
|
||||||
|
values: colorOptions,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: faker.image.urlPlaceholder({
|
||||||
|
text: title,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: faker.image.urlPlaceholder({
|
||||||
|
text: title,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variants: new Array(10).fill(0).map((_, variantIndex) => ({
|
||||||
|
title: `${title} ${variantIndex}`,
|
||||||
|
sku: `variant-${variantIndex}${index}`,
|
||||||
|
prices: new Array(10).fill(0).map((_, priceIndex) => ({
|
||||||
|
currency_code,
|
||||||
|
amount: 10 * priceIndex,
|
||||||
|
})),
|
||||||
|
options: {
|
||||||
|
Size: sizeOptions[Math.floor(Math.random() * 3)],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
sales_channels: [
|
||||||
|
{
|
||||||
|
id: defaultSalesChannel[0].id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO seed products
|
||||||
|
```
|
||||||
|
|
||||||
|
You generate fifty products using the sales channel and variables you initialized, and using Faker for random data, such as the product's title or images.
|
||||||
|
|
||||||
|
Then, replace the new `TODO` with the following:
|
||||||
|
|
||||||
|
```ts title="src/scripts/demo-products.ts"
|
||||||
|
const { result: products } = await createProductsWorkflow(container).run({
|
||||||
|
input: {
|
||||||
|
products: productsData,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(`Seeded ${products.length} products.`)
|
||||||
|
|
||||||
|
// TODO add inventory levels
|
||||||
|
```
|
||||||
|
|
||||||
|
You create the generated products using the `createProductsWorkflow` imported previously from `@medusajs/medusa/core-flows`. It accepts the product data as input, and returns the created products.
|
||||||
|
|
||||||
|
Only thing left is to create inventory levels for the products. So, replace the last `TODO` with the following:
|
||||||
|
|
||||||
|
```ts title="src/scripts/demo-products.ts"
|
||||||
|
logger.info("Seeding inventory levels.")
|
||||||
|
|
||||||
|
const { data: stockLocations } = await query.graph({
|
||||||
|
entity: "stock_location",
|
||||||
|
fields: ["id"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: inventoryItems } = await query.graph({
|
||||||
|
entity: "inventory_item",
|
||||||
|
fields: ["id"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const inventoryLevels = inventoryItems.map((inventoryItem) => ({
|
||||||
|
location_id: stockLocations[0].id,
|
||||||
|
stocked_quantity: 1000000,
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
}))
|
||||||
|
|
||||||
|
await createInventoryLevelsWorkflow(container).run({
|
||||||
|
input: {
|
||||||
|
inventory_levels: inventoryLevels,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info("Finished seeding inventory levels data.")
|
||||||
|
```
|
||||||
|
|
||||||
|
You use Query to retrieve the stock location, to use the first location in the application, and the inventory items.
|
||||||
|
|
||||||
|
Then, you generate inventory levels for each inventory item, associating it with the first stock location.
|
||||||
|
|
||||||
|
Finally, you use the `createInventoryLevelsWorkflow` imported from `@medusajs/medusa/core-flows` to create the inventory levels.
|
||||||
|
|
||||||
|
### Test Script
|
||||||
|
|
||||||
|
To test out the script, run the following command in your project's directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx medusa exec ./src/scripts/demo-products.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
This seeds the products to your database. If you run your Medusa application and view the products in the dashboard, you'll find fifty new products.
|
||||||
@@ -31,9 +31,9 @@ export default defineLink(
|
|||||||
extraColumns: {
|
extraColumns: {
|
||||||
metadata: {
|
metadata: {
|
||||||
type: "json",
|
type: "json",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
@@ -94,16 +94,16 @@ Learn more about the remote link, how to resolve it, and its methods in [this ch
|
|||||||
```ts
|
```ts
|
||||||
await remoteLink.create({
|
await remoteLink.create({
|
||||||
[Modules.PRODUCT]: {
|
[Modules.PRODUCT]: {
|
||||||
product_id: "123"
|
product_id: "123",
|
||||||
},
|
},
|
||||||
HELLO_MODULE: {
|
HELLO_MODULE: {
|
||||||
my_custom_id: "321"
|
my_custom_id: "321",
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
metadata: {
|
metadata: {
|
||||||
test: true
|
test: true,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -138,8 +138,8 @@ const { data } = await query.graph({
|
|||||||
entity: productHelloLink.entryPoint,
|
entity: productHelloLink.entryPoint,
|
||||||
fields: ["metadata", "product.*", "my_custom.*"],
|
fields: ["metadata", "product.*", "my_custom.*"],
|
||||||
filters: {
|
filters: {
|
||||||
product_id: "prod_123"
|
product_id: "prod_123",
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -160,15 +160,15 @@ For example:
|
|||||||
```ts
|
```ts
|
||||||
await remoteLink.create({
|
await remoteLink.create({
|
||||||
[Modules.PRODUCT]: {
|
[Modules.PRODUCT]: {
|
||||||
product_id: "123"
|
product_id: "123",
|
||||||
},
|
},
|
||||||
HELLO_MODULE: {
|
HELLO_MODULE: {
|
||||||
my_custom_id: "321"
|
my_custom_id: "321",
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
metadata: {
|
metadata: {
|
||||||
test: false
|
test: false,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const methodsHighlight = [
|
|||||||
// other imports...
|
// other imports...
|
||||||
import {
|
import {
|
||||||
InjectManager,
|
InjectManager,
|
||||||
MedusaContext
|
MedusaContext,
|
||||||
} from "@medusajs/framework/utils"
|
} from "@medusajs/framework/utils"
|
||||||
|
|
||||||
class HelloModuleService {
|
class HelloModuleService {
|
||||||
@@ -99,7 +99,7 @@ export const opHighlights = [
|
|||||||
import {
|
import {
|
||||||
InjectManager,
|
InjectManager,
|
||||||
InjectTransactionManager,
|
InjectTransactionManager,
|
||||||
MedusaContext
|
MedusaContext,
|
||||||
} from "@medusajs/framework/utils"
|
} from "@medusajs/framework/utils"
|
||||||
import { Context } from "@medusajs/framework/types"
|
import { Context } from "@medusajs/framework/types"
|
||||||
import { EntityManager } from "@mikro-orm/knex"
|
import { EntityManager } from "@mikro-orm/knex"
|
||||||
@@ -118,10 +118,10 @@ class HelloModuleService {
|
|||||||
await transactionManager.nativeUpdate(
|
await transactionManager.nativeUpdate(
|
||||||
"my_custom",
|
"my_custom",
|
||||||
{
|
{
|
||||||
id: input.id
|
id: input.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: input.name
|
name: input.name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -307,7 +307,7 @@ export const repoHighlights = [
|
|||||||
import {
|
import {
|
||||||
InjectManager,
|
InjectManager,
|
||||||
InjectTransactionManager,
|
InjectTransactionManager,
|
||||||
MedusaContext
|
MedusaContext,
|
||||||
} from "@medusajs/framework/utils"
|
} from "@medusajs/framework/utils"
|
||||||
import { Context } from "@medusajs/framework/types"
|
import { Context } from "@medusajs/framework/types"
|
||||||
import { EntityManager } from "@mikro-orm/knex"
|
import { EntityManager } from "@mikro-orm/knex"
|
||||||
@@ -327,10 +327,10 @@ class HelloModuleService {
|
|||||||
await transactionManager.nativeUpdate(
|
await transactionManager.nativeUpdate(
|
||||||
"my_custom",
|
"my_custom",
|
||||||
{
|
{
|
||||||
id: input.id
|
id: input.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: input.name
|
name: input.name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -342,7 +342,7 @@ class HelloModuleService {
|
|||||||
return updatedRecord
|
return updatedRecord
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
transaction: sharedContext.transactionManager
|
transaction: sharedContext.transactionManager,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -397,7 +397,7 @@ class HelloModuleService {
|
|||||||
// ...
|
// ...
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
transaction: sharedContext.transactionManager
|
transaction: sharedContext.transactionManager,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -430,7 +430,7 @@ class HelloModuleService {
|
|||||||
// ...
|
// ...
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isolationLevel: IsolationLevel.READ_COMMITTED
|
isolationLevel: IsolationLevel.READ_COMMITTED,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -456,7 +456,7 @@ class HelloModuleService {
|
|||||||
// ...
|
// ...
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enableNestedTransactions: false
|
enableNestedTransactions: false,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,12 @@ This chapter provides links to example sections on different Medusa topics.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Custom CLI Scripts
|
||||||
|
|
||||||
|
- [Seed Dummy Products](../../advanced-development/custom-cli-scripts/seed-data/page.mdx)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Admin Customizations
|
## Admin Customizations
|
||||||
|
|
||||||
- [Send a request to custom API routes from widgets or UI routes](../../customization/customize-admin/widget/page.mdx)
|
- [Send a request to custom API routes from widgets or UI routes](../../customization/customize-admin/widget/page.mdx)
|
||||||
|
|||||||
@@ -108,9 +108,10 @@ export const generatedEditDates = {
|
|||||||
"app/customization/next-steps/page.mdx": "2024-09-12T10:50:04.873Z",
|
"app/customization/next-steps/page.mdx": "2024-09-12T10:50:04.873Z",
|
||||||
"app/customization/page.mdx": "2024-09-12T11:16:18.504Z",
|
"app/customization/page.mdx": "2024-09-12T11:16:18.504Z",
|
||||||
"app/more-resources/cheatsheet/page.mdx": "2024-07-11T16:11:26.480Z",
|
"app/more-resources/cheatsheet/page.mdx": "2024-07-11T16:11:26.480Z",
|
||||||
"app/more-resources/examples/page.mdx": "2024-09-27T07:17:16.892Z",
|
"app/more-resources/examples/page.mdx": "2024-10-03T11:12:50.956Z",
|
||||||
"app/architecture/architectural-modules/page.mdx": "2024-09-23T12:51:04.520Z",
|
"app/architecture/architectural-modules/page.mdx": "2024-09-23T12:51:04.520Z",
|
||||||
"app/architecture/overview/page.mdx": "2024-09-23T12:55:01.339Z",
|
"app/architecture/overview/page.mdx": "2024-09-23T12:55:01.339Z",
|
||||||
"app/advanced-development/data-models/infer-type/page.mdx": "2024-09-30T08:43:53.123Z",
|
"app/advanced-development/data-models/infer-type/page.mdx": "2024-09-30T08:43:53.123Z",
|
||||||
|
"app/advanced-development/custom-cli-scripts/seed-data/page.mdx": "2024-10-03T11:11:07.181Z",
|
||||||
"app/basics/modules/page.mdx": "2024-10-03T13:05:49.551Z"
|
"app/basics/modules/page.mdx": "2024-10-03T13:05:49.551Z"
|
||||||
}
|
}
|
||||||
@@ -472,6 +472,13 @@ export const sidebar = numberSidebarItems(
|
|||||||
type: "link",
|
type: "link",
|
||||||
path: "/advanced-development/custom-cli-scripts",
|
path: "/advanced-development/custom-cli-scripts",
|
||||||
title: "Custom CLI Scripts",
|
title: "Custom CLI Scripts",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "link",
|
||||||
|
path: "/advanced-development/custom-cli-scripts/seed-data",
|
||||||
|
title: "Seed Data",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "link",
|
type: "link",
|
||||||
|
|||||||
Reference in New Issue
Block a user