api-ref: custom API reference (#4770)
* initialized next.js project * finished markdown sections * added operation schema component * change page metadata * eslint fixes * fixes related to deployment * added response schema * resolve max stack issue * support for different property types * added support for property types * added loading for components * added more loading * type fixes * added oneOf type * removed console * fix replace with push * refactored everything * use static content for description * fixes and improvements * added code examples section * fix path name * optimizations * fixed tag navigation * add support for admin and store references * general enhancements * optimizations and fixes * fixes and enhancements * added search bar * loading enhancements * added loading * added code blocks * added margin top * add empty response text * fixed oneOf parameters * added path and query parameters * general fixes * added base path env variable * small fix for arrays * enhancements * design enhancements * general enhancements * fix isRequired * added enum values * enhancements * general fixes * general fixes * changed oas generation script * additions to the introduction section * added copy button for code + other enhancements * fix response code block * fix metadata * formatted store introduction * move sidebar logic to Tags component * added test env variables * fix code block bug * added loading animation * added expand param + loading * enhance operation loading * made responsive + improvements * added loading provider * fixed loading * adjustments for small devices * added sidebar label for endpoints * added feedback component * fixed analytics * general fixes * listen to scroll for other headings * added sample env file * update api ref files + support new fields * fix for external docs link * added new sections * fix last item in sidebar not showing * move docs content to www/docs * change redirect url * revert change * resolve build errors * configure rewrites * changed to environment variable url * revert changing environment variable name * add environment variable for API path * fix links * fix tailwind settings * remove vercel file * reconfigured api route * move api page under api * fix page metadata * fix external link in navigation bar * update api spec * updated api specs * fixed google lint error * add max-height on request samples * add padding before loading * fix for one of name * fix undefined types * general fixes * remove response schema example * redesigned navigation bar * redesigned sidebar * fixed up paddings * added feedback component + report issue * fixed up typography, padding, and general styling * redesigned code blocks * optimization * added error timeout * fixes * added indexing with algolia + fixes * fix errors with algolia script * redesign operation sections * fix heading scroll * design fixes * fix padding * fix padding + scroll issues * fix scroll issues * improve scroll performance * fixes for safari * optimization and fixes * fixes to docs + details animation * padding fixes for code block * added tab animation * fixed incorrect link * added selection styling * fix lint errors * redesigned details component * added detailed feedback form * api reference fixes * fix tabs * upgrade + fixes * updated documentation links * optimizations to sidebar items * fix spacing in sidebar item * optimizations and fixes * fix endpoint path styling * remove margin * final fixes * change margin on small devices * generated OAS * fixes for mobile * added feedback modal * optimize dark mode button * fixed color mode useeffect * minimize dom size * use new style system * radius and spacing design system * design fixes * fix eslint errors * added meta files * change cron schedule * fix docusaurus configurations * added operating system to feedback data * change content directory name * fixes to contribution guidelines * revert renaming content * added api-reference to documentation workflow * fixes for search * added dark mode + fixes * oas fixes * handle bugs * added code examples for clients * changed tooltip text * change authentication to card * change page title based on selected section * redesigned mobile navbar * fix icon colors * fix key colors * fix medusa-js installation command * change external regex in algolia * change changeset * fix padding on mobile * fix hydration error * update depedencies
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
---
|
||||
description: "Learn how to create a file service in a Medusa backend or a plugin."
|
||||
---
|
||||
|
||||
# How to Create a File Service
|
||||
|
||||
In this document, you’ll learn how to create a file service in Medusa.
|
||||
|
||||
## Overview
|
||||
|
||||
In this guide, you’ll learn about the steps to implement a file service and the methods you’re required to implement in a file service. You can implement the file service within the Medusa backend codebase or in a plugin.
|
||||
|
||||
The file service you’ll be creating in this guide will be a local file service that allows you to upload files into your Medusa backend’s codebase. This is to give you a realistic example of the implementation of a file service. You’re free to implement the file service as required for your case.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Multer Types Package
|
||||
|
||||
If you’re using TypeScript, as instructed by this guide, you should install the Multer types package to resolve errors within your file service types.
|
||||
|
||||
To do that, run the following command in the directory of your Medusa backend or plugin:
|
||||
|
||||
```bash npm2yarn
|
||||
npm install @types/multer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create the File Service Class
|
||||
|
||||
A file service class is defined in a TypeScript or JavaScript file that’s created in the `src/services` directory. The class must extend the `AbstractFileService` class imported from the `@medusajs/medusa` package.
|
||||
|
||||
Based on services’ naming conventions, the file’s name should be the slug version of the file service’s name without `service`, and the class’s name should be the pascal case of the file service’s name following by `Service`.
|
||||
|
||||
For example, if you’re creating a local file service, the file name would be `local-file.ts`, whereas the class name would be `LocalFileService`.
|
||||
|
||||
:::tip
|
||||
|
||||
You can learn more about services and their naming convention in [this documentation](../services/overview.mdx).
|
||||
|
||||
:::
|
||||
|
||||
For example, create the file `src/services/local-file.ts` with the following content:
|
||||
|
||||
```ts title=src/services/local-file.ts
|
||||
import {
|
||||
AbstractFileService,
|
||||
DeleteFileType,
|
||||
FileServiceGetUploadStreamResult,
|
||||
FileServiceUploadResult,
|
||||
GetUploadedFileType,
|
||||
UploadStreamDescriptorType,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
class LocalFileService extends AbstractFileService {
|
||||
async upload(
|
||||
fileData: Express.Multer.File
|
||||
): Promise<FileServiceUploadResult> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
async uploadProtected(
|
||||
fileData: Express.Multer.File
|
||||
): Promise<FileServiceUploadResult> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
async delete(
|
||||
fileData: DeleteFileType
|
||||
): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
async getUploadStreamDescriptor(
|
||||
fileData: UploadStreamDescriptorType
|
||||
): Promise<FileServiceGetUploadStreamResult> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
async getDownloadStream(
|
||||
fileData: GetUploadedFileType
|
||||
): Promise<NodeJS.ReadableStream> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
async getPresignedDownloadUrl(
|
||||
fileData: GetUploadedFileType
|
||||
): Promise<string> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
|
||||
export default LocalFileService
|
||||
```
|
||||
|
||||
This creates the service `LocalFileService` which, at the moment, adds a general implementation of the methods defined in the abstract class `AbstractFileService`.
|
||||
|
||||
### Using a Constructor
|
||||
|
||||
You can use a constructor to access services and resources registered in the dependency container, to define any necessary clients if you’re integrating a third-party storage service, and to access plugin options if your file service is defined in a plugin.
|
||||
|
||||
For example, the local service’s constructor could be useful to prepare the local upload directory:
|
||||
|
||||
```ts title=src/services/local-file.ts
|
||||
// ...
|
||||
import * as fs from "fs"
|
||||
|
||||
class LocalFileService extends AbstractFileService {
|
||||
// can also be replaced by an environment variable
|
||||
// or a plugin option
|
||||
protected serverUrl = "http://localhost:9000"
|
||||
protected publicPath = "uploads"
|
||||
protected protectedPath = "protected-uploads"
|
||||
|
||||
constructor(container) {
|
||||
super(container)
|
||||
|
||||
// for public uploads
|
||||
if (!fs.existsSync(this.publicPath)) {
|
||||
fs.mkdirSync(this.publicPath)
|
||||
}
|
||||
|
||||
// for protected uploads
|
||||
if (!fs.existsSync(this.protectedPath)) {
|
||||
fs.mkdirSync(this.protectedPath)
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Another example showcasing how to access resources using dependency injection:
|
||||
|
||||
<!-- eslint-disable prefer-rest-params -->
|
||||
|
||||
```ts title=src/services/local-file.ts
|
||||
type InjectedDependencies = {
|
||||
logger: Logger
|
||||
}
|
||||
|
||||
class LocalFileService extends AbstractFileService {
|
||||
// ...
|
||||
protected logger_: Logger
|
||||
|
||||
constructor({ logger }: InjectedDependencies) {
|
||||
super(...arguments)
|
||||
this.logger_ = logger
|
||||
// ...
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
You can access the plugin options in the second parameter passed to the constructor:
|
||||
|
||||
```ts title=src/services/local-file.ts
|
||||
class LocalFileService extends AbstractFileService {
|
||||
protected serverUrl = "http://localhost:9000"
|
||||
// ...
|
||||
|
||||
constructor(
|
||||
container,
|
||||
pluginOptions
|
||||
) {
|
||||
super(container)
|
||||
|
||||
if (pluginOptions?.serverUrl) {
|
||||
this.serverUrl = pluginOptions?.serverUrl
|
||||
}
|
||||
// ...
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Implement Required Methods
|
||||
|
||||
In this section, you’ll learn about the required methods to implement in the file service.
|
||||
|
||||
### upload
|
||||
|
||||
This method is used to upload a file to the Medusa backend. You must handle the upload logic within this method.
|
||||
|
||||
This method accepts one parameter, which is a [multer file object](http://expressjs.com/en/resources/middleware/multer.html#file-information). The file is uploaded to a temporary directory by default. Among the file’s details, you can access the file’s path in the `path` property of the file object.
|
||||
|
||||
So, for example, you can create a read stream to the file’s content if necessary using the `fs` library:
|
||||
|
||||
```ts
|
||||
fs.createReadStream(file.path)
|
||||
```
|
||||
|
||||
Where `file` is the parameter passed to the `upload` method.
|
||||
|
||||
The method is expected to return an object that has one property `url`, which is a string indicating the full accessible URL to the file.
|
||||
|
||||
An example implementation of this method for the local file service:
|
||||
|
||||
```ts title=src/services/local-file.ts
|
||||
class LocalFileService extends AbstractFileService {
|
||||
|
||||
async upload(
|
||||
fileData: Express.Multer.File
|
||||
): Promise<FileServiceUploadResult> {
|
||||
const filePath =
|
||||
`${this.publicPath}/${fileData.originalname}`
|
||||
fs.copyFileSync(fileData.path, filePath)
|
||||
return {
|
||||
url: `${this.serverUrl}/${filePath}`,
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
:::tip
|
||||
|
||||
This example does not account for duplicate names to maintain simplicity in this guide. So, an uploaded file can replace another existing file that has the same name.
|
||||
|
||||
:::
|
||||
|
||||
### uploadProtected
|
||||
|
||||
This method is used to upload a file to the Medusa backend, but to a protected storage. Typically, this would be used to store files that shouldn’t be accessible by using the file’s URL or should only be accessible by authenticated users.
|
||||
|
||||
You must handle the upload logic and the file permissions or private storage configuration within this method.
|
||||
|
||||
This method accepts one parameter, which is a [multer file object](http://expressjs.com/en/resources/middleware/multer.html#file-information). The file is uploaded to a temporary directory by default. Among the file’s details, you can access the file’s path in the `path` property of the file object.
|
||||
|
||||
So, for example, you can create a read stream to the file’s content if necessary using the `fs` library:
|
||||
|
||||
```ts
|
||||
fs.createReadStream(file.path)
|
||||
```
|
||||
|
||||
Where `file` is the parameter passed to the `uploadProtected` method.
|
||||
|
||||
The method is expected to return an object that has one property `url`, which is a string indicating the full accessible URL to the file.
|
||||
|
||||
An example implementation of this method for the local file service:
|
||||
|
||||
```ts title=src/services/local-file.ts
|
||||
class LocalFileService extends AbstractFileService {
|
||||
|
||||
async uploadProtected(
|
||||
fileData: Express.Multer.File
|
||||
): Promise<FileServiceUploadResult> {
|
||||
const filePath =
|
||||
`${this.protectedPath}/${fileData.originalname}`
|
||||
fs.copyFileSync(fileData.path, filePath)
|
||||
return {
|
||||
url: `${this.serverUrl}/${filePath}`,
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### delete
|
||||
|
||||
This method is used to delete a file from storage. You must handle the delete logic within this method.
|
||||
This method accepts one parameter, which is an object that holds a `fileKey` property. The value of this property is a string that acts as an identifier of the file to delete. For example, for local file service, it could be the file name.
|
||||
|
||||
This method is not expected to return anything.
|
||||
|
||||
An example implementation of this method for the local file service:
|
||||
|
||||
```ts title=src/services/local-file.ts
|
||||
class LocalFileService extends AbstractFileService {
|
||||
|
||||
async delete(
|
||||
fileData: DeleteFileType
|
||||
): Promise<void> {
|
||||
fs.rmSync(fileData.fileKey)
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### getUploadStreamDescriptor
|
||||
|
||||
This method is used to upload a file using a write stream. This is useful if the file is being written through a stream rather than uploaded to the temporary directory.
|
||||
|
||||
The method accepts one parameter, which is an object that has the following properties:
|
||||
|
||||
- `name`: a string indicating the name of the file.
|
||||
- `ext`: an optional string indicating the extension of the file.
|
||||
- `acl`: an optional string indicating the file’s permission. If the file should be uploaded privately, its value will be `private`.
|
||||
|
||||
The method is expected to return an object having the following properties:
|
||||
|
||||
- `writeStream`: a write stream object.
|
||||
- `promise`: A promise that should resolved when the writing process is done to finish the upload. This depends on the type of file service you’re creating.
|
||||
- `url`: a string indicating the URL of the file once it’s uploaded.
|
||||
- `fileKey`: a string indicating the identifier of your file in the storage. For example, for a local file service this can be the file name.
|
||||
|
||||
An example implementation of this method for the local file service:
|
||||
|
||||
```ts title=src/services/local-file.ts
|
||||
class LocalFileService extends AbstractFileService {
|
||||
|
||||
async getUploadStreamDescriptor(
|
||||
fileData: UploadStreamDescriptorType
|
||||
): Promise<FileServiceGetUploadStreamResult> {
|
||||
const filePath = `${fileData.acl !== "private" ?
|
||||
this.publicPath : this.protectedPath
|
||||
}/${fileData.name}.${fileData.ext}`
|
||||
const writeStream = fs.createWriteStream(filePath)
|
||||
|
||||
return {
|
||||
writeStream,
|
||||
promise: Promise.resolve(),
|
||||
url: `${this.serverUrl}/${filePath}`,
|
||||
fileKey: filePath,
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### getDownloadStream
|
||||
|
||||
This method is used to read a file using a read stream, typically for download.
|
||||
|
||||
The method accepts as a parameter an object having the `fileKey` property, which is a string indicating the identifier of the file.
|
||||
|
||||
The method is expected to return a readable stream.
|
||||
|
||||
An example implementation of this method for the local file service:
|
||||
|
||||
```ts title=src/services/local-file.ts
|
||||
class LocalFileService extends AbstractFileService {
|
||||
|
||||
async getDownloadStream(
|
||||
fileData: GetUploadedFileType
|
||||
): Promise<NodeJS.ReadableStream> {
|
||||
const filePath = `${fileData.acl !== "private" ?
|
||||
this.publicPath : this.protectedPath
|
||||
}/${fileData.name}.${fileData.ext}`
|
||||
const readStream = fs.createReadStream(filePath)
|
||||
|
||||
return readStream
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### getPresignedDownloadUrl
|
||||
|
||||
The `getPresignedDownloadUrl` method is used to retrieve a download URL of the file. For some file services, such as S3, a presigned URL indicates a temporary URL to get access to a file.
|
||||
|
||||
If your file service doesn’t perform or offer a similar functionality, you can just return the URL to download the file.
|
||||
|
||||
This method accepts as a parameter an object having the `fileKey` property, which is a string indicating the identifier of the file.
|
||||
|
||||
The method is expected to return a string, being the URL of the file.
|
||||
|
||||
An example implementation of this method for the local file service:
|
||||
|
||||
```ts title=src/services/local-file.ts
|
||||
class LocalFileService extends AbstractFileService {
|
||||
|
||||
async getPresignedDownloadUrl(
|
||||
fileData: GetUploadedFileType
|
||||
): Promise<string> {
|
||||
return `${this.serverUrl}/${fileData.fileKey}`
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Run Build Command
|
||||
|
||||
In the directory of the Medusa backend, run the `build` command to transpile the files in the `src` directory into the `dist` directory:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
:::note
|
||||
|
||||
This section explains how to test out your implementation if the file service was created in the Medusa backend codebase. You can refer to the [plugin documentation](../plugins/create.mdx#test-your-plugin) on how to test a plugin.
|
||||
|
||||
:::
|
||||
|
||||
Run your backend to test it out:
|
||||
|
||||
```bash npm2yarn
|
||||
npx medusa develop
|
||||
```
|
||||
|
||||
Then, try uploading a file, for example, using the [Upload File endpoint](https://docs.medusajs.com/api/admin#uploads_postuploads). The file should be uploaded based on the logic you’ve implemented.
|
||||
|
||||
### (Optional) Accessing the File
|
||||
|
||||
:::note
|
||||
|
||||
This step is only useful if you're implementing a local file service.
|
||||
|
||||
:::
|
||||
|
||||
Since the file is uploaded to a local directory `uploads`, you need to configure a static route in express that allows accessing the files within the `uploads` directory.
|
||||
|
||||
To do that, create the file `src/api/index.ts` with the following content:
|
||||
|
||||
```ts
|
||||
import express from "express"
|
||||
|
||||
export default () => {
|
||||
const app = express.Router()
|
||||
|
||||
app.use(`/uploads`, express.static(uploadDir))
|
||||
|
||||
return app
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [How to create a plugin](../plugins/create.mdx)
|
||||
- [How to publish a plugin](../plugins/publish.mdx)
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
description: "Learn what a file service is in Medusa. A file service defines how files are stored in the Medusa Backend."
|
||||
---
|
||||
|
||||
import DocCard from '@theme/DocCard';
|
||||
import Icons from '@theme/Icon';
|
||||
|
||||
# File Service
|
||||
|
||||
In this document, you’ll learn what a file service is in Medusa.
|
||||
|
||||
## Introduction
|
||||
|
||||
A file service defines how files are stored in the Medusa Backend. Those files include products’ images and files used to import or export data.
|
||||
|
||||
Medusa Backend includes a default file service that acts as a placeholder, but does not actually perform any storage functionalities. So, you must either install one of the [existing file-service plugins](../../plugins/file-service/index.mdx), such as [MinIO](../../plugins/file-service/minio.md) or [S3](../../plugins/file-service/s3.mdx), or create your own file service if you want to utilize storage functionalities.
|
||||
|
||||
A file service is a TypeScript or JavaScript class that extends the `AbstractFileService` class from the core `@medusajs/medusa` package. By extending this class, the file service must implement the necessary methods that take care of general upload and download functionalities. The Medusa Backend then uses these methods when necessary, for example, when a product image is uploaded.
|
||||
|
||||
---
|
||||
|
||||
## Custom Development
|
||||
|
||||
Developers can create a custom file service with the desired functionality directly within the Medusa Core, in a plugin, or in a module.
|
||||
|
||||
<DocCard item={{
|
||||
type: 'link',
|
||||
href: '/development/file-service/create-file-service',
|
||||
label: 'Create a File Service',
|
||||
customProps: {
|
||||
icon: Icons['academic-cap-solid'],
|
||||
description: 'Learn how to create a file service.',
|
||||
}
|
||||
}} />
|
||||
Reference in New Issue
Block a user