From a5707b48fc4f7aba6d94634c890ce8a34fc4b3a7 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Thu, 4 May 2023 13:51:12 +0300 Subject: [PATCH] docs: added create file service guide (#4009) * docs: added create file service guide * fix eslint --- .../file-service/create-file-service.md | 410 ++++++++++++++++++ .../development/file-service/overview.mdx | 3 +- .../strategies/override-strategy.md | 2 +- www/docs/sidebars.js | 7 +- 4 files changed, 414 insertions(+), 8 deletions(-) create mode 100644 docs/content/development/file-service/create-file-service.md diff --git a/docs/content/development/file-service/create-file-service.md b/docs/content/development/file-service/create-file-service.md new file mode 100644 index 0000000000..a255f5ab87 --- /dev/null +++ b/docs/content/development/file-service/create-file-service.md @@ -0,0 +1,410 @@ +--- +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 { + throw new Error("Method not implemented.") + } + async uploadProtected( + fileData: Express.Multer.File + ): Promise { + throw new Error("Method not implemented.") + } + async delete( + fileData: DeleteFileType + ): Promise { + throw new Error("Method not implemented.") + } + async getUploadStreamDescriptor( + fileData: UploadStreamDescriptorType + ): Promise { + throw new Error("Method not implemented.") + } + async getDownloadStream( + fileData: GetUploadedFileType + ): Promise { + throw new Error("Method not implemented.") + } + async getPresignedDownloadUrl( + fileData: GetUploadedFileType + ): Promise { + 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: + + + +```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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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.md#test-your-plugin) on how to test a plugin. + +::: + +Run your backend to test it out: + +```bash npm2yarn +npm run start +``` + +Then, try uploading a file, for example, using the [Upload File endpoint](/api/admin#tag/Uploads/operation/PostUploads). The file should be uploaded based on the logic you’ve implemented. + +--- + +## See Also + +- [How to create a plugin](../plugins/create.md) +- [How to publish a plugin](../plugins/publish.md) diff --git a/docs/content/development/file-service/overview.mdx b/docs/content/development/file-service/overview.mdx index 4ea5b6b05b..6b0cf2921e 100644 --- a/docs/content/development/file-service/overview.mdx +++ b/docs/content/development/file-service/overview.mdx @@ -25,11 +25,10 @@ Developers can create a custom file service with the desired functionality direc \ No newline at end of file diff --git a/docs/content/development/strategies/override-strategy.md b/docs/content/development/strategies/override-strategy.md index b2980f773e..99e64bf4ee 100644 --- a/docs/content/development/strategies/override-strategy.md +++ b/docs/content/development/strategies/override-strategy.md @@ -1,5 +1,5 @@ --- -description: "Learn what a Strategy is in Medusa. A strategy is an isolated piece of business logic that can be overridden and customized." +description: "Learn how to override a strategy in a Medusa backend or plugin." addHowToData: true --- diff --git a/www/docs/sidebars.js b/www/docs/sidebars.js index 6de20d3c98..7b530d2ba1 100644 --- a/www/docs/sidebars.js +++ b/www/docs/sidebars.js @@ -1650,12 +1650,9 @@ module.exports = { }, }, { - type: "link", - href: "#", + type: "doc", + id: "development/file-service/create-file-service", label: "Create a File Service", - customProps: { - sidebar_is_soon: true, - }, }, ], },