docs: revamped endpoints, services, and entities (#4660)
* revamp create endpoint docs * docs: revamped endpoints, services, and entities * eslint fixes * fix metadata * fix missing closing tag * fixes to create migration doc * fixes to create endpoint doc * small fix in create service doc
This commit is contained in:
@@ -57,7 +57,7 @@ To apply a middleware on any endpoint, you can use the same router defined in `s
|
||||
|
||||
:::warning
|
||||
|
||||
The examples used here don't apply Cross-Origin Resource Origin (CORS) options for simplicity. Make sure to apply them, especially for core routes, as explained in the [Create Endpoint](./create.md#cors-configuration) documentation.
|
||||
The examples used here don't apply Cross-Origin Resource Origin (CORS) options for simplicity. Make sure to apply them, especially for core routes, as explained in the [Create Endpoint](./create.mdx#cors-configuration) documentation.
|
||||
|
||||
:::
|
||||
|
||||
|
||||
@@ -1,376 +0,0 @@
|
||||
---
|
||||
description: 'Learn how to create endpoints in Medusa. This guide also includes how to add CORS configurations, creating multiple endpoints, adding protected routes, and more.'
|
||||
addHowToData: true
|
||||
---
|
||||
|
||||
# How to Create Endpoints
|
||||
|
||||
In this document, you’ll learn how to create endpoints in Medusa.
|
||||
|
||||
## Overview
|
||||
|
||||
Custom endpoints are created under the `src/api` directory in your Medusa Backend. They're defined in a TypeScript or JavaScript file named `index` (for example, `index.ts`). This file should export a function that returns an Express router or an array of routes and middlewares.
|
||||
|
||||
To consume the custom endpoints in your Medusa backend, you must transpile them with the `build` command before starting your backend.
|
||||
|
||||
---
|
||||
|
||||
## Implementation
|
||||
|
||||
To create a new endpoint, start by creating a new file in `src/api` called `index.ts`. At its basic format, `index.ts` should look something like this:
|
||||
|
||||
```ts title=src/api/index.ts
|
||||
import { Router } from "express"
|
||||
|
||||
export default (rootDirectory, options) => {
|
||||
const router = Router()
|
||||
|
||||
router.get("/hello", (req, res) => {
|
||||
res.json({
|
||||
message: "Welcome to My Store!",
|
||||
})
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
```
|
||||
|
||||
This exports a function that returns an Express router. The function receives two parameters:
|
||||
|
||||
- `rootDirectory` is the absolute path to the root directory that your backend is running from.
|
||||
- `options` is an object that contains the configurations exported from `medusa-config.js`. If your API route is part of a plugin, then it will contain the plugin's options instead.
|
||||
|
||||
### Defining Multiple Routes or Middlewares
|
||||
|
||||
Instead of returning an Express router in the function, you can return an array of routes and [middlewares](./add-middleware.mdx).
|
||||
|
||||
For example:
|
||||
|
||||
```ts title=src/api/index.ts
|
||||
import { Router } from "express"
|
||||
|
||||
export default (rootDirectory, options) => {
|
||||
const router = Router()
|
||||
|
||||
router.get("/hello", (req, res) => {
|
||||
res.json({
|
||||
message: "Welcome to My Store!",
|
||||
})
|
||||
})
|
||||
|
||||
// you can also define the middleware
|
||||
// in another file and import it
|
||||
const middleware = (res, req, next) => {
|
||||
// TODO define global middleware
|
||||
console.log("hello from middleware")
|
||||
next()
|
||||
}
|
||||
|
||||
const anotherRouter = Router()
|
||||
anotherRouter.get("/store/*", (req, res, next) => {
|
||||
// TODO perform an actions for all store endpoints
|
||||
next()
|
||||
})
|
||||
|
||||
return [middleware, router, anotherRouter]
|
||||
}
|
||||
```
|
||||
|
||||
This allows you to export multiple routers and middlewares from the same file. You can also import the routers, routes, and middlewares from other files, then import them in `src/api/index.ts` instead of defining them within the same file.
|
||||
|
||||
### Endpoints Path
|
||||
|
||||
Your endpoint can be under any path you wish.
|
||||
|
||||
By Medusa’s conventions:
|
||||
|
||||
- All Storefront REST APIs are prefixed by `/store`. For example, the `/store/products` endpoint lets you retrieve the products to display them on your storefront.
|
||||
- All Admin REST APIs are prefixed by `/admin`. For example, the `/admin/products` endpoint lets you retrieve the products to display them on your Admin.
|
||||
|
||||
You can also create endpoints that don't reside under these two prefixes, similar to the `hello` endpoint in the previous example.
|
||||
|
||||
---
|
||||
|
||||
## Retrieve Medusa Config
|
||||
|
||||
As mentioned, the second parameter `options` includes the configurations exported from `medusa-config.js`. However, in plugins it only includes the plugin's options.
|
||||
|
||||
If you need to access the Medusa configuration in your endpoint, you can use the `getConfigFile` method imported from `medusa-core-utils`. It accepts the following parameters:
|
||||
|
||||
1. `rootDirectory`: The first parameter is a string indicating root directory of your Medusa backend.
|
||||
2. `config`: The second parameter is a string indicating the name of the config file, which should be `medusa-config` unless you change it.
|
||||
|
||||
The function returns an object with the following properties:
|
||||
|
||||
1. `configModule`: An object containing the configurations exported from `medusa-config.js`.
|
||||
2. `configFilePath`: A string indicating absolute path to the configuration file.
|
||||
3. `error`: if any errors occur, they'll be included as the value of this property. Otherwise, its value will be `undefined`.
|
||||
|
||||
Here's an example of retrieving the configurations within an endpoint using `getConfigFile`:
|
||||
|
||||
```ts title=src/api/index.ts
|
||||
import { Router } from "express"
|
||||
import { ConfigModule } from "@medusajs/medusa"
|
||||
import { getConfigFile } from "medusa-core-utils"
|
||||
|
||||
export default (rootDirectory) => {
|
||||
const router = Router()
|
||||
const { configModule } = getConfigFile<ConfigModule>(
|
||||
rootDirectory,
|
||||
"medusa-config"
|
||||
)
|
||||
|
||||
router.get("/store-cors", (req, res) => {
|
||||
res.json({
|
||||
store_cors: configModule.projectConfig.store_cors,
|
||||
})
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
```
|
||||
|
||||
Notice that `getConfigFile` is a generic function. So, if you're using TypeScript, you should pass it the type `ConfigModule` imported from `@medusajs/medusa`.
|
||||
|
||||
If you're accessing custom configurations, you'll need to create a new type that defines these configurations. For example:
|
||||
|
||||
```ts title=src/api/index.ts
|
||||
import { Router } from "express"
|
||||
import { ConfigModule } from "@medusajs/medusa"
|
||||
import { getConfigFile } from "medusa-core-utils"
|
||||
|
||||
type MyConfigModule = ConfigModule & {
|
||||
projectConfig: {
|
||||
custom_config?: string
|
||||
}
|
||||
}
|
||||
|
||||
export default (rootDirectory) => {
|
||||
const router = Router()
|
||||
const { configModule } = getConfigFile<MyConfigModule>(
|
||||
rootDirectory,
|
||||
"medusa-config"
|
||||
)
|
||||
|
||||
router.get("/hello", (req, res) => {
|
||||
res.json({
|
||||
custom_config: configModule.projectConfig.custom_config,
|
||||
})
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CORS Configuration
|
||||
|
||||
If you’re adding a storefront or admin endpoint and you want to access these endpoints from the storefront or Medusa admin, you need to pass your endpoints Cross-Origin Resource Origin (CORS) options using the `cors` package.
|
||||
|
||||
First, you need to import the necessary utility functions and types from Medusa's packages with the `cors` library:
|
||||
|
||||
```ts
|
||||
import {
|
||||
getConfigFile,
|
||||
parseCorsOrigins,
|
||||
} from "medusa-core-utils"
|
||||
import {
|
||||
ConfigModule,
|
||||
} from "@medusajs/medusa/dist/types/global"
|
||||
import cors from "cors"
|
||||
```
|
||||
|
||||
Next, in the exported function, retrieve the CORS configurations of your backend using the utility functions you imported:
|
||||
|
||||
```ts
|
||||
export default (rootDirectory) => {
|
||||
// ...
|
||||
|
||||
const { configModule } =
|
||||
getConfigFile<ConfigModule>(rootDirectory, "medusa-config")
|
||||
const { projectConfig } = configModule
|
||||
|
||||
// ....
|
||||
}
|
||||
```
|
||||
|
||||
Then, create an object that will hold the CORS configurations. If it’s a storefront endpoint, pass the `origin` property storefront options:
|
||||
|
||||
```ts
|
||||
const corsOptions = {
|
||||
origin: projectConfig.store_cors.split(","),
|
||||
credentials: true,
|
||||
}
|
||||
```
|
||||
|
||||
If it’s an admin endpoint, pass the `origin` property admin options:
|
||||
|
||||
```ts
|
||||
const corsOptions = {
|
||||
origin: projectConfig.admin_cors.split(","),
|
||||
credentials: true,
|
||||
}
|
||||
```
|
||||
|
||||
Finally, for each route you add, create an `OPTIONS` request and add `cors` as a middleware for the route passing it the CORS option:
|
||||
|
||||
```ts
|
||||
router.options("/admin/hello", cors(corsOptions))
|
||||
router.get("/admin/hello", cors(corsOptions), (req, res) => {
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Parse Request Body Parameters
|
||||
|
||||
If you want to accept request body parameters, you need to pass express middlewares that parse the payload type to your router.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import bodyParser from "body-parser"
|
||||
import express, { Router } from "express"
|
||||
|
||||
|
||||
export default (rootDirectory, pluginOptions) => {
|
||||
const router = Router()
|
||||
|
||||
router.use(express.json())
|
||||
router.use(express.urlencoded({ extended: true }))
|
||||
|
||||
router.post("/store/hello", (req, res) => {
|
||||
res.json({
|
||||
message: req.body.name,
|
||||
})
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
```
|
||||
|
||||
In the code snippet above, you use the following middlewares:
|
||||
|
||||
- `express.json()`: parses requests with JSON payloads
|
||||
- `express.urlencoded()`: parses requests with urlencoded payloads.
|
||||
|
||||
You can learn about other available middlewares in the [Express documentation](https://expressjs.com/en/api.html#express).
|
||||
|
||||
---
|
||||
|
||||
## Protected Routes
|
||||
|
||||
Protected routes are routes that should be accessible by logged-in customers or users only.
|
||||
|
||||
### Protect Store Routes
|
||||
|
||||
To make a storefront route protected, first, import the `authenticateCustomer` middleware:
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```ts
|
||||
import { authenticateCustomer } from "@medusajs/medusa"
|
||||
```
|
||||
|
||||
Then, add the middleware to your route:
|
||||
|
||||
```ts
|
||||
router.options("/store/hello", cors(corsOptions))
|
||||
router.get(
|
||||
"/store/hello",
|
||||
cors(corsOptions),
|
||||
authenticateCustomer(),
|
||||
async (req, res) => {
|
||||
if (req.user) {
|
||||
// user is logged in
|
||||
// to get customer id: req.user.customer_id
|
||||
}
|
||||
// ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Please note that the endpoint is still accessible by all users, however, you’ll be able to access the current logged-in customer if there’s any.
|
||||
|
||||
To disallow guest customers from accessing the endpoint, you can throw an error if `req.user` is `false`.
|
||||
|
||||
### Protect Admin Routes
|
||||
|
||||
To make an admin route protected, first, import the `authenticate` middleware:
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```ts
|
||||
import { authenticate } from "@medusajs/medusa"
|
||||
```
|
||||
|
||||
Then, add the middleware to your route:
|
||||
|
||||
```ts
|
||||
router.options("/admin/products/count", cors(corsOptions))
|
||||
router.get(
|
||||
"/admin/products/count",
|
||||
cors(corsOptions),
|
||||
authenticate(),
|
||||
async (req, res) => {
|
||||
// access current user
|
||||
const id = req.user.userId
|
||||
const userService = req.scope.resolve("userService")
|
||||
|
||||
const user = await userService.retrieve(id)
|
||||
// ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Now, only authenticated users can access this endpoint.
|
||||
|
||||
---
|
||||
|
||||
## Use Services
|
||||
|
||||
Services in Medusa bundle a set of functionalities into one class. Then, you can use that class anywhere in your backend. For example, you can use the `ProductService` to retrieve products or perform operations like creating or updating a product.
|
||||
|
||||
You can retrieve any registered service in your endpoint using `req.scope.resolve` passing it the service’s registration name.
|
||||
|
||||
Here’s an example of an endpoint that retrieves the count of products in your store:
|
||||
|
||||
```ts
|
||||
router.get(
|
||||
"/admin/products/count",
|
||||
cors(corsOptions),
|
||||
authenticate(),
|
||||
(req, res) => {
|
||||
const productService = req.scope.resolve("productService")
|
||||
|
||||
productService.count().then((count) => {
|
||||
res.json({
|
||||
count,
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The `productService` has a `count` method that returns a Promise. This Promise resolves to the count of the products. You return a JSON of the product count.
|
||||
|
||||
---
|
||||
|
||||
## Building Files
|
||||
|
||||
Custom endpoints must be transpiled and moved to the `dist` directory before you can start consuming them. When you run your backend using either the `medusa develop` or `npx medusa develop` commands, it watches the files under `src` for any changes, then triggers the `build` command and restarts the server.
|
||||
|
||||
The build isn't triggered though when the backend first starts running. So, make sure to run the `build` command before starting the backend:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Storefront API Reference](/api/store)
|
||||
- [Admin API Reference](/api/admin)
|
||||
953
docs/content/development/endpoints/create.mdx
Normal file
953
docs/content/development/endpoints/create.mdx
Normal file
@@ -0,0 +1,953 @@
|
||||
---
|
||||
description: 'Learn how to create endpoints in Medusa. This guide also includes how to add CORS configurations, creating multiple endpoints, adding protected routes, and more.'
|
||||
addHowToData: true
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
# How to Create Endpoints
|
||||
|
||||
In this document, you’ll learn how to create endpoints in Medusa.
|
||||
|
||||
## Overview
|
||||
|
||||
Custom endpoints are created under the `src/api` directory in your Medusa Backend. They're defined in a TypeScript or JavaScript file named `index` (for example, `index.ts`). This file should export a function that returns an Express router or an array of routes and middlewares.
|
||||
|
||||
To consume the custom endpoints in your Medusa backend, you must transpile them with the `build` command before starting your backend.
|
||||
|
||||
---
|
||||
|
||||
## Basic Implementation
|
||||
|
||||
To create a new endpoint, start by creating a new file in `src/api` called `index.ts`. At its basic format, `index.ts` should look something like this:
|
||||
|
||||
```ts title=src/api/index.ts
|
||||
import { Router } from "express"
|
||||
|
||||
export default (rootDirectory, options) => {
|
||||
const router = Router()
|
||||
|
||||
router.get("/hello", (req, res) => {
|
||||
res.json({
|
||||
message: "Welcome to My Store!",
|
||||
})
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
```
|
||||
|
||||
This exports a function that returns an Express router. The function receives two parameters:
|
||||
|
||||
- `rootDirectory` is the absolute path to the root directory that your backend is running from.
|
||||
- `options` is an object that contains the configurations exported from `medusa-config.js`. If your API route is part of a plugin, then it will contain the plugin's options instead.
|
||||
|
||||
---
|
||||
|
||||
## Building Files
|
||||
|
||||
Custom endpoints must be transpiled and moved to the `dist` directory before you can start consuming them. When you run your backend using either the `medusa develop` or `npx medusa develop` commands, it watches the files under `src` for any changes, then triggers the `build` command and restarts the server.
|
||||
|
||||
However, the build isn't triggered when the backend first starts running, and it's never triggered when the `medusa start` or `npx medusa start` commands are used.
|
||||
|
||||
So, make sure to run the `build` command before starting the backend:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Defining Multiple Routes or Middlewares
|
||||
|
||||
Instead of returning an Express router in the function, you can return an array of routes and [middlewares](./add-middleware.mdx).
|
||||
|
||||
For example:
|
||||
|
||||
```ts title=src/api/index.ts
|
||||
import { Router } from "express"
|
||||
|
||||
export default (rootDirectory, options) => {
|
||||
const router = Router()
|
||||
|
||||
router.get("/hello", (req, res) => {
|
||||
res.json({
|
||||
message: "Welcome to My Store!",
|
||||
})
|
||||
})
|
||||
|
||||
// you can also define the middleware
|
||||
// in another file and import it
|
||||
const middleware = (res, req, next) => {
|
||||
// TODO define global middleware
|
||||
console.log("hello from middleware")
|
||||
next()
|
||||
}
|
||||
|
||||
const anotherRouter = Router()
|
||||
anotherRouter.get("/store/*", (req, res, next) => {
|
||||
// TODO perform an actions for all store endpoints
|
||||
next()
|
||||
})
|
||||
|
||||
return [middleware, router, anotherRouter]
|
||||
}
|
||||
```
|
||||
|
||||
This allows you to export multiple routers and middlewares from the same file. You can also import the routers, routes, and middlewares from other files, then import them in `src/api/index.ts` instead of defining them within the same file.
|
||||
|
||||
---
|
||||
|
||||
## Endpoint Path
|
||||
|
||||
Your endpoint can be under any path you wish.
|
||||
|
||||
By Medusa’s conventions:
|
||||
|
||||
- All Storefront REST APIs are prefixed by `/store`. For example, the `/store/products` endpoint lets you retrieve the products to display them on your storefront.
|
||||
- All Admin REST APIs are prefixed by `/admin`. For example, the `/admin/products` endpoint lets you retrieve the products to display them on your Admin.
|
||||
|
||||
You can also create endpoints that don't reside under these two prefixes, similar to the `hello` endpoint in the previous example.
|
||||
|
||||
---
|
||||
|
||||
## CORS Configuration
|
||||
|
||||
If you’re adding a storefront or admin endpoint and you want to access these endpoints from the storefront or Medusa admin, you need to pass your endpoints Cross-Origin Resource Origin (CORS) options using the `cors` package.
|
||||
|
||||
First, import the necessary utility functions and types from Medusa's packages with the `cors` package:
|
||||
|
||||
```ts
|
||||
import {
|
||||
getConfigFile,
|
||||
parseCorsOrigins,
|
||||
} from "medusa-core-utils"
|
||||
import {
|
||||
ConfigModule,
|
||||
} from "@medusajs/medusa/dist/types/global"
|
||||
import cors from "cors"
|
||||
```
|
||||
|
||||
Next, in the exported function, retrieve the CORS configurations of your backend using the utility functions you imported:
|
||||
|
||||
```ts
|
||||
export default (rootDirectory) => {
|
||||
// ...
|
||||
|
||||
const { configModule } =
|
||||
getConfigFile<ConfigModule>(rootDirectory, "medusa-config")
|
||||
const { projectConfig } = configModule
|
||||
|
||||
// ....
|
||||
}
|
||||
```
|
||||
|
||||
Then, create an object that will hold the CORS configurations. Based on whether it's storefront or admin CORS options, you pass it the respective configuration from `projectConfig`:
|
||||
|
||||
<Tabs groupId="endpoint-type" isCodeTabs={true}>
|
||||
<TabItem value="storefront" label="Storefront CORS" default>
|
||||
|
||||
```ts
|
||||
const storeCorsOptions = {
|
||||
origin: projectConfig.store_cors.split(","),
|
||||
credentials: true,
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="admin" label="Admin CORS">
|
||||
|
||||
```ts
|
||||
const adminCorsOptions = {
|
||||
origin: projectConfig.admin_cors.split(","),
|
||||
credentials: true,
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Finally, you can either pass the `cors` middleware for a specific route, or pass it to the entire router:
|
||||
|
||||
<Tabs groupId="pass-type" isCodeTabs={true}>
|
||||
<TabItem value="single" label="Pass to Endpoint" default>
|
||||
|
||||
```ts
|
||||
adminRouter.options("/admin/hello", cors(adminCorsOptions))
|
||||
adminRouter.get(
|
||||
"/admin/hello",
|
||||
cors(adminCorsOptions),
|
||||
(req, res) => {
|
||||
// ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="router" label="Pass to Router">
|
||||
|
||||
```ts
|
||||
adminRouter.use(cors(adminCorsOptions))
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## Parse Request Body Parameters
|
||||
|
||||
If you want to accept request body parameters, you need to pass express middlewares that parse the payload type to your router.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title=src/api/index.ts
|
||||
import bodyParser from "body-parser"
|
||||
import express, { Router } from "express"
|
||||
|
||||
|
||||
export default (rootDirectory, pluginOptions) => {
|
||||
const router = Router()
|
||||
|
||||
router.use(express.json())
|
||||
router.use(express.urlencoded({ extended: true }))
|
||||
|
||||
router.post("/store/hello", (req, res) => {
|
||||
res.json({
|
||||
message: req.body.name,
|
||||
})
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
```
|
||||
|
||||
In the code snippet above, you use the following middlewares:
|
||||
|
||||
- `express.json()`: parses requests with JSON payloads
|
||||
- `express.urlencoded()`: parses requests with urlencoded payloads.
|
||||
|
||||
You can learn about other available middlewares in the [Express documentation](https://expressjs.com/en/api.html#express).
|
||||
|
||||
---
|
||||
|
||||
## Protected Routes
|
||||
|
||||
Protected routes are routes that should only be accessible by logged-in customers or users.
|
||||
|
||||
### Protect Store Routes
|
||||
|
||||
There are two approaches to make a storefront route protected:
|
||||
|
||||
- Using the `requireCustomerAuthentication` middleware, which disallows unauthenticated customers from accessing a route, and allows you to access the logged-in customer's ID.
|
||||
- Using the `authenticateCustomer` middleware, which allows both authenticated and unauthenticated customers to access your route, but allows you to access the logged-in customer's ID as well.
|
||||
|
||||
To make a storefront route protected using either middlewares, first, import the middleware at the top of your file:
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```ts
|
||||
import { requireCustomerAuthentication } from "@medusajs/medusa"
|
||||
// import { authenticateCustomer } from "@medusajs/medusa"
|
||||
```
|
||||
|
||||
Then, pass the middleware to either a single route or an entire router:
|
||||
|
||||
<Tabs groupId="pass-type" isCodeTabs={true}>
|
||||
<TabItem value="single" label="Pass to Endpoint" default>
|
||||
|
||||
```ts
|
||||
// only necessary if you're passing cors options per route
|
||||
router.options("/store/hello", cors(storeCorsOptions))
|
||||
router.get(
|
||||
"/store/hello",
|
||||
cors(storeCorsOptions),
|
||||
requireCustomerAuthentication(),
|
||||
// authenticateCustomer()
|
||||
async (req, res) => {
|
||||
// access current customer
|
||||
const id = req.user.customerId
|
||||
// if you're using authenticateCustomer middleware
|
||||
// check if id is set first
|
||||
|
||||
const customerService = req.scope.resolve("customerService")
|
||||
|
||||
const customer = await customerService.retrieve(id)
|
||||
// ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="router" label="Pass to Router">
|
||||
|
||||
```ts
|
||||
storeRouter.use(requireCustomerAuthentication())
|
||||
// all routes added to storeRouter are now protected
|
||||
// the logged in customer can be accessed using:
|
||||
// req.user.customer_id
|
||||
|
||||
// storeRouter.use(authenticateCustomer())
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Protect Admin Routes
|
||||
|
||||
To protect admin routes and only allow logged-in users from accessing them, first, import the `authenticate` middleware at the top of the file:
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```ts
|
||||
import { authenticate } from "@medusajs/medusa"
|
||||
```
|
||||
|
||||
Then, pass the middleware to either a single route or an entire router:
|
||||
|
||||
<Tabs groupId="pass-type" isCodeTabs={true}>
|
||||
<TabItem value="single" label="Pass to Endpoint" default>
|
||||
|
||||
```ts
|
||||
// only necessary if you're passing cors options per route
|
||||
adminRouter.options("/admin/hello", cors(adminCorsOptions))
|
||||
adminRouter.get(
|
||||
"/admin/hello",
|
||||
cors(adminCorsOptions),
|
||||
authenticate(),
|
||||
async (req, res) => {
|
||||
// access current user
|
||||
const id = req.user.userId
|
||||
const userService = req.scope.resolve("userService")
|
||||
|
||||
const user = await userService.retrieve(id)
|
||||
// ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="router" label="Pass to Router">
|
||||
|
||||
```ts
|
||||
adminRouter.use(authenticate())
|
||||
// all routes added to adminRouter are now protected
|
||||
// the logged in user can be accessed using:
|
||||
// req.user.userId
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## Retrieve Medusa Config
|
||||
|
||||
As mentioned, the second parameter `options` includes the configurations exported from `medusa-config.js`. However, in plugins it only includes the plugin's options.
|
||||
|
||||
If you need to access the Medusa configuration in your endpoint, you can use the `getConfigFile` method imported from `medusa-core-utils`. It accepts the following parameters:
|
||||
|
||||
1. `rootDirectory`: The first parameter is a string indicating root directory of your Medusa backend.
|
||||
2. `config`: The second parameter is a string indicating the name of the config file, which should be `medusa-config` unless you change it.
|
||||
|
||||
The function returns an object with the following properties:
|
||||
|
||||
1. `configModule`: An object containing the configurations exported from `medusa-config.js`.
|
||||
2. `configFilePath`: A string indicating absolute path to the configuration file.
|
||||
3. `error`: if any errors occur, they'll be included as the value of this property. Otherwise, its value will be `undefined`.
|
||||
|
||||
Here's an example of retrieving the configurations within an endpoint using `getConfigFile`:
|
||||
|
||||
```ts title=src/api/index.ts
|
||||
import { Router } from "express"
|
||||
import { ConfigModule } from "@medusajs/medusa"
|
||||
import { getConfigFile } from "medusa-core-utils"
|
||||
|
||||
export default (rootDirectory) => {
|
||||
const router = Router()
|
||||
const { configModule } = getConfigFile<ConfigModule>(
|
||||
rootDirectory,
|
||||
"medusa-config"
|
||||
)
|
||||
|
||||
router.get("/store-cors", (req, res) => {
|
||||
res.json({
|
||||
store_cors: configModule.projectConfig.store_cors,
|
||||
})
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
```
|
||||
|
||||
Notice that `getConfigFile` is a generic function. So, if you're using TypeScript, you should pass it the type `ConfigModule` imported from `@medusajs/medusa`.
|
||||
|
||||
If you're accessing custom configurations, you'll need to create a new type that defines these configurations. For example:
|
||||
|
||||
```ts title=src/api/index.ts
|
||||
import { Router } from "express"
|
||||
import { ConfigModule } from "@medusajs/medusa"
|
||||
import { getConfigFile } from "medusa-core-utils"
|
||||
|
||||
type MyConfigModule = ConfigModule & {
|
||||
projectConfig: {
|
||||
custom_config?: string
|
||||
}
|
||||
}
|
||||
|
||||
export default (rootDirectory) => {
|
||||
const router = Router()
|
||||
const { configModule } = getConfigFile<MyConfigModule>(
|
||||
rootDirectory,
|
||||
"medusa-config"
|
||||
)
|
||||
|
||||
router.get("/hello", (req, res) => {
|
||||
res.json({
|
||||
custom_config: configModule.projectConfig.custom_config,
|
||||
})
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Handle Errors
|
||||
|
||||
As Medusa uses v4 of Express, you need to manually handle errors thrown asynchronously as explained in [Express's documentation](https://expressjs.com/en/guide/error-handling.html).
|
||||
|
||||
You can use [middlewares](./add-middleware.mdx) to handle errors. You can also use middlewares defined by Medusa, which ensure that your error handling is consistent across your Medusa backend.
|
||||
|
||||
:::note
|
||||
|
||||
Code snippets are taken from the [full example available at the end of this document](#example-crud-endpoints).
|
||||
|
||||
:::
|
||||
|
||||
To handle errors using Medusa's middlewares, first, import the `errorHandler` middleware from `@medusajs/medusa` and apply it on your routers. Make sure it's applied after all other middlewares and routes:
|
||||
|
||||
```ts title=src/api/index.ts
|
||||
import express, { Router } from "express"
|
||||
import adminRoutes from "./admin"
|
||||
import storeRoutes from "./store"
|
||||
import { errorHandler } from "@medusajs/medusa"
|
||||
|
||||
export default (rootDirectory, options) => {
|
||||
const router = Router()
|
||||
|
||||
router.use(express.json())
|
||||
router.use(express.urlencoded({ extended: true }))
|
||||
|
||||
adminRoutes(router, options)
|
||||
storeRoutes(router, options)
|
||||
|
||||
router.use(errorHandler())
|
||||
|
||||
return router
|
||||
}
|
||||
```
|
||||
|
||||
Then, wrap the function handler of every route with the `wrapHandler` middleware imported from `@medusajs/medusa`. For example:
|
||||
|
||||
```ts title=src/api/admin.ts
|
||||
import { wrapHandler } from "@medusajs/medusa"
|
||||
|
||||
// ...
|
||||
|
||||
export default function adminRoutes(
|
||||
router: Router,
|
||||
options: ConfigModule
|
||||
) {
|
||||
// ...
|
||||
|
||||
adminRouter.get("/posts", wrapHandler(async (req, res) => {
|
||||
const postService: PostService =
|
||||
req.scope.resolve("postService")
|
||||
|
||||
res.json({
|
||||
posts: await postService.list(),
|
||||
})
|
||||
}))
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, you can define the endpoints in different files, and import and use them in your router:
|
||||
|
||||
<!-- eslint-disable @typescript-eslint/no-var-requires -->
|
||||
|
||||
```ts title=src/api/admin.ts
|
||||
import { wrapHandler } from "@medusajs/medusa"
|
||||
|
||||
// ...
|
||||
|
||||
export default function adminRoutes(
|
||||
router: Router,
|
||||
options: ConfigModule
|
||||
) {
|
||||
// ...
|
||||
|
||||
adminRouter.get(
|
||||
"/posts",
|
||||
wrapHandler(require("./list-posts").default)
|
||||
)
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Now all errors thrown in your custom endpoints, including in their custom services, will be caught and returned to the user.
|
||||
|
||||
However, if you throw errors like this:
|
||||
|
||||
```ts
|
||||
throw new Error ("Post was not found")
|
||||
```
|
||||
|
||||
You'll notice that the endpoint returns the following object error in the response:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "unknown_error",
|
||||
"type": "unknown_error",
|
||||
"message": "An unknown error occurred."
|
||||
}
|
||||
```
|
||||
|
||||
To ensure the error message is relayed in the response, it's recommended to use `MedusaError` imported from `@medusajs/utils` as the thrown error instead:
|
||||
|
||||
```ts
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
|
||||
// ...
|
||||
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
"Post was not found"
|
||||
)
|
||||
```
|
||||
|
||||
The constructor of `MedusaError` accepts the following parameters:
|
||||
|
||||
1. The first parameter is the type of the error. You can use one of the predefined errors under `MedusaError.Types`, such as `MedusaError.Types.NOT_FOUND` which sets the response status code to `404` automatically.
|
||||
2. The second parameter is the message of the error.
|
||||
3. The third parameter is an optional code, which is a string, that can be returned in the error object.
|
||||
|
||||
After using `MedusaError`, you'll notice that the returned error in the response provides a clearer message:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "not_found",
|
||||
"message": "Post was not found"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use Other Resources
|
||||
|
||||
### Entities and Repositories
|
||||
|
||||
Your endpoints likely perform an action on an entity. For example, you may create an endpoint to retrieve a list of posts.
|
||||
|
||||
You can perform actions on an entity either through its [Repository](../entities/overview.mdx#what-are-repositories) or through a [service](#services). This section covers how to retrieve a repository in an endpoint, but it's recommended to use services instead.
|
||||
|
||||
You can retrieve any registered resource, including repositories, in your endpoint using `req.scope.resolve` passing it the resource's registration name in the [dependency container](../fundamentals/dependency-injection.md). Repositories are registered as their camel-case name. So, for example, if you have a `PostRepository`, it's registered as `postRepository`.
|
||||
|
||||
Here’s an example of an endpoint that retrieves the list of posts in a store:
|
||||
|
||||
:::note
|
||||
|
||||
Posts are represented by a custom entity not covered in this guide. You can refer to the [entities](../entities/create.mdx#adding-relations) for more details on how to create a custom entity.
|
||||
|
||||
:::
|
||||
|
||||
```ts
|
||||
import { PostRepository } from "../repositories/post"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
// ...
|
||||
|
||||
export default () => {
|
||||
// ...
|
||||
|
||||
storeRouter.get("/posts", async (req, res) => {
|
||||
const postRepository: typeof PostRepository =
|
||||
req.scope.resolve("postRepository")
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
const postRepo = manager.withRepository(postRepository)
|
||||
|
||||
return res.json({
|
||||
posts: await postRepo.find(),
|
||||
})
|
||||
})
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Notice that to retrieve an instance of the repository, you need to retrieve first Typeorm's Entity manager and use its `withRepository` method.
|
||||
|
||||
### Services
|
||||
|
||||
Services in Medusa bundle a set of functionalities into one class. Typically, these functionalities are associated with an entity, such as methods to retrieve, create, or update its records.
|
||||
|
||||
You can retrieve any registered resource, including services, in your endpoint using `req.scope.resolve` passing it the service’s registration name in the [dependency container](../fundamentals/dependency-injection.md). Services are registered as their camel-case name. So, for example, if you have a `PostService`, it's registered as `postService`.
|
||||
|
||||
Here’s an example of an endpoint that retrieves the list of posts in a store:
|
||||
|
||||
:::note
|
||||
|
||||
`PostService` is a custom service that is not covered in this guide. You can refer to the [services](../services/create-service.mdx) documentation for more details on how to create a custom service, and find an [example of PostService](../services/create-service.mdx#example-services-with-crud-operations)
|
||||
|
||||
:::
|
||||
|
||||
```ts
|
||||
storeRouter.get("/posts", async (req, res) => {
|
||||
const postService: PostService = req.scope.resolve(
|
||||
"postService"
|
||||
)
|
||||
|
||||
return res.json({
|
||||
posts: await postService.list(),
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Other Resources
|
||||
|
||||
Any resource that is registered in the dependency container, such as strategies or file services, can be accessed through `req.scope.resolve`. Refer to the [dependency injection](../fundamentals/dependency-injection.md) documentation for details on registered resources.
|
||||
|
||||
---
|
||||
|
||||
## Example: CRUD Endpoints
|
||||
|
||||
This section services as an example of creating endpoints that perform Create, Read, Update, and Delete (CRUD) operations. Note that all admin endpoints are placed in `src/api/admin.ts`, and store endpoints are placed in `src/api/store.ts`. You can also place each endpoint in a separate file, import it, and add it to its respective router.
|
||||
|
||||
You can refer to the [Entities](../entities/create.mdx#adding-relations) and [Services](../services/create-service.mdx#example-services-with-crud-operations) documentation to learn how to create the custom entities and services used in this example.
|
||||
|
||||
<Tabs groupId="files" isCodeTabs={true}>
|
||||
<TabItem value="index" label="src/api/index.ts" default>
|
||||
|
||||
```ts
|
||||
import express, { Router } from "express"
|
||||
import adminRoutes from "./admin"
|
||||
import storeRoutes from "./store"
|
||||
import { errorHandler } from "@medusajs/medusa"
|
||||
|
||||
export default (rootDirectory, options) => {
|
||||
const router = Router()
|
||||
|
||||
router.use(express.json())
|
||||
router.use(express.urlencoded({ extended: true }))
|
||||
|
||||
adminRoutes(router, options)
|
||||
storeRoutes(router, options)
|
||||
|
||||
router.use(errorHandler())
|
||||
|
||||
return router
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="admin" label="src/api/admin.ts">
|
||||
|
||||
```ts
|
||||
import { Router } from "express"
|
||||
import PostService from "../services/post"
|
||||
import {
|
||||
ConfigModule,
|
||||
} from "@medusajs/medusa/dist/types/global"
|
||||
import cors from "cors"
|
||||
import { authenticate, wrapHandler } from "@medusajs/medusa"
|
||||
import AuthorService from "../services/author"
|
||||
|
||||
export default function adminRoutes(
|
||||
router: Router,
|
||||
options: ConfigModule
|
||||
) {
|
||||
const { projectConfig } = options
|
||||
|
||||
const corsOptions = {
|
||||
origin: projectConfig.admin_cors.split(","),
|
||||
credentials: true,
|
||||
}
|
||||
|
||||
const adminRouter = Router()
|
||||
|
||||
router.use("/admin/blog", adminRouter)
|
||||
|
||||
adminRouter.use(cors(corsOptions))
|
||||
adminRouter.use(authenticate())
|
||||
|
||||
// it's recommended to define the routes
|
||||
// in separate files. They're done in
|
||||
// the same file here for simplicity
|
||||
|
||||
|
||||
// list all blog posts
|
||||
adminRouter.get(
|
||||
"/posts",
|
||||
wrapHandler(async (req, res) => {
|
||||
const postService: PostService = req.scope.resolve(
|
||||
"postService"
|
||||
)
|
||||
|
||||
res.json({
|
||||
posts: await postService.list(),
|
||||
})
|
||||
}))
|
||||
|
||||
|
||||
// retrieve a single blog post
|
||||
adminRouter.get(
|
||||
"/posts/:id",
|
||||
wrapHandler(async (req, res) => {
|
||||
const postService: PostService = req.scope.resolve(
|
||||
"postService"
|
||||
)
|
||||
|
||||
const post = await postService.retrieve(req.params.id)
|
||||
|
||||
res.json({
|
||||
post,
|
||||
})
|
||||
}))
|
||||
|
||||
// create a blog post
|
||||
adminRouter.post(
|
||||
"/posts",
|
||||
wrapHandler(async (req, res) => {
|
||||
const postService: PostService = req.scope.resolve(
|
||||
"postService"
|
||||
)
|
||||
|
||||
// basic validation of request body
|
||||
if (!req.body.title || !req.body.author_id) {
|
||||
throw new Error("`title` and `author_id` are required.")
|
||||
}
|
||||
|
||||
const post = await postService.create(req.body)
|
||||
|
||||
res.json({
|
||||
post,
|
||||
})
|
||||
}))
|
||||
|
||||
// update a blog post
|
||||
adminRouter.post(
|
||||
"/posts/:id",
|
||||
wrapHandler(async (req, res) => {
|
||||
const postService: PostService = req.scope.resolve(
|
||||
"postService"
|
||||
)
|
||||
|
||||
// basic validation of request body
|
||||
if (req.body.id) {
|
||||
throw new Error("Can't update post ID")
|
||||
}
|
||||
|
||||
const post = await postService.update(
|
||||
req.params.id,
|
||||
req.body
|
||||
)
|
||||
|
||||
res.json({
|
||||
post,
|
||||
})
|
||||
}))
|
||||
|
||||
// delete a blog post
|
||||
adminRouter.delete(
|
||||
"/posts/:id",
|
||||
wrapHandler(async (req, res) => {
|
||||
const postService: PostService = req.scope.resolve(
|
||||
"postService"
|
||||
)
|
||||
|
||||
await postService.delete(req.params.id)
|
||||
|
||||
res.status(200).end()
|
||||
}))
|
||||
|
||||
// list all blog authors
|
||||
adminRouter.get(
|
||||
"/authors",
|
||||
wrapHandler(async (req, res) => {
|
||||
const authorService: AuthorService = req.scope.resolve(
|
||||
"authorService"
|
||||
)
|
||||
|
||||
res.json({
|
||||
authors: await authorService.list(),
|
||||
})
|
||||
}))
|
||||
|
||||
// retrieve a single blog author
|
||||
adminRouter.get(
|
||||
"/authors/:id",
|
||||
wrapHandler(async (req, res) => {
|
||||
const authorService: AuthorService = req.scope.resolve(
|
||||
"authorService"
|
||||
)
|
||||
|
||||
res.json({
|
||||
post: await authorService.retrieve(req.params.id),
|
||||
})
|
||||
}))
|
||||
|
||||
// create a blog author
|
||||
adminRouter.post(
|
||||
"/authors",
|
||||
wrapHandler(async (req, res) => {
|
||||
const authorService: AuthorService = req.scope.resolve(
|
||||
"authorService"
|
||||
)
|
||||
|
||||
// basic validation of request body
|
||||
if (!req.body.name) {
|
||||
throw new Error("`name` is required.")
|
||||
}
|
||||
|
||||
const author = await authorService.create(req.body)
|
||||
|
||||
res.json({
|
||||
author,
|
||||
})
|
||||
}))
|
||||
|
||||
// update a blog author
|
||||
adminRouter.post(
|
||||
"/authors/:id",
|
||||
wrapHandler(async (req, res) => {
|
||||
const authorService: AuthorService = req.scope.resolve(
|
||||
"authorService"
|
||||
)
|
||||
|
||||
// basic validation of request body
|
||||
if (req.body.id) {
|
||||
throw new Error("Can't update author ID")
|
||||
}
|
||||
|
||||
const author = await authorService.update(
|
||||
req.params.id,
|
||||
req.body
|
||||
)
|
||||
|
||||
res.json({
|
||||
author,
|
||||
})
|
||||
}))
|
||||
|
||||
// delete a blog author
|
||||
adminRouter.delete(
|
||||
"/authors/:id",
|
||||
wrapHandler(async (req, res) => {
|
||||
const authorService: AuthorService = req.scope.resolve(
|
||||
"authorService"
|
||||
)
|
||||
|
||||
await authorService.delete(req.params.id)
|
||||
|
||||
res.status(200).end()
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="store" label="src/api/store.ts">
|
||||
|
||||
```ts
|
||||
import { Router } from "express"
|
||||
import {
|
||||
ConfigModule,
|
||||
} from "@medusajs/medusa/dist/types/global"
|
||||
import PostService from "../services/post"
|
||||
import cors from "cors"
|
||||
import AuthorService from "../services/author"
|
||||
import { wrapHandler } from "@medusajs/medusa"
|
||||
|
||||
export default function storeRoutes(
|
||||
router: Router,
|
||||
options: ConfigModule
|
||||
) {
|
||||
const { projectConfig } = options
|
||||
|
||||
const storeCorsOptions = {
|
||||
origin: projectConfig.store_cors.split(","),
|
||||
credentials: true,
|
||||
}
|
||||
|
||||
const storeRouter = Router()
|
||||
router.use("/store/blog", storeRouter)
|
||||
|
||||
storeRouter.use(cors(storeCorsOptions))
|
||||
|
||||
// list all blog posts
|
||||
storeRouter.get(
|
||||
"/posts",
|
||||
wrapHandler(async (req, res) => {
|
||||
const postService: PostService = req.scope.resolve(
|
||||
"postService"
|
||||
)
|
||||
|
||||
res.json({
|
||||
posts: await postService.list(),
|
||||
})
|
||||
}))
|
||||
|
||||
// retrieve a single blog post
|
||||
storeRouter.get(
|
||||
"/posts/:id",
|
||||
wrapHandler(async (req, res) => {
|
||||
const postService: PostService = req.scope.resolve(
|
||||
"postService"
|
||||
)
|
||||
|
||||
res.json({
|
||||
post: await postService.retrieve(req.params.id),
|
||||
})
|
||||
}))
|
||||
|
||||
// list all blog authors
|
||||
storeRouter.get(
|
||||
"/authors",
|
||||
wrapHandler(async (req, res) => {
|
||||
const authorService: AuthorService = req.scope.resolve(
|
||||
"authorService"
|
||||
)
|
||||
|
||||
res.json({
|
||||
authors: await authorService.list(),
|
||||
})
|
||||
}))
|
||||
|
||||
// retrieve a single blog author
|
||||
storeRouter.get(
|
||||
"/authors/:id",
|
||||
wrapHandler(async (req, res) => {
|
||||
const authorService: AuthorService = req.scope.resolve(
|
||||
"authorService"
|
||||
)
|
||||
|
||||
res.json({
|
||||
post: await authorService.retrieve(req.params.id),
|
||||
})
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Storefront API Reference](/api/store)
|
||||
- [Admin API Reference](/api/admin)
|
||||
@@ -13,7 +13,7 @@ In this document, you’ll see an example of how you can use middlewares and end
|
||||
|
||||
This guide showcases how to register the logged-in admin user, but you can apply the same steps if you want to register the current customer.
|
||||
|
||||
This documentation does not explain the basics of [middlewares](./add-middleware.mdx) and [endpoints](./create.md). You can refer to their respective guides for more details about each.
|
||||
This documentation does not explain the basics of [middlewares](./add-middleware.mdx) and [endpoints](./create.mdx). You can refer to their respective guides for more details about each.
|
||||
|
||||
## Step 1: Create the Middleware
|
||||
|
||||
@@ -87,7 +87,7 @@ For endpoints that require Cross-Origin Resource Origin (CORS) options, such as
|
||||
|
||||
:::tip
|
||||
|
||||
In the above code snippet, the `authenticate` middleware imported from `@medusajs/medusa` is used to ensure that the user is logged in first. If you're implementing this for middleware to register the logged-in customer, make sure to use the [customer's authenticate middleware](./create.md#protect-store-routes).
|
||||
In the above code snippet, the `authenticate` middleware imported from `@medusajs/medusa` is used to ensure that the user is logged in first. If you're implementing this for middleware to register the logged-in customer, make sure to use the [customer's authenticate middleware](./create.mdx#protect-store-routes).
|
||||
|
||||
:::
|
||||
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
---
|
||||
description: 'Learn how to create an entity in Medusa. This guide also explains how to create a repository and access and delete the entity.'
|
||||
addHowToData: true
|
||||
---
|
||||
|
||||
# How to Create an Entity
|
||||
|
||||
In this document, you’ll learn how you can create a custom [Entity](./overview.mdx).
|
||||
|
||||
## Step 1: Create the Entity
|
||||
|
||||
To create an entity, create a TypeScript file in `src/models`. For example, here’s a `Post` entity defined in the file `src/models/post.ts`:
|
||||
|
||||
```ts title=src/models/post.ts
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
} from "typeorm"
|
||||
import { BaseEntity } from "@medusajs/medusa"
|
||||
import { generateEntityId } from "@medusajs/medusa/dist/utils"
|
||||
|
||||
@Entity()
|
||||
export class Post extends BaseEntity {
|
||||
@Column({ type: "varchar" })
|
||||
title: string | null
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert(): void {
|
||||
this.id = generateEntityId(this.id, "post")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This entity has one column `title` defined. However, since it extends `BaseEntity` it will also have the `id`, `created_at`, and `updated_at` columns.
|
||||
|
||||
Medusa’s core entities all have the following format for IDs: `<PREFIX>_<RANDOM>`. For example, an order might have the ID `order_01G35WVGY4D1JCA4TPGVXPGCQM`.
|
||||
|
||||
To generate an ID for your entity that matches the IDs generated for Medusa’s core entities, you should add a `BeforeInsert` event handler. Then, inside that handler use Medusa’s utility function `generateEntityId` to generate the ID. It accepts the ID as a first parameter and the prefix as a second parameter. The `Post` entity IDs will be of the format `post_<RANDOM>`.
|
||||
|
||||
If you want the entity to also be soft deletable then it should extend `SoftDeletableEntity` instead:
|
||||
|
||||
```ts
|
||||
import { SoftDeletableEntity } from "@medusajs/medusa"
|
||||
|
||||
@Entity()
|
||||
export class Post extends SoftDeletableEntity {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
You can learn more about what decorators and column types you can use in [Typeorm’s documentation](https://typeorm.io/entities).
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Create a Migration
|
||||
|
||||
Additionally, you must create a migration for your entity. Migrations are used to update the database schema with new tables or changes to existing tables.
|
||||
|
||||
You can learn more about Migrations, how to create or generate them, and how to run them in the [Migration documentation](./migrations/create.md).
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Create a Repository
|
||||
|
||||
Entities data can be easily accessed and modified using Typeorm [Repositories](https://typeorm.io/working-with-repository). To create a repository, create a file in `src/repositories`. For example, here’s a repository `PostRepository` created in `src/repositories/post.ts`:
|
||||
|
||||
```ts title=src/repositories/post.ts
|
||||
import { Post } from "../models/post"
|
||||
import {
|
||||
dataSource,
|
||||
} from "@medusajs/medusa/dist/loaders/database"
|
||||
|
||||
export const PostRepository = dataSource
|
||||
.getRepository(Post)
|
||||
|
||||
export default PostRepository
|
||||
```
|
||||
|
||||
The repository is created using the `getRepository` method of the data source exported from the core package in Medusa. This method accepts the entity as a parameter.
|
||||
|
||||
:::tip
|
||||
|
||||
A data source is Typeorm’s connection settings that allows you to connect to your database. You can learn more about it in [Typeorm’s documentation](https://typeorm.io/data-source).
|
||||
|
||||
:::
|
||||
|
||||
If you want to add methods to that repository or override Typeorm's Repository methods, you can do that using the `extend` method:
|
||||
|
||||
```ts title=src/repositories/post.ts
|
||||
import { Post } from "../models/post"
|
||||
import {
|
||||
dataSource,
|
||||
} from "@medusajs/medusa/dist/loaders/database"
|
||||
|
||||
export const PostRepository = dataSource
|
||||
.getRepository(Post)
|
||||
.extend({
|
||||
customFunction(): void {
|
||||
// TODO add custom implementation
|
||||
return
|
||||
},
|
||||
})
|
||||
|
||||
export default PostRepository
|
||||
```
|
||||
|
||||
You can learn about available Repository methods in [Typeorm's documentation](https://typeorm.io/repository-api).
|
||||
|
||||
---
|
||||
|
||||
## (Optional) Step 4: Run Migrations
|
||||
|
||||
Before you start using your entity, make sure to run the migrations that reflect the entity on your database schema. If you've already ran migrations, you can skip this step.
|
||||
|
||||
To run migrations, run the `build` command that transpiles your code:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run build
|
||||
```
|
||||
|
||||
Then, run the `migration` command:
|
||||
|
||||
```bash
|
||||
npx medusa migrations run
|
||||
```
|
||||
|
||||
You should see that your migration have executed.
|
||||
|
||||
---
|
||||
|
||||
## Example: Use Entity in Service
|
||||
|
||||
You can access your custom entity data in the database in services or subscribers using the repository. For example, here’s a service that lists all posts:
|
||||
|
||||
```ts
|
||||
import { TransactionBaseService } from "@medusajs/medusa"
|
||||
import PostRepository from "../repositories/post"
|
||||
|
||||
class PostService extends TransactionBaseService {
|
||||
protected readonly postRepository_: typeof PostRepository
|
||||
|
||||
constructor({ postRepository, manager }) {
|
||||
super({ postRepository, manager })
|
||||
|
||||
this.postRepository_ = postRepository
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async list() {
|
||||
const postRepo = this.manager_
|
||||
.withRepository(this.postRepository_)
|
||||
return await postRepo.find()
|
||||
}
|
||||
}
|
||||
|
||||
export default PostService
|
||||
```
|
||||
|
||||
In the constructor, you can use dependency injection to get access to instances of services and repositories. Here, you initialize class fields `postRepository` and `manager`. The `manager` is a [Typeorm Entity Manager](https://typeorm.io/working-with-entity-manager).
|
||||
|
||||
Then, in the method `list`, you can create an instance of the `PostRepository` using the `this.manager_.withRepository` method passing it `this.postRepository` as a parameter.
|
||||
|
||||
After getting an instance of the repository, you can then use [Typeorm’s Repository methods](https://typeorm.io/repository-api) to perform Create, Read, Update, and Delete (CRUD) operations on your entity. You can also use any custom methods that you defined in the Repository.
|
||||
|
||||
:::note
|
||||
|
||||
This same usage of repositories can be done in other resources such as subscribers or endpoints.
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Delete a Soft-Deletable Entity
|
||||
|
||||
To delete soft-deletable entities that extend the `SoftDeletableEntity` class, you can use the repository method `softDelete` method:
|
||||
|
||||
```ts
|
||||
await postRepository.softDelete(post.id)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Entity Definitions
|
||||
|
||||
With entities, you can create relationships, index keys, and more. As Medusa uses Typeorm, you can learn about using these functionalities through [Typeorm's documentation](https://typeorm.io/).
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Extend Entity](./extend-entity.md)
|
||||
- [Create a Plugin](../plugins/create.mdx)
|
||||
182
docs/content/development/entities/create.mdx
Normal file
182
docs/content/development/entities/create.mdx
Normal file
@@ -0,0 +1,182 @@
|
||||
---
|
||||
description: 'Learn how to create an entity in Medusa. This guide also explains how to create a repository and access and delete the entity.'
|
||||
addHowToData: true
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
# How to Create an Entity
|
||||
|
||||
In this document, you’ll learn how you can create a custom [Entity](./overview.mdx).
|
||||
|
||||
## Step 1: Create the Entity
|
||||
|
||||
To create an entity, create a TypeScript file in `src/models`. For example, here’s a `Post` entity defined in the file `src/models/post.ts`:
|
||||
|
||||
```ts title=src/models/post.ts
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
} from "typeorm"
|
||||
import { BaseEntity } from "@medusajs/medusa"
|
||||
import { generateEntityId } from "@medusajs/medusa/dist/utils"
|
||||
|
||||
@Entity()
|
||||
export class Post extends BaseEntity {
|
||||
@Column({ type: "varchar" })
|
||||
title: string | null
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert(): void {
|
||||
this.id = generateEntityId(this.id, "post")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This entity has one column `title` defined. However, since it extends `BaseEntity` it will also have the `id`, `created_at`, and `updated_at` columns.
|
||||
|
||||
Medusa’s core entities all have the following format for IDs: `<PREFIX>_<RANDOM>`. For example, an order might have the ID `order_01G35WVGY4D1JCA4TPGVXPGCQM`.
|
||||
|
||||
To generate an ID for your entity that matches the IDs generated for Medusa’s core entities, you should add a `BeforeInsert` event handler. Then, inside that handler use Medusa’s utility function `generateEntityId` to generate the ID. It accepts the ID as a first parameter and the prefix as a second parameter. The `Post` entity IDs will be of the format `post_<RANDOM>`.
|
||||
|
||||
You can learn more about what decorators and column types you can use in [Typeorm’s documentation](https://typeorm.io/entities).
|
||||
|
||||
### Soft-Deletable Entities
|
||||
|
||||
If you want the entity to also be soft deletable then it should extend `SoftDeletableEntity` instead:
|
||||
|
||||
```ts
|
||||
import { SoftDeletableEntity } from "@medusajs/medusa"
|
||||
|
||||
@Entity()
|
||||
export class Post extends SoftDeletableEntity {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Relations
|
||||
|
||||
Your entity may be related to another entity. You can showcase the relation with [Typeorm's relation decorators](https://typeorm.io/relations).
|
||||
|
||||
For example, you can create another entity `Author` and add a `ManyToOne` relation to it from the `Post`, and a `OneToMany` relation from the `Author` to the `Post`:
|
||||
|
||||
<Tabs groupId="files" isCodeTabs={true}>
|
||||
<TabItem value="post" label="src/models/post.ts" default>
|
||||
|
||||
```ts
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
} from "typeorm"
|
||||
import { BaseEntity } from "@medusajs/medusa"
|
||||
import { generateEntityId } from "@medusajs/medusa/dist/utils"
|
||||
import { Author } from "./author"
|
||||
|
||||
@Entity()
|
||||
export class Post extends BaseEntity {
|
||||
@Column({ type: "varchar" })
|
||||
title: string | null
|
||||
|
||||
@Column({ type: "varchar" })
|
||||
author_id: string
|
||||
|
||||
@ManyToOne(() => Author, (author) => author.posts)
|
||||
author: Author
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert(): void {
|
||||
this.id = generateEntityId(this.id, "post")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="author" label="src/services/author.ts">
|
||||
|
||||
```ts
|
||||
import { BaseEntity, generateEntityId } from "@medusajs/medusa"
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
Entity,
|
||||
OneToMany,
|
||||
} from "typeorm"
|
||||
import { Post } from "./post"
|
||||
|
||||
@Entity()
|
||||
export class Author extends BaseEntity {
|
||||
@Column({ type: "varchar" })
|
||||
name: string
|
||||
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
image?: string
|
||||
|
||||
@OneToMany(() => Post, (post) => post.author)
|
||||
posts: Post[]
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert(): void {
|
||||
this.id = generateEntityId(this.id, "auth")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Adding these relations allows you to later on expand these relations when retrieving records of this entity with repositories.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Create a Migration
|
||||
|
||||
Additionally, you must create a migration for your entity. Migrations are used to update the database schema with new tables or changes to existing tables.
|
||||
|
||||
You can learn more about Migrations, how to create or generate them, and how to run them in the [Migration documentation](./migrations/create.md).
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Create a Repository
|
||||
|
||||
An repository is required for every entity you create. They provide methods to access and modify an entity's records, and they're used by other resources such as services or endpoints to perform those functionalities.
|
||||
|
||||
To learn how to create a repository, refer to the [Repositories](./repositories.md) documentation.
|
||||
|
||||
---
|
||||
|
||||
## (Optional) Step 4: Run Migrations
|
||||
|
||||
Before you start using your entity, make sure to run the migrations that reflect the entity on your database schema. If you've already ran migrations, you can skip this step.
|
||||
|
||||
To run migrations, run the `build` command that transpiles your code:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run build
|
||||
```
|
||||
|
||||
Then, run the `migration` command:
|
||||
|
||||
```bash
|
||||
npx medusa migrations run
|
||||
```
|
||||
|
||||
You should see that your migration have executed.
|
||||
|
||||
---
|
||||
|
||||
## Advanced Entity Definitions
|
||||
|
||||
With entities, you can create relationships, index keys, and more. As Medusa uses Typeorm, you can learn about using these functionalities through [Typeorm's documentation](https://typeorm.io/entities).
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [How to use repositories](./repositories.md)
|
||||
- [Extend an entity](./extend-entity.md)
|
||||
- [Create a plugin](../plugins/create.mdx)
|
||||
@@ -9,75 +9,117 @@ In this document, you’ll learn how to create a [Migration](./overview.mdx) usi
|
||||
|
||||
## Step 1: Create Migration File
|
||||
|
||||
To create a migration that makes changes to your Medusa schema, run the following command:
|
||||
There are two ways to create a migration file: create and write its content manually, or create and generate its content.
|
||||
|
||||
```bash
|
||||
npx typeorm migration:create src/migrations/UserChanged
|
||||
If you're creating a custom entity, then it's recommended to generate the migration file. However, if you're extending an entity from Medusa's core, then you should create and write the migration manually.
|
||||
|
||||
### Option 1: Generate Migration File
|
||||
|
||||
:::warning
|
||||
|
||||
Generating migration files for extended entities may cause unexpected errors. It's highly recommended to write them manually instead.
|
||||
|
||||
:::
|
||||
|
||||
Typeorm provides a `migration:generate` command that allows you to pass it a Typeorm [DataSource](https://typeorm.io/data-source). The `DataSource` includes database connection details, as well as the path to your custom entities.
|
||||
|
||||
Start by creating the file `datasource.js` in the root of your Medusa backend project with the following content:
|
||||
|
||||
```js
|
||||
const { DataSource } = require("typeorm")
|
||||
|
||||
const AppDataSource = new DataSource({
|
||||
type: "postgres",
|
||||
port: 5432,
|
||||
username: "<YOUR_DB_USERNAME>",
|
||||
password: "<YOUR_DB_PASSWORD>",
|
||||
database: "<YOUR_DB_NAME>",
|
||||
entities: [
|
||||
"dist/models/*.js",
|
||||
],
|
||||
migrations: [
|
||||
"dist/migrations/*.js",
|
||||
],
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
datasource: AppDataSource,
|
||||
}
|
||||
```
|
||||
|
||||
This will create the migration file in the path you specify. You can use this without the need to install Typeorm's CLI tool. You can then go ahead and make changes to it as necessary.
|
||||
Make sure to replace `<YOUR_DB_USERNAME>`, `<YOUR_DB_PASSWORD>`, and `<YOUR_DB_NAME>` with the necessary values for your database connection.
|
||||
|
||||
The migration file must be inside the `src/migrations` directory. When you run the build command, it will be transpiled into the directory `dist/migrations`. The `migrations run` command can only pick up migrations under the `dist/migrations` directory on a Medusa backend. This applies to migrations created in a Medusa backend, and not in a Medusa plugin. For plugins, check out the [Plugin's Structure section](../../plugins/create.mdx).
|
||||
Then, after creating your entity, run the `build` command:
|
||||
|
||||
<details>
|
||||
<summary>Generating Migrations for Entities</summary>
|
||||
```bash npm2yarn
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can alternatively use Typeorm's `generate` command to generate a Migration file from existing entity classes. As of v1.8, Medusa uses Typeorm v0.3.x. So, you have to create a [DataSource](https://typeorm.io/data-source) first before using the `migration:generate` command.
|
||||
Finally, run the following command to generate a migration for your custom entity:
|
||||
|
||||
For example, create the file `datasource.js` in the root of your Medusa server with the following content:
|
||||
```bash
|
||||
npx typeorm migration:generate -d datasource.js src/migrations/PostCreate
|
||||
```
|
||||
|
||||
```js
|
||||
const { DataSource } = require("typeorm")
|
||||
|
||||
const AppDataSource = new DataSource({
|
||||
type: "postgres",
|
||||
port: 5432,
|
||||
username: "<YOUR_DB_USERNAME>",
|
||||
password: "<YOUR_DB_PASSWORD>",
|
||||
database: "<YOUR_DB_NAME>",
|
||||
entities: [
|
||||
"dist/models/*.js",
|
||||
],
|
||||
migrations: [
|
||||
"dist/migrations/*.js",
|
||||
],
|
||||
})
|
||||
This will generate the migration file in the path you specify, where `PostCreate` is just an example of the name of the migration to create. The migration file must be inside the `src/migrations` directory. When you run the build command, it will be transpiled into the `dist/migrations` directory.
|
||||
|
||||
module.exports = {
|
||||
datasource: AppDataSource,
|
||||
The `migrations run` command can only pick up migrations under the `dist/migrations` directory on a Medusa backend. This applies to migrations created in a Medusa backend, and not in a Medusa plugin. For plugins, check out the [Plugin's Structure section](../../plugins/create.mdx).
|
||||
|
||||
You can now continue to [step 2](#step-2-build-files) of this guide.
|
||||
|
||||
### Option 2: Write Migration File
|
||||
|
||||
With this option, you'll use Typeorm's CLI tool to create the migration file, but you'll write the content yourself.
|
||||
|
||||
Run the following command in the root directory of your Medusa backend project:
|
||||
|
||||
```bash
|
||||
npx typeorm migration:create src/migrations/PostCreate
|
||||
```
|
||||
|
||||
This will create the migration file in the path you specify, where `PostCreate` is just an example of the name of the migration to create. The migration file must be inside the `src/migrations` directory. When you run the build command, it will be transpiled into the `dist/migrations` directory.
|
||||
|
||||
The `migrations run` command can only pick up migrations under the `dist/migrations` directory on a Medusa backend. This applies to migrations created in a Medusa backend, and not in a Medusa plugin. For plugins, check out the [Plugin's Structure section](../../plugins/create.mdx).
|
||||
|
||||
If you open the file, you'll find `up` and `down` methods. The `up` method is used to reflect the changes on the database. The `down` method is used to revert the changes, which will be executed if the `npx medusa migrations revert` command is used.
|
||||
|
||||
In each of the `up` and `down` methods, you can write the migration either with [SQL syntax](https://www.postgresql.org/docs/current/sql-syntax.html), or using the [migration API](https://typeorm.io/migrations#using-migration-api-to-write-migrations).
|
||||
|
||||
For example:
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
|
||||
```ts
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
|
||||
export class AddAuthorsAndPosts1690876698954 implements MigrationInterface {
|
||||
name = "AddAuthorsAndPosts1690876698954"
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "post" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "title" character varying NOT NULL, "author_id" character varying NOT NULL, "authorId" character varying, CONSTRAINT "PK_be5fda3aac270b134ff9c21cdee" PRIMARY KEY ("id"))`)
|
||||
await queryRunner.query(`CREATE TABLE "author" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" character varying NOT NULL, "image" character varying, CONSTRAINT "PK_5a0e79799d372fe56f2f3fa6871" PRIMARY KEY ("id"))`)
|
||||
await queryRunner.query(`ALTER TABLE "post" ADD CONSTRAINT "FK_c6fb082a3114f35d0cc27c518e0" FOREIGN KEY ("authorId") REFERENCES "author"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`)
|
||||
}
|
||||
```
|
||||
|
||||
Make sure to replace `<YOUR_DB_USERNAME>`, `<YOUR_DB_PASSWORD>`, and `<YOUR_DB_NAME>` with the necessary values for your database connection.
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "post" DROP CONSTRAINT "FK_c6fb082a3114f35d0cc27c518e0"`)
|
||||
await queryRunner.query(`DROP TABLE "author"`)
|
||||
await queryRunner.query(`DROP TABLE "post"`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then, after creating your entity, run the `build` command:
|
||||
:::warning
|
||||
|
||||
```bash npm2yarn
|
||||
npm run build
|
||||
```
|
||||
If you're copying the code snippet above, make sure to not copy the class name or the `name` attribute in it. Your migration should keep its timestamp.
|
||||
|
||||
Finally, run the following command to generate a Migration for your new entity:
|
||||
|
||||
```bash
|
||||
npx typeorm migration:generate -d datasource.js src/migrations/PostCreate
|
||||
```
|
||||
|
||||
Where `PostCreate` is just an example of the name of the migration to generate. The migration will then be generated in `src/migrations/<TIMESTAMP>-PostCreate.ts`. You can then skip to [step 3](#step-3-build-files) of this guide.
|
||||
</details>
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Write Migration File
|
||||
## Step 2: Build Files
|
||||
|
||||
The migration file contains the necessary commands to create the database columns, foreign keys, and more.
|
||||
|
||||
You can learn more about writing the migration file in You can learn more about writing migrations in [Typeorm’s Documentation](https://typeorm.io/migrations#using-migration-api-to-write-migrations).
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Build Files
|
||||
|
||||
Before you can run the migrations you need to run the build command to transpile the TypeScript files to JavaScript files:
|
||||
Before you can run the migrations, you need to run the build command to transpile the TypeScript files to JavaScript files:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run build
|
||||
|
||||
@@ -15,7 +15,7 @@ Entities in medusa represent tables in the database as classes. An example of th
|
||||
|
||||
Aside from the entities in the Medusa core package, you can also create custom entities to use in your Medusa backend. Custom entities are TypeScript or JavaScript files located in the `src/models` directory of your Medusa backend. You then transpile these entities to be used during the backend's runtime using the `build` command, which moves them to the `dist/models` directory.
|
||||
|
||||
Entities are TypeScript files and they are based on [Typeorm’s Entities](https://typeorm.io/entities) and use Typeorm decorators.
|
||||
Entities are based on [Typeorm’s Entities](https://typeorm.io/entities) and use Typeorm decorators. Each entity also require a [repository](./repositories.md) to be created. A repository provides basic methods to access and manipulate the entity's data.
|
||||
|
||||
---
|
||||
|
||||
@@ -68,19 +68,11 @@ If you want to remove a property from the `metadata` object, you can pass the `m
|
||||
|
||||
---
|
||||
|
||||
## What are Repositories
|
||||
|
||||
Repositories provide generic helper methods for entities. For example, a `list` method to retrieve all entities with pagination, or `retrieve` to retrieve a single entity record.
|
||||
|
||||
Repostories are [Typeorm repositories](https://typeorm.io/working-with-repository), so you can refer to Typeorm's documentation on all available methods.
|
||||
|
||||
---
|
||||
|
||||
## Custom Development
|
||||
|
||||
Developers can create custom entities in the Medusa backend, a plugin, or in a module, then ensure it reflects in the database using a migration.
|
||||
|
||||
<DocCardList colSize={6} items={[
|
||||
<DocCardList colSize={4} items={[
|
||||
{
|
||||
type: 'link',
|
||||
href: '/development/entities/create',
|
||||
@@ -90,6 +82,15 @@ Developers can create custom entities in the Medusa backend, a plugin, or in a m
|
||||
description: 'Learn how to create an entity in Medusa.'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
href: '/development/entities/repositories',
|
||||
label: 'Create a Repository',
|
||||
customProps: {
|
||||
icon: Icons['academic-cap-solid'],
|
||||
description: 'Learn how to use a repository in Medusa.'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
href: '/development/entities/migrations/create',
|
||||
|
||||
308
docs/content/development/entities/repositories.md
Normal file
308
docs/content/development/entities/repositories.md
Normal file
@@ -0,0 +1,308 @@
|
||||
---
|
||||
description: "In this document, you'll learn what repositories are, how to use them within your Medusa backend, and what are some of their common methods."
|
||||
---
|
||||
|
||||
# Repositories
|
||||
|
||||
In this document, you'll learn what repositories are, how to use them within your Medusa backend, and what are some of their common methods.
|
||||
|
||||
## Overview
|
||||
|
||||
Repositories provide generic helper methods for entities. For example, a `find` method to retrieve all entities with pagination, or `findOne` to retrieve a single entity record.
|
||||
|
||||
Repostories are [Typeorm repositories](https://typeorm.io/working-with-repository), so you can refer to Typeorm's documentation on all available methods.
|
||||
|
||||
This guide provides some basic methods you'll need during your custom development with Medusa.
|
||||
|
||||
---
|
||||
|
||||
## Basic Implementation
|
||||
|
||||
Each entity you create needs a repository. A repository is created under the `src/repositories` directory of your Medusa backend project. The file name is the name of the repository without `Repository`.
|
||||
|
||||
For example, to create a repository for a `Post` entity, create the file `src/repositories/post.ts` with the following content:
|
||||
|
||||
```ts src=src/repositories/post.ts
|
||||
import { Post } from "../models/post"
|
||||
import {
|
||||
dataSource,
|
||||
} from "@medusajs/medusa/dist/loaders/database"
|
||||
|
||||
export const PostRepository = dataSource
|
||||
.getRepository(Post)
|
||||
|
||||
export default PostRepository
|
||||
```
|
||||
|
||||
The repository is created using the `getRepository` method of the data source exported from the core package in Medusa. This method accepts the entity as a parameter.
|
||||
|
||||
:::tip
|
||||
|
||||
A data source is Typeorm’s connection settings that allows you to connect to your database. You can learn more about it in [Typeorm’s documentation](https://typeorm.io/data-source).
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Customizing a Repository
|
||||
|
||||
If you want to add methods to the repository or override Typeorm's Repository methods, you can do that using the `extend` method:
|
||||
|
||||
```ts title=src/repositories/post.ts
|
||||
import { Post } from "../models/post"
|
||||
import {
|
||||
dataSource,
|
||||
} from "@medusajs/medusa/dist/loaders/database"
|
||||
|
||||
export const PostRepository = dataSource
|
||||
.getRepository(Post)
|
||||
.extend({
|
||||
customFunction(): void {
|
||||
// TODO add custom implementation
|
||||
return
|
||||
},
|
||||
})
|
||||
|
||||
export default PostRepository
|
||||
```
|
||||
|
||||
You can learn about available Repository methods in [Typeorm's documentation](https://typeorm.io/repository-api).
|
||||
|
||||
---
|
||||
|
||||
## Using Repositories in Other Resources
|
||||
|
||||
When you want to perform an action or use on an entity in one of your custom resources, such as an endpoint or a service, you need to use its repository.
|
||||
|
||||
### Endpoints
|
||||
|
||||
To access a repository within an endpoint, use the `req.scope.resolve` method. For example:
|
||||
|
||||
```ts
|
||||
import { PostRepository } from "../repositories/post"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
// ...
|
||||
|
||||
export default () => {
|
||||
// ...
|
||||
|
||||
storeRouter.get("/posts", async (req, res) => {
|
||||
const postRepository: typeof PostRepository =
|
||||
req.scope.resolve("postRepository")
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
const postRepo = manager.withRepository(postRepository)
|
||||
|
||||
return res.json({
|
||||
posts: await postRepo.find(),
|
||||
})
|
||||
})
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
You can learn more about endpoints [here](../endpoints/overview.mdx).
|
||||
|
||||
### Services and Subscribers
|
||||
|
||||
As repositories are registered in the [dependency container](../fundamentals/dependency-injection.md#dependency-container-and-injection), they can be accessed through dependency injection in the constructor of a service or a subscriber.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title=/src/services/post.ts
|
||||
import { PostRepository } from "../repositories/post"
|
||||
|
||||
class PostService extends TransactionBaseService {
|
||||
// ...
|
||||
protected postRepository_: typeof PostRepository
|
||||
|
||||
constructor(container) {
|
||||
super(container)
|
||||
// ...
|
||||
this.postRepository_ = container.postRepository
|
||||
}
|
||||
|
||||
async list(): Promise<Post[]> {
|
||||
const postRepo = this.activeManager_.withRepository(
|
||||
this.postRepository_
|
||||
)
|
||||
return await postRepo.find()
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
You can learn more about services [here](../services/overview.mdx).
|
||||
|
||||
### Other Resources
|
||||
|
||||
Resources that have access to the dependency container can access repositories just like any other resources. You can learn more about the dependency container and dependency injection in [this documentation](../fundamentals/dependency-injection.md).
|
||||
|
||||
---
|
||||
|
||||
## Common Methods
|
||||
|
||||
This section covers some common methods and use cases you'll use with repositories. You can refer to [Typeorm's documentation](https://typeorm.io/repository-api) for full details on available methods.
|
||||
|
||||
### Retrieving a List of Records
|
||||
|
||||
To retrieve a list of records of an entity, use the `find` method:
|
||||
|
||||
```ts
|
||||
const posts = await postRepository.find()
|
||||
```
|
||||
|
||||
You can also filter the retrieved items by passing an object of type [FindOption](https://typeorm.io/find-options) as a first parameter:
|
||||
|
||||
```ts
|
||||
const posts = await postRepository.find({
|
||||
where: {
|
||||
id: "1",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
In addition, you can pass `skip` and `take` properties to the object for pagination purposes. `skip`'s value is a number that indicates how many items to skip before retrieving the results, and `take` indicates how many items to return:
|
||||
|
||||
```ts
|
||||
const posts = await postRepository.find({
|
||||
skip: 0,
|
||||
take: 20,
|
||||
})
|
||||
```
|
||||
|
||||
To expand relations and retrieve them as part of each item in the result, you can pass the `relations` property to the parameter object:
|
||||
|
||||
```ts
|
||||
const posts = await postRepository.find({
|
||||
relations: ["authors"],
|
||||
})
|
||||
```
|
||||
|
||||
Medusa provides a utility method `buildQuery` that allows you to easily format the object to pass to the `find` method. `buildQuery` accepts two parameters:
|
||||
|
||||
1. The first parameter is an object whose keys are the attributes of the entity, and their values are the value to filter by.
|
||||
2. The second parameter includes the options related to pagination (such as `skip` and `take`), the relations to expand, and fields to select in each returned item.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { buildQuery } from "@medusajs/medusa"
|
||||
|
||||
// ...
|
||||
const selector = {
|
||||
id: "1",
|
||||
}
|
||||
|
||||
const config = {
|
||||
skip: 0,
|
||||
take: 20,
|
||||
relations: ["authors"],
|
||||
select: ["title"],
|
||||
}
|
||||
|
||||
const query = buildQuery(selector, config)
|
||||
|
||||
const posts = await postRepository.find(query)
|
||||
```
|
||||
|
||||
### Retrieving a List of Records with Count
|
||||
|
||||
You can retrieve a list of records along with their count using the `findAndCount` method:
|
||||
|
||||
```ts
|
||||
const [posts, count] = await postRepository.findAndCount()
|
||||
```
|
||||
|
||||
This method also accepts the same options object as a parameter similar to the [find](#retrieving-a-list-of-records) method.
|
||||
|
||||
### Retrieving a Single Record
|
||||
|
||||
You can retrieve one record of an entity using the `findOne` method:
|
||||
|
||||
```ts
|
||||
const post = await postRepository.findOne({
|
||||
where: {
|
||||
id: "1",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
If the record does not exist, `null` will be returned instead.
|
||||
|
||||
You can also pass the method an options object similar to the [find](#retrieving-a-list-of-records) method to expand relations or specify what fields to select.
|
||||
|
||||
### Create Record
|
||||
|
||||
To create a new record of an entity, use the `create` and `save` methods of the repository:
|
||||
|
||||
```ts
|
||||
const post = postRepository.create()
|
||||
post.title = data.title
|
||||
post.author_id = data.author_id
|
||||
const result = await postRepository.save(post)
|
||||
```
|
||||
|
||||
The `save` method is what actually persists the created record in the database.
|
||||
|
||||
### Update Record
|
||||
|
||||
To update a record of an entity, use the `save` method of the repository:
|
||||
|
||||
```ts
|
||||
// const data = {
|
||||
// title: ''
|
||||
// }
|
||||
|
||||
// const post = await postRepository.findOne({
|
||||
// where: {
|
||||
// id: '1'
|
||||
// }
|
||||
// })
|
||||
|
||||
Object.assign(post, data)
|
||||
|
||||
const updatedPost = await postRepository.save(post)
|
||||
```
|
||||
|
||||
### Delete a Record
|
||||
|
||||
To delete a record of an entity, use the `remove` method of the repository:
|
||||
|
||||
```ts
|
||||
const post = await postRepository.findOne({
|
||||
where: {
|
||||
id: "1",
|
||||
},
|
||||
})
|
||||
|
||||
await postRepository.remove([post])
|
||||
```
|
||||
|
||||
This method accepts an array of records to delete.
|
||||
|
||||
### Soft Delete a Record
|
||||
|
||||
If an entity extends the `SoftDeletableEntity` class, it can be soft deleted. This means that the entity won't be fully deleted from the database, but it can't be retrieved as a non-deleted entity would.
|
||||
|
||||
To soft-delete a record of an entity, use the `softRemove` method:
|
||||
|
||||
```ts
|
||||
const post = await postRepository.findOne({
|
||||
where: {
|
||||
id: "1",
|
||||
},
|
||||
})
|
||||
|
||||
await postRepository.softRemove([post])
|
||||
```
|
||||
|
||||
You can later retrieve that entity by passing the `withDeleted` option to methods like `find`, `findAndCount`, or `findOne`:
|
||||
|
||||
```ts
|
||||
const posts = await postRepository.find({
|
||||
withDeleted: true,
|
||||
})
|
||||
```
|
||||
@@ -5,33 +5,156 @@ addHowToData: true
|
||||
|
||||
import Troubleshooting from '@site/src/components/Troubleshooting'
|
||||
import ServiceLifetimeSection from '../../troubleshooting/awilix-resolution-error/_service-lifetime.md'
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
# How to Create a Service
|
||||
|
||||
In this document, you’ll learn how you can create a [Service](./overview.mdx) and use it across your Medusa backend just like any of the core services.
|
||||
|
||||
## Service Implementation
|
||||
## Basic Service Implementation
|
||||
|
||||
To create a service, create a TypeScript or JavaScript file in `src/services` to hold the service. The name of the file should be the name of the service without `Service`. This is essential as the file name is used when registering the service in the [dependency container](../fundamentals/dependency-injection.md), and `Service` is appended to the camel-case version of the file name automatically.
|
||||
|
||||
For example, if you want to create a service `helloService`, create the file `hello.ts` in `src/services` with the following content:
|
||||
For example, if you want to create a service `PostService`, eventually registered as `postService`, create the file `post.ts` in `src/services` with the following content:
|
||||
|
||||
```ts title=/src/services/hello.ts
|
||||
```ts title=/src/services/post.ts
|
||||
import { TransactionBaseService } from "@medusajs/medusa"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
class HelloService extends TransactionBaseService {
|
||||
protected manager_: EntityManager
|
||||
protected transactionManager_: EntityManager
|
||||
class PostService extends TransactionBaseService {
|
||||
getMessage() {
|
||||
return `Welcome to My Store!`
|
||||
}
|
||||
}
|
||||
|
||||
export default HelloService
|
||||
export default PostService
|
||||
```
|
||||
|
||||
This service will be registered in the dependency container as `helloService`.
|
||||
This service will be registered in the [dependency container](../fundamentals/dependency-injection.md) as `postService`. It contains a single sample method `getMessage`.
|
||||
|
||||
---
|
||||
|
||||
## Build Files
|
||||
|
||||
Custom services must be transpiled and moved to the `dist` directory before you can start consuming them. When you run your backend using either the `medusa develop` or `npx medusa develop` commands, it watches the files under `src` for any changes, then triggers the `build` command and restarts the server.
|
||||
|
||||
However, the build isn't triggered when the backend first starts running, and it's never triggered when the `medusa start` or `npx medusa start` commands are used.
|
||||
|
||||
So, make sure to run the `build` command before starting the backend:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Service Constructor
|
||||
|
||||
As the service extends the `TransactionBaseService` class, all resources registered in the dependency container can be accessed through [dependency injection](../fundamentals/dependency-injection.md). This includes services, repositories, and other resources in the Medusa core, as well as your custom services and resources.
|
||||
|
||||
So, if you want your service to use another service, add it as part of your constructor’s dependencies and set it to a field inside your service’s class:
|
||||
|
||||
```ts title=/src/services/post.ts
|
||||
import { ProductService } from "@medusajs/medusa"
|
||||
import { PostRepository } from "../repositories/post"
|
||||
|
||||
class PostService extends TransactionBaseService {
|
||||
private productService: ProductService
|
||||
|
||||
constructor(container) {
|
||||
super(container)
|
||||
this.productService = container.productService
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Then, you can use that service anywhere in your custom service. For example:
|
||||
|
||||
```ts title=/src/services/post.ts
|
||||
class PostService extends TransactionBaseService {
|
||||
// ...
|
||||
async getProductCount() {
|
||||
return await this.productService.count()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use Repositories
|
||||
|
||||
As your service provides helper methods related to one or more entities in your backend, you'll need to perform operations on that entity. To do that, you need to use the entity's repository.
|
||||
|
||||
Repositories, just like services, are registered in the dependency container and can be accessed with [dependency injection](../fundamentals/dependency-injection.md).
|
||||
|
||||
However, to actually get an instance of the repository within the service's methods, you need to use the service's `activeManager_`, which is declared in the parent class `TransactionBaseService`. `activeManager_` is an instance of Typeorm's `EntityManager` and has a method `withRepository` which allows you to retrieve an instance of the repository.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title=/src/services/post.ts
|
||||
import { PostRepository } from "../repositories/post"
|
||||
|
||||
class PostService extends TransactionBaseService {
|
||||
// ...
|
||||
protected postRepository_: typeof PostRepository
|
||||
|
||||
constructor(container) {
|
||||
super(container)
|
||||
// ...
|
||||
this.postRepository_ = container.postRepository
|
||||
}
|
||||
|
||||
async list(): Promise<Post[]> {
|
||||
const postRepo = this.activeManager_.withRepository(
|
||||
this.postRepository_
|
||||
)
|
||||
return await postRepo.find()
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Refer to the [repositories](../entities/repositories.md) documentation to learn about its different methods.
|
||||
|
||||
---
|
||||
|
||||
## Transactions
|
||||
|
||||
Transactions ensure that when an error occurs, all data manipulation within the transaction is reverted. As services are likely to include methods that manipulate data, such as create or update a post, it's very useful to wrap that logic within a transaction block.
|
||||
|
||||
Since services extend the `TransactionBaseService` class, you can use its `atomicPhase_` method. The `atomicPhase_` method allows you to wrap code within a transactional block.
|
||||
|
||||
It accepts as a parameter a function, which includes the logic to be performed inside the transactional block. The function accepts as a parameter a transaction manager, which is Typeorm's `EntityManager`. You can use it within the function to retrieve repositories, among other functionalities.
|
||||
|
||||
The data returned by the function passed as a parameter to the `atomicPhase_` method will be return by the `atomicPhase_` method as well.
|
||||
|
||||
For example, the `PostService`'s `create` method with the `atomicPhase_` method:
|
||||
|
||||
```ts title=/src/services/post.ts
|
||||
class PostService extends TransactionBaseService {
|
||||
protected postRepository_: typeof PostRepository
|
||||
// ...
|
||||
async create(
|
||||
data: Pick<Post, "title" | "author_id">
|
||||
): Promise<Post> {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const postRepo = manager.withRepository(
|
||||
this.postRepository_
|
||||
)
|
||||
const post = postRepo.create()
|
||||
|
||||
post.title = data.title
|
||||
post.author_id = data.author_id
|
||||
|
||||
const result = await postRepo.save(post)
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -47,11 +170,11 @@ There are three lifetime types:
|
||||
|
||||
You can set the lifetime of your service by setting the `LIFE_TIME` static property:
|
||||
|
||||
```ts title=/src/services/hello.ts
|
||||
```ts title=/src/services/post.ts
|
||||
import { TransactionBaseService } from "@medusajs/medusa"
|
||||
import { Lifetime } from "awilix"
|
||||
|
||||
class HelloService extends TransactionBaseService {
|
||||
class PostService extends TransactionBaseService {
|
||||
static LIFE_TIME = Lifetime.SCOPED
|
||||
|
||||
// ...
|
||||
@@ -60,58 +183,24 @@ class HelloService extends TransactionBaseService {
|
||||
|
||||
---
|
||||
|
||||
## Service Constructor
|
||||
|
||||
As the service extends the `TransactionBaseService` class, all services in Medusa’s core, as well as all your custom services, will be available in your service’s constructor using dependency injection.
|
||||
|
||||
So, if you want your service to use another service, simply add it as part of your constructor’s dependencies and set it to a field inside your service’s class:
|
||||
|
||||
```ts
|
||||
class HelloService extends TransactionBaseService {
|
||||
private productService: ProductService
|
||||
|
||||
constructor(container) {
|
||||
super(container)
|
||||
this.productService = container.productService
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Then, you can use that service anywhere in your custom service:
|
||||
|
||||
```ts
|
||||
class HelloService extends TransactionBaseService {
|
||||
// ...
|
||||
async getProductCount() {
|
||||
return await this.productService.count()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Retrieve Medusa Configurations
|
||||
|
||||
Within your service, you may need to access the Medusa configuration exported from `medusa-config.js`. To do that, you can access `configModule` using dependency injection.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
```ts title=/src/services/post.ts
|
||||
import {
|
||||
ConfigModule,
|
||||
TransactionBaseService,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
type InjectedDependencies = {
|
||||
configModule: ConfigModule
|
||||
}
|
||||
|
||||
class HelloService extends TransactionBaseService {
|
||||
class PostService extends TransactionBaseService {
|
||||
protected readonly configModule_: ConfigModule
|
||||
|
||||
constructor(container: InjectedDependencies) {
|
||||
constructor(container) {
|
||||
super(container)
|
||||
// ...
|
||||
this.configModule_ = container.configModule
|
||||
}
|
||||
|
||||
@@ -122,7 +211,145 @@ class HelloService extends TransactionBaseService {
|
||||
// ...
|
||||
}
|
||||
|
||||
export default HelloService
|
||||
export default PostService
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pagination, Filtering, and Relations
|
||||
|
||||
Often, your service will provide methods that retrieve a list of items, which can be used by endpoints. In these methods, it can be helpful to provide filtering and pagination utilities that can be used by endpoints or any other resources utilitizing this service.
|
||||
|
||||
The `@medusajs/medusa` package provides the following generic types that you can use to create the signature of your method that accepts filtering and pagination parameters:
|
||||
|
||||
1. `Selector`: Can be used to accepts the attributes of an entity as possible filtering parameters, based on each attribute's type.
|
||||
2. `FindConfig`: Can be used to provide pagination parameters such as `skip`, `take`, and `relations`. `skip` indicates how many items to skip before retrieving the results, `take` indicates how many results to return, and `relations`, indicate which relations to expand and include in the returned objects.
|
||||
|
||||
The `@medusajs/medusa` package also provides a `buildQuery` method that allows you to pass two parameter, the first of type `Selector` and the second of type `FindConfig`, to build the object that should be passed to the repository.
|
||||
|
||||
So, for example, to create a method that retrieves a list of posts and the total number of posts available:
|
||||
|
||||
```ts title=src/services/post.ts
|
||||
import {
|
||||
FindConfig,
|
||||
Selector,
|
||||
TransactionBaseService,
|
||||
buildQuery,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
class PostService extends TransactionBaseService {
|
||||
// ...
|
||||
async listAndCount(
|
||||
selector?: Selector<Post>,
|
||||
config: FindConfig<Post> = {
|
||||
skip: 0,
|
||||
take: 20,
|
||||
relations: [],
|
||||
}): Promise<[Post[], number]> {
|
||||
const postRepo = this.activeManager_.withRepository(
|
||||
this.postRepository_
|
||||
)
|
||||
|
||||
const query = buildQuery(selector, config)
|
||||
|
||||
return postRepo.findAndCount(query)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In addition, you can expand relations when retrieving a single item with the help of `FindConfig` and `buildQuery`.
|
||||
|
||||
For example, to create a method that retrieves a single post:
|
||||
|
||||
```ts title=src/services/post.ts
|
||||
import {
|
||||
FindConfig,
|
||||
TransactionBaseService,
|
||||
buildQuery,
|
||||
} from "@medusajs/medusa"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
|
||||
class PostService extends TransactionBaseService {
|
||||
// ...
|
||||
|
||||
async retrieve(
|
||||
id: string,
|
||||
config?: FindConfig<Post>
|
||||
): Promise<Post> {
|
||||
const postRepo = this.activeManager_.withRepository(
|
||||
this.postRepository_
|
||||
)
|
||||
|
||||
const query = buildQuery({
|
||||
id,
|
||||
}, config)
|
||||
|
||||
const post = await postRepo.findOne(query)
|
||||
|
||||
if (!post) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
"Post was not found"
|
||||
)
|
||||
}
|
||||
|
||||
return post
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then, any other resources such as endpoints or services that use this method can pass what relations to expand in the next parameter:
|
||||
|
||||
```ts
|
||||
await postService.retrieve(id, {
|
||||
relations: ["authors"],
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Throwing Errors
|
||||
|
||||
When you need to throw errors in your service methods, it's recommended to use `MedusaError` imported from `@medusajs/util`. That way, when an error is thrown within a request, the error will be returned in the response in a consistent format as Medusa's errors.
|
||||
|
||||
:::note
|
||||
|
||||
This assumes you're handling errors in your custom endpoints as explained [here](../endpoints/create.mdx#handle-errors).
|
||||
|
||||
:::
|
||||
|
||||
For example:
|
||||
|
||||
```ts title=src/services/post.ts
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
|
||||
class PostService extends TransactionBaseService {
|
||||
// ...
|
||||
|
||||
async retrieve(
|
||||
id: string,
|
||||
config?: FindConfig<Post>
|
||||
): Promise<Post> {
|
||||
const postRepo = this.activeManager_.withRepository(
|
||||
this.postRepository_
|
||||
)
|
||||
|
||||
const query = buildQuery({
|
||||
id,
|
||||
}, config)
|
||||
|
||||
const post = await postRepo.findOne(query)
|
||||
|
||||
if (!post) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
"Post was not found"
|
||||
)
|
||||
}
|
||||
|
||||
return post
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
@@ -133,11 +360,7 @@ In this section, you'll learn how to use services throughout your Medusa backend
|
||||
|
||||
:::note
|
||||
|
||||
Before using your service, make sure you run the `build` command:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run build
|
||||
```
|
||||
Before using your service, make sure you run the [build command](#build-files).
|
||||
|
||||
:::
|
||||
|
||||
@@ -149,7 +372,7 @@ To use your custom service in another custom service, you can have easy access t
|
||||
class MyService extends TransactionBaseService {
|
||||
constructor(container) {
|
||||
super(container)
|
||||
this.helloService = container.helloService
|
||||
this.postService = container.postService
|
||||
}
|
||||
// ...
|
||||
}
|
||||
@@ -160,10 +383,10 @@ class MyService extends TransactionBaseService {
|
||||
To use your custom service in an endpoint, you can use `req.scope.resolve` passing it the service’s registration name:
|
||||
|
||||
```ts
|
||||
const helloService = req.scope.resolve("helloService")
|
||||
const postService = req.scope.resolve("postService")
|
||||
|
||||
res.json({
|
||||
message: helloService.getMessage(),
|
||||
posts: postService.list(),
|
||||
})
|
||||
```
|
||||
|
||||
@@ -173,8 +396,8 @@ To use your custom service in a subscriber, you can have easy access to it in th
|
||||
|
||||
```ts
|
||||
class MySubscriber {
|
||||
constructor({ helloService, eventBusService }) {
|
||||
this.helloService = helloService
|
||||
constructor({ postService, eventBusService }) {
|
||||
this.postService = postService
|
||||
}
|
||||
// ...
|
||||
}
|
||||
@@ -195,6 +418,260 @@ class MySubscriber {
|
||||
|
||||
---
|
||||
|
||||
## Example: Services with CRUD Operations
|
||||
|
||||
In this section, you'll find a full example of the `PostService` and `AuthorService` that implement Create, Read, Update, and Delete (CRUD) operations.
|
||||
|
||||
You can refer to the [Entities](../entities/create.mdx#adding-relations) documentation to learn how to create the custom entities used in this example.
|
||||
|
||||
<Tabs groupId="files" isCodeTabs={true}>
|
||||
<TabItem value="post" label="src/services/post.ts" default>
|
||||
|
||||
```ts
|
||||
import {
|
||||
FindConfig,
|
||||
Selector,
|
||||
TransactionBaseService,
|
||||
buildQuery,
|
||||
} from "@medusajs/medusa"
|
||||
import { PostRepository } from "../repositories/post"
|
||||
import { Post } from "../models/post"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
|
||||
class PostService extends TransactionBaseService {
|
||||
protected postRepository_: typeof PostRepository
|
||||
|
||||
constructor(container) {
|
||||
super(container)
|
||||
this.postRepository_ = container.postRepository
|
||||
}
|
||||
|
||||
async listAndCount(
|
||||
selector?: Selector<Post>,
|
||||
config: FindConfig<Post> = {
|
||||
skip: 0,
|
||||
take: 20,
|
||||
relations: [],
|
||||
}): Promise<[Post[], number]> {
|
||||
const postRepo = this.activeManager_.withRepository(
|
||||
this.postRepository_
|
||||
)
|
||||
|
||||
const query = buildQuery(selector, config)
|
||||
|
||||
return postRepo.findAndCount(query)
|
||||
}
|
||||
|
||||
async list(
|
||||
selector?: Selector<Post>,
|
||||
config: FindConfig<Post> = {
|
||||
skip: 0,
|
||||
take: 20,
|
||||
relations: [],
|
||||
}): Promise<Post[]> {
|
||||
const [posts] = await this.listAndCount(selector, config)
|
||||
|
||||
return posts
|
||||
}
|
||||
|
||||
async retrieve(
|
||||
id: string,
|
||||
config?: FindConfig<Post>
|
||||
): Promise<Post> {
|
||||
const postRepo = this.activeManager_.withRepository(
|
||||
this.postRepository_
|
||||
)
|
||||
|
||||
const query = buildQuery({
|
||||
id,
|
||||
}, config)
|
||||
|
||||
const post = await postRepo.findOne(query)
|
||||
|
||||
if (!post) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
"Post was not found"
|
||||
)
|
||||
}
|
||||
|
||||
return post
|
||||
}
|
||||
|
||||
async create(
|
||||
data: Pick<Post, "title" | "author_id">
|
||||
): Promise<Post> {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const postRepo = manager.withRepository(
|
||||
this.postRepository_
|
||||
)
|
||||
const post = postRepo.create()
|
||||
post.title = data.title
|
||||
post.author_id = data.author_id
|
||||
const result = await postRepo.save(post)
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
data: Omit<Partial<Post>, "id">
|
||||
): Promise<Post> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const postRepo = manager.withRepository(
|
||||
this.postRepository_
|
||||
)
|
||||
const post = await this.retrieve(id)
|
||||
|
||||
Object.assign(post, data)
|
||||
|
||||
return await postRepo.save(post)
|
||||
})
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const postRepo = manager.withRepository(
|
||||
this.postRepository_
|
||||
)
|
||||
const post = await this.retrieve(id)
|
||||
|
||||
await postRepo.remove([post])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default PostService
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="author" label="src/services/author.ts">
|
||||
|
||||
```ts
|
||||
import {
|
||||
FindConfig,
|
||||
Selector,
|
||||
TransactionBaseService,
|
||||
buildQuery,
|
||||
} from "@medusajs/medusa"
|
||||
import { EntityManager } from "typeorm"
|
||||
import AuthorRepository from "../repositories/author"
|
||||
import { Author } from "../models/author"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
|
||||
class AuthorService extends TransactionBaseService {
|
||||
protected manager_: EntityManager
|
||||
protected transactionManager_: EntityManager
|
||||
protected authorRepository_: typeof AuthorRepository
|
||||
|
||||
constructor(container) {
|
||||
super(container)
|
||||
this.authorRepository_ = container.authorRepository
|
||||
}
|
||||
|
||||
async listAndCount(
|
||||
selector?: Selector<Author>,
|
||||
config: FindConfig<Author> = {
|
||||
skip: 0,
|
||||
take: 20,
|
||||
relations: [],
|
||||
}): Promise<[Author[], number]> {
|
||||
const authorRepo = this.activeManager_.withRepository(
|
||||
this.authorRepository_
|
||||
)
|
||||
|
||||
const query = buildQuery(selector, config)
|
||||
|
||||
return authorRepo.findAndCount(query)
|
||||
}
|
||||
|
||||
async list(
|
||||
selector?: Selector<Author>,
|
||||
config: FindConfig<Author> = {
|
||||
skip: 0,
|
||||
take: 20,
|
||||
relations: [],
|
||||
}): Promise<Author[]> {
|
||||
const [authors] = await this.listAndCount(selector, config)
|
||||
|
||||
return authors
|
||||
}
|
||||
|
||||
async retrieve(
|
||||
id: string,
|
||||
config?: FindConfig<Author>
|
||||
): Promise<Author> {
|
||||
const authorRepo = this.activeManager_.withRepository(
|
||||
this.authorRepository_
|
||||
)
|
||||
|
||||
const query = buildQuery({
|
||||
id,
|
||||
}, config)
|
||||
|
||||
const author = await authorRepo.findOne(query)
|
||||
|
||||
if (!author) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
"Author was not found"
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
async create(
|
||||
data: Pick<Author, "name" | "image">
|
||||
): Promise<Author> {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const authorRepo = manager.withRepository(
|
||||
this.authorRepository_
|
||||
)
|
||||
const post = authorRepo.create(data)
|
||||
const result = await authorRepo.save(post)
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
data: Omit<Partial<Author>, "id">
|
||||
): Promise<Author> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const authorRepo = manager.withRepository(
|
||||
this.authorRepository_
|
||||
)
|
||||
const post = await this.retrieve(id)
|
||||
|
||||
Object.assign(post, data)
|
||||
|
||||
return await authorRepo.save(post)
|
||||
})
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const authorRepo = manager.withRepository(
|
||||
this.authorRepository_
|
||||
)
|
||||
const post = await this.retrieve(id)
|
||||
|
||||
await authorRepo.remove([post])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthorService
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Create a Plugin](../plugins/create.mdx)
|
||||
|
||||
@@ -1248,6 +1248,11 @@ module.exports = {
|
||||
id: "development/entities/create",
|
||||
label: "Create an Entity",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
id: "development/entities/repositories",
|
||||
label: "Use a Repository",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
id: "development/entities/extend-entity",
|
||||
|
||||
Reference in New Issue
Block a user