diff --git a/packages/medusa-file-minio/src/services/minio.js b/packages/medusa-file-minio/src/services/minio.js index 1f06a4b237..c1fb6ab4f8 100644 --- a/packages/medusa-file-minio/src/services/minio.js +++ b/packages/medusa-file-minio/src/services/minio.js @@ -1,8 +1,9 @@ import fs from "fs" import aws from "aws-sdk" -import { FileService } from "medusa-interfaces" +import { AbstractFileService } from '@medusajs/medusa' -class MinioService extends FileService { +class MinioService extends AbstractFileService { + constructor({}, options) { super() @@ -15,14 +16,14 @@ class MinioService extends FileService { } upload(file) { - aws.config.setPromisesDependency() + aws.config.setPromisesDependency(null) aws.config.update({ accessKeyId: this.accessKeyId_, secretAccessKey: this.secretAccessKey_, endpoint: this.endpoint_, s3ForcePathStyle: this.s3ForcePathStyle_, signatureVersion: this.signatureVersion_, - }) + }, true) const s3 = new aws.S3() const params = { @@ -46,14 +47,14 @@ class MinioService extends FileService { } delete(file) { - aws.config.setPromisesDependency() + aws.config.setPromisesDependency(null) aws.config.update({ accessKeyId: this.accessKeyId_, secretAccessKey: this.secretAccessKey_, endpoint: this.endpoint_, s3ForcePathStyle: this.s3ForcePathStyle_, signatureVersion: this.signatureVersion_, - }) + }, true) const s3 = new aws.S3() const params = { @@ -71,6 +72,18 @@ class MinioService extends FileService { }) }) } + + async getUploadStreamDescriptor(fileData) { + throw new Error("Method not implemented.") + } + + async getDownloadStream(fileData) { + throw new Error("Method not implemented.") + } + + async getPresignedDownloadUrl(fileData) { + throw new Error("Method not implemented.") + } } export default MinioService diff --git a/packages/medusa-file-s3/src/services/s3.js b/packages/medusa-file-s3/src/services/s3.js index 85541260eb..63dfe9c1ff 100644 --- a/packages/medusa-file-s3/src/services/s3.js +++ b/packages/medusa-file-s3/src/services/s3.js @@ -1,8 +1,8 @@ import fs from "fs" import aws from "aws-sdk" -import { FileService } from "medusa-interfaces" +import { AbstractFileService } from '@medusajs/medusa' -class S3Service extends FileService { +class S3Service extends AbstractFileService { constructor({}, options) { super() @@ -15,13 +15,13 @@ class S3Service extends FileService { } upload(file) { - aws.config.setPromisesDependency() + aws.config.setPromisesDependency(null) aws.config.update({ accessKeyId: this.accessKeyId_, secretAccessKey: this.secretAccessKey_, region: this.region_, endpoint: this.endpoint_, - }) + }, true) const s3 = new aws.S3() var params = { @@ -44,13 +44,13 @@ class S3Service extends FileService { } delete(file) { - aws.config.setPromisesDependency() + aws.config.setPromisesDependency(null) aws.config.update({ accessKeyId: this.accessKeyId_, secretAccessKey: this.secretAccessKey_, region: this.region_, endpoint: this.endpoint_, - }) + }, true) const s3 = new aws.S3() var params = { @@ -68,6 +68,18 @@ class S3Service extends FileService { }) }) } + + async getUploadStreamDescriptor(fileData) { + throw new Error("Method not implemented.") + } + + async getDownloadStream(fileData) { + throw new Error("Method not implemented.") + } + + async getPresignedDownloadUrl(fileData) { + throw new Error("Method not implemented.") + } } export default S3Service diff --git a/packages/medusa-file-spaces/src/services/digital-ocean.js b/packages/medusa-file-spaces/src/services/digital-ocean.js index d9ef58e2e7..2d26a63c35 100644 --- a/packages/medusa-file-spaces/src/services/digital-ocean.js +++ b/packages/medusa-file-spaces/src/services/digital-ocean.js @@ -1,9 +1,11 @@ import fs from "fs" import aws from "aws-sdk" import { parse } from "path" -import { FileService } from "medusa-interfaces" +import { AbstractFileService, FileServiceUploadResult } from "@medusajs/medusa" +import { EntityManager } from "typeorm" +import stream from "stream" -class DigitalOceanService extends FileService { +class DigitalOceanService extends AbstractFileService { constructor({}, options) { super() @@ -16,13 +18,16 @@ class DigitalOceanService extends FileService { } upload(file) { - aws.config.setPromisesDependency() - aws.config.update({ - accessKeyId: this.accessKeyId_, - secretAccessKey: this.secretAccessKey_, - region: this.region_, - endpoint: this.endpoint_, - }) + aws.config.setPromisesDependency(null) + aws.config.update( + { + accessKeyId: this.accessKeyId_, + secretAccessKey: this.secretAccessKey_, + region: this.region_, + endpoint: this.endpoint_, + }, + true + ) const parsedFilename = parse(file.originalname) const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}` @@ -51,13 +56,16 @@ class DigitalOceanService extends FileService { } delete(file) { - aws.config.setPromisesDependency() - aws.config.update({ - accessKeyId: this.accessKeyId_, - secretAccessKey: this.secretAccessKey_, - region: this.region_, - endpoint: this.endpoint_, - }) + aws.config.setPromisesDependency(null) + aws.config.update( + { + accessKeyId: this.accessKeyId_, + secretAccessKey: this.secretAccessKey_, + region: this.region_, + endpoint: this.endpoint_, + }, + true + ) const s3 = new aws.S3() var params = { @@ -75,6 +83,18 @@ class DigitalOceanService extends FileService { }) }) } + + async getUploadStreamDescriptor(fileData) { + throw new Error("Method not implemented.") + } + + async getDownloadStream(fileData) { + throw new Error("Method not implemented.") + } + + async getPresignedDownloadUrl(fileData) { + throw new Error("Method not implemented.") + } } export default DigitalOceanService diff --git a/packages/medusa/package.json b/packages/medusa/package.json index 5ed727a409..0080f6f917 100644 --- a/packages/medusa/package.json +++ b/packages/medusa/package.json @@ -21,6 +21,7 @@ "@babel/core": "^7.14.3", "@babel/preset-typescript": "^7.13.0", "@types/express": "^4.17.13", + "@types/multer": "^1.4.7", "@types/jest": "^27.5.0", "@types/jsonwebtoken": "^8.5.5", "babel-preset-medusa-package": "^1.1.19", diff --git a/packages/medusa/src/interfaces/file-service.ts b/packages/medusa/src/interfaces/file-service.ts new file mode 100644 index 0000000000..db9b599bf6 --- /dev/null +++ b/packages/medusa/src/interfaces/file-service.ts @@ -0,0 +1,92 @@ +import stream from "stream" +import { TransactionBaseService } from "./transaction-base-service" + +export type FileServiceUploadResult = { + url: string +} + +export type FileServiceGetUploadStreamResult = { + writeStream: stream.PassThrough + promise: Promise + url: string + fileKey: string + [x: string]: unknown +} + +export type GetUploadedFileType = { + fileKey: string + [x: string]: unknown +} + +export type UploadStreamDescriptorType = { + name: string + ext?: string + acl?: string + [x: string]: unknown +} + +export interface IFileService> + extends TransactionBaseService { + /** + * upload file to fileservice + * @param file Multer file from express multipart/form-data + * */ + upload(file: Express.Multer.File): Promise + + /** + * remove file from fileservice + * @param fileData Remove file described by record + * */ + delete(fileData: Record): void + + /** + * upload file to fileservice from stream + * @param fileData file metadata relevant for fileservice to create and upload the file + * @param fileStream readable stream of the file to upload + * */ + getUploadStreamDescriptor( + fileData: UploadStreamDescriptorType + ): Promise + + /** + * download file from fileservice as stream + * @param fileData file metadata relevant for fileservice to download the file + * @returns readable stream of the file to download + * */ + getDownloadStream( + fileData: GetUploadedFileType + ): Promise + + /** + * Generate a presigned download url to obtain a file + * @param fileData file metadata relevant for fileservice to download the file + * @returns presigned url to download the file + * */ + getPresignedDownloadUrl(fileData: GetUploadedFileType): Promise +} +export abstract class AbstractFileService> + extends TransactionBaseService + implements IFileService +{ + abstract upload( + fileData: Express.Multer.File + ): Promise + + abstract delete(fileData: Record): void + + abstract getUploadStreamDescriptor( + fileData: UploadStreamDescriptorType + ): Promise + + abstract getDownloadStream( + fileData: GetUploadedFileType + ): Promise + + abstract getPresignedDownloadUrl( + fileData: GetUploadedFileType + ): Promise +} + +export const isFileService = (object: unknown): boolean => { + return object instanceof AbstractFileService +} diff --git a/packages/medusa/src/interfaces/index.ts b/packages/medusa/src/interfaces/index.ts index a3faeabb80..cc92e8f118 100644 --- a/packages/medusa/src/interfaces/index.ts +++ b/packages/medusa/src/interfaces/index.ts @@ -2,5 +2,6 @@ export * from "./tax-calculation-strategy" export * from "./cart-completion-strategy" export * from "./tax-service" export * from "./transaction-base-service" +export * from "./file-service" export * from "./models/base-entity" export * from "./models/soft-deletable-entity" diff --git a/packages/medusa/src/loaders/plugins.ts b/packages/medusa/src/loaders/plugins.ts index 7c24df9b17..e4274a869e 100644 --- a/packages/medusa/src/loaders/plugins.ts +++ b/packages/medusa/src/loaders/plugins.ts @@ -1,5 +1,5 @@ import glob from "glob" -import { Express } from 'express' +import { Express } from "express" import { EntitySchema } from "typeorm" import { BaseService, @@ -16,9 +16,20 @@ import path from "path" import fs from "fs" import { asValue, asClass, asFunction, aliasTo } from "awilix" import { sync as existsSync } from "fs-exists-cached" -import { AbstractTaxService, isTaxCalculationStrategy } from "../interfaces" +import { + AbstractFileService, + AbstractTaxService, + isFileService, + isTaxCalculationStrategy, + TransactionBaseService, +} from "../interfaces" import formatRegistrationName from "../utils/format-registration-name" -import { ClassConstructor, ConfigModule, Logger, MedusaContainer } from "../types/global" +import { + ClassConstructor, + ConfigModule, + Logger, + MedusaContainer, +} from "../types/global" import { MiddlewareService } from "../services" type Options = { @@ -40,7 +51,13 @@ type PluginDetails = { /** * Registers all services in the services directory */ -export default async ({ rootDirectory, container, app, configModule, activityId }: Options): Promise => { +export default async ({ + rootDirectory, + container, + app, + configModule, + activityId, +}: Options): Promise => { const resolved = getResolvedPlugins(rootDirectory, configModule) || [] await Promise.all( @@ -59,7 +76,10 @@ export default async ({ rootDirectory, container, app, configModule, activityId ) } -function getResolvedPlugins(rootDirectory: string, configModule: ConfigModule): undefined | PluginDetails[] { +function getResolvedPlugins( + rootDirectory: string, + configModule: ConfigModule +): undefined | PluginDetails[] { const { plugins } = configModule const resolved = plugins.map((plugin) => { @@ -85,10 +105,14 @@ function getResolvedPlugins(rootDirectory: string, configModule: ConfigModule): } export async function registerPluginModels({ - rootDirectory, - container, - configModule -}: { rootDirectory: string; container: MedusaContainer; configModule: ConfigModule; }): Promise { + rootDirectory, + container, + configModule, +}: { + rootDirectory: string + container: MedusaContainer + configModule: ConfigModule +}): Promise { const resolved = getResolvedPlugins(rootDirectory, configModule) || [] await Promise.all( resolved.map(async (pluginDetails) => { @@ -97,7 +121,10 @@ export async function registerPluginModels({ ) } -async function runLoaders(pluginDetails: PluginDetails, container: MedusaContainer): Promise { +async function runLoaders( + pluginDetails: PluginDetails, + container: MedusaContainer +): Promise { const loaderFiles = glob.sync( `${pluginDetails.resolve}/loaders/[!__]*.js`, {} @@ -118,12 +145,18 @@ async function runLoaders(pluginDetails: PluginDetails, container: MedusaContain ) } -function registerMedusaApi(pluginDetails: PluginDetails, container: MedusaContainer): void { +function registerMedusaApi( + pluginDetails: PluginDetails, + container: MedusaContainer +): void { registerMedusaMiddleware(pluginDetails, container) registerStrategies(pluginDetails, container) } -function registerStrategies(pluginDetails: PluginDetails, container: MedusaContainer): void { +function registerStrategies( + pluginDetails: PluginDetails, + container: MedusaContainer +): void { let module try { const path = `${pluginDetails.resolve}/strategies/tax-calculation` @@ -150,7 +183,10 @@ function registerStrategies(pluginDetails: PluginDetails, container: MedusaConta } } -function registerMedusaMiddleware(pluginDetails: PluginDetails, container: MedusaContainer): void { +function registerMedusaMiddleware( + pluginDetails: PluginDetails, + container: MedusaContainer +): void { let module try { module = require(`${pluginDetails.resolve}/api/medusa-middleware`).default @@ -158,7 +194,8 @@ function registerMedusaMiddleware(pluginDetails: PluginDetails, container: Medus return } - const middlewareService = container.resolve("middlewareService") + const middlewareService = + container.resolve("middlewareService") if (module.postAuthentication) { middlewareService.addPostAuthentication( module.postAuthentication, @@ -178,8 +215,12 @@ function registerMedusaMiddleware(pluginDetails: PluginDetails, container: Medus } } -function registerCoreRouters(pluginDetails: PluginDetails, container: MedusaContainer): void { - const middlewareService = container.resolve("middlewareService") +function registerCoreRouters( + pluginDetails: PluginDetails, + container: MedusaContainer +): void { + const middlewareService = + container.resolve("middlewareService") const { resolve } = pluginDetails const adminFiles = glob.sync(`${resolve}/api/admin/[!__]*.js`, {}) const storeFiles = glob.sync(`${resolve}/api/store/[!__]*.js`, {}) @@ -245,16 +286,22 @@ function registerApi( * registered * @return {void} */ -export async function registerServices(pluginDetails: PluginDetails, container: MedusaContainer): Promise { +export async function registerServices( + pluginDetails: PluginDetails, + container: MedusaContainer +): Promise { const files = glob.sync(`${pluginDetails.resolve}/services/[!__]*.js`, {}) await Promise.all( files.map(async (fn) => { const loaded = require(fn).default const name = formatRegistrationName(fn) - if (!(loaded.prototype instanceof BaseService)) { + if ( + !(loaded.prototype instanceof BaseService) && + !(loaded.prototype instanceof TransactionBaseService) + ) { const logger = container.resolve("logger") - const message = `Services must inherit from BaseService, please check ${fn}` + const message = `File must be a valid service implementation, please check ${fn}` logger.error(message) throw new Error(message) } @@ -277,7 +324,8 @@ export async function registerServices(pluginDetails: PluginDetails, container: } else if (loaded.prototype instanceof OauthService) { const appDetails = loaded.getAppDetails(pluginDetails.options) - const oauthService = container.resolve("oauthService") + const oauthService = + container.resolve("oauthService") await oauthService.registerOauthApp(appDetails) const name = appDetails.application_name @@ -324,6 +372,15 @@ export async function registerServices(pluginDetails: PluginDetails, container: ), [`fileService`]: aliasTo(name), }) + } else if (isFileService(loaded.prototype)) { + // Add the service directly to the container in order to make simple + // resolution if we already know which file storage provider we need to use + container.register({ + [name]: asFunction( + (cradle) => new loaded(cradle, pluginDetails.options) + ), + [`fileService`]: aliasTo(name), + }) } else if (loaded.prototype instanceof SearchService) { // Add the service directly to the container in order to make simple // resolution if we already know which search provider we need to use @@ -365,7 +422,10 @@ export async function registerServices(pluginDetails: PluginDetails, container: * registered * @return {void} */ -function registerSubscribers(pluginDetails: PluginDetails, container: MedusaContainer): void { +function registerSubscribers( + pluginDetails: PluginDetails, + container: MedusaContainer +): void { const files = glob.sync(`${pluginDetails.resolve}/subscribers/*.js`, {}) files.forEach((fn) => { const loaded = require(fn).default @@ -387,19 +447,24 @@ function registerSubscribers(pluginDetails: PluginDetails, container: MedusaCont * registered * @return {void} */ -function registerRepositories(pluginDetails: PluginDetails, container: MedusaContainer): void { +function registerRepositories( + pluginDetails: PluginDetails, + container: MedusaContainer +): void { const files = glob.sync(`${pluginDetails.resolve}/repositories/*.js`, {}) files.forEach((fn) => { const loaded = require(fn) as ClassConstructor - Object.entries(loaded).map(([, val]: [string, ClassConstructor]) => { - if (typeof val === "function") { - const name = formatRegistrationName(fn) - container.register({ - [name]: asClass(val), - }) + Object.entries(loaded).map( + ([, val]: [string, ClassConstructor]) => { + if (typeof val === "function") { + const name = formatRegistrationName(fn) + container.register({ + [name]: asClass(val), + }) + } } - }) + ) }) } @@ -414,21 +479,26 @@ function registerRepositories(pluginDetails: PluginDetails, container: MedusaCon * registered * @return {void} */ -function registerModels(pluginDetails: PluginDetails, container: MedusaContainer): void { +function registerModels( + pluginDetails: PluginDetails, + container: MedusaContainer +): void { const files = glob.sync(`${pluginDetails.resolve}/models/*.js`, {}) files.forEach((fn) => { const loaded = require(fn) as ClassConstructor | EntitySchema - Object.entries(loaded).map(([, val]: [string, ClassConstructor | EntitySchema]) => { - if (typeof val === "function" || val instanceof EntitySchema) { - const name = formatRegistrationName(fn) - container.register({ - [name]: asValue(val), - }) + Object.entries(loaded).map( + ([, val]: [string, ClassConstructor | EntitySchema]) => { + if (typeof val === "function" || val instanceof EntitySchema) { + const name = formatRegistrationName(fn) + container.register({ + [name]: asValue(val), + }) - container.registerAdd("db_entities", asValue(val)) + container.registerAdd("db_entities", asValue(val)) + } } - }) + ) }) } @@ -446,11 +516,11 @@ function createPluginId(name: string): string { * @return {object} the plugin details */ function resolvePlugin(pluginName: string): { - resolve: string; - id: string; - name: string; + resolve: string + id: string + name: string options: Record - version: string; + version: string } { // Only find plugins when we're not given an absolute path if (!existsSync(pluginName)) {