feat(medusa): Extend file-service interface + move to core (#1577)

This commit is contained in:
Philip Korsholm
2022-06-04 10:11:17 +02:00
committed by GitHub
parent d7a3218fbe
commit 8e42d37e84
7 changed files with 279 additions and 70 deletions
@@ -1,8 +1,9 @@
import fs from "fs" import fs from "fs"
import aws from "aws-sdk" 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) { constructor({}, options) {
super() super()
@@ -15,14 +16,14 @@ class MinioService extends FileService {
} }
upload(file) { upload(file) {
aws.config.setPromisesDependency() aws.config.setPromisesDependency(null)
aws.config.update({ aws.config.update({
accessKeyId: this.accessKeyId_, accessKeyId: this.accessKeyId_,
secretAccessKey: this.secretAccessKey_, secretAccessKey: this.secretAccessKey_,
endpoint: this.endpoint_, endpoint: this.endpoint_,
s3ForcePathStyle: this.s3ForcePathStyle_, s3ForcePathStyle: this.s3ForcePathStyle_,
signatureVersion: this.signatureVersion_, signatureVersion: this.signatureVersion_,
}) }, true)
const s3 = new aws.S3() const s3 = new aws.S3()
const params = { const params = {
@@ -46,14 +47,14 @@ class MinioService extends FileService {
} }
delete(file) { delete(file) {
aws.config.setPromisesDependency() aws.config.setPromisesDependency(null)
aws.config.update({ aws.config.update({
accessKeyId: this.accessKeyId_, accessKeyId: this.accessKeyId_,
secretAccessKey: this.secretAccessKey_, secretAccessKey: this.secretAccessKey_,
endpoint: this.endpoint_, endpoint: this.endpoint_,
s3ForcePathStyle: this.s3ForcePathStyle_, s3ForcePathStyle: this.s3ForcePathStyle_,
signatureVersion: this.signatureVersion_, signatureVersion: this.signatureVersion_,
}) }, true)
const s3 = new aws.S3() const s3 = new aws.S3()
const params = { 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 export default MinioService
+18 -6
View File
@@ -1,8 +1,8 @@
import fs from "fs" import fs from "fs"
import aws from "aws-sdk" 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) { constructor({}, options) {
super() super()
@@ -15,13 +15,13 @@ class S3Service extends FileService {
} }
upload(file) { upload(file) {
aws.config.setPromisesDependency() aws.config.setPromisesDependency(null)
aws.config.update({ aws.config.update({
accessKeyId: this.accessKeyId_, accessKeyId: this.accessKeyId_,
secretAccessKey: this.secretAccessKey_, secretAccessKey: this.secretAccessKey_,
region: this.region_, region: this.region_,
endpoint: this.endpoint_, endpoint: this.endpoint_,
}) }, true)
const s3 = new aws.S3() const s3 = new aws.S3()
var params = { var params = {
@@ -44,13 +44,13 @@ class S3Service extends FileService {
} }
delete(file) { delete(file) {
aws.config.setPromisesDependency() aws.config.setPromisesDependency(null)
aws.config.update({ aws.config.update({
accessKeyId: this.accessKeyId_, accessKeyId: this.accessKeyId_,
secretAccessKey: this.secretAccessKey_, secretAccessKey: this.secretAccessKey_,
region: this.region_, region: this.region_,
endpoint: this.endpoint_, endpoint: this.endpoint_,
}) }, true)
const s3 = new aws.S3() const s3 = new aws.S3()
var params = { 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 export default S3Service
@@ -1,9 +1,11 @@
import fs from "fs" import fs from "fs"
import aws from "aws-sdk" import aws from "aws-sdk"
import { parse } from "path" 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) { constructor({}, options) {
super() super()
@@ -16,13 +18,16 @@ class DigitalOceanService extends FileService {
} }
upload(file) { upload(file) {
aws.config.setPromisesDependency() aws.config.setPromisesDependency(null)
aws.config.update({ aws.config.update(
accessKeyId: this.accessKeyId_, {
secretAccessKey: this.secretAccessKey_, accessKeyId: this.accessKeyId_,
region: this.region_, secretAccessKey: this.secretAccessKey_,
endpoint: this.endpoint_, region: this.region_,
}) endpoint: this.endpoint_,
},
true
)
const parsedFilename = parse(file.originalname) const parsedFilename = parse(file.originalname)
const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}` const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}`
@@ -51,13 +56,16 @@ class DigitalOceanService extends FileService {
} }
delete(file) { delete(file) {
aws.config.setPromisesDependency() aws.config.setPromisesDependency(null)
aws.config.update({ aws.config.update(
accessKeyId: this.accessKeyId_, {
secretAccessKey: this.secretAccessKey_, accessKeyId: this.accessKeyId_,
region: this.region_, secretAccessKey: this.secretAccessKey_,
endpoint: this.endpoint_, region: this.region_,
}) endpoint: this.endpoint_,
},
true
)
const s3 = new aws.S3() const s3 = new aws.S3()
var params = { 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 export default DigitalOceanService
+1
View File
@@ -21,6 +21,7 @@
"@babel/core": "^7.14.3", "@babel/core": "^7.14.3",
"@babel/preset-typescript": "^7.13.0", "@babel/preset-typescript": "^7.13.0",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/multer": "^1.4.7",
"@types/jest": "^27.5.0", "@types/jest": "^27.5.0",
"@types/jsonwebtoken": "^8.5.5", "@types/jsonwebtoken": "^8.5.5",
"babel-preset-medusa-package": "^1.1.19", "babel-preset-medusa-package": "^1.1.19",
@@ -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<any>
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<T extends TransactionBaseService<any>>
extends TransactionBaseService<T> {
/**
* upload file to fileservice
* @param file Multer file from express multipart/form-data
* */
upload(file: Express.Multer.File): Promise<FileServiceUploadResult>
/**
* remove file from fileservice
* @param fileData Remove file described by record
* */
delete(fileData: Record<string, any>): 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<FileServiceGetUploadStreamResult>
/**
* 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<NodeJS.ReadableStream>
/**
* 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<string>
}
export abstract class AbstractFileService<T extends TransactionBaseService<any>>
extends TransactionBaseService<T>
implements IFileService<T>
{
abstract upload(
fileData: Express.Multer.File
): Promise<FileServiceUploadResult>
abstract delete(fileData: Record<string, any>): void
abstract getUploadStreamDescriptor(
fileData: UploadStreamDescriptorType
): Promise<FileServiceGetUploadStreamResult>
abstract getDownloadStream(
fileData: GetUploadedFileType
): Promise<NodeJS.ReadableStream>
abstract getPresignedDownloadUrl(
fileData: GetUploadedFileType
): Promise<string>
}
export const isFileService = (object: unknown): boolean => {
return object instanceof AbstractFileService
}
+1
View File
@@ -2,5 +2,6 @@ export * from "./tax-calculation-strategy"
export * from "./cart-completion-strategy" export * from "./cart-completion-strategy"
export * from "./tax-service" export * from "./tax-service"
export * from "./transaction-base-service" export * from "./transaction-base-service"
export * from "./file-service"
export * from "./models/base-entity" export * from "./models/base-entity"
export * from "./models/soft-deletable-entity" export * from "./models/soft-deletable-entity"
+112 -42
View File
@@ -1,5 +1,5 @@
import glob from "glob" import glob from "glob"
import { Express } from 'express' import { Express } from "express"
import { EntitySchema } from "typeorm" import { EntitySchema } from "typeorm"
import { import {
BaseService, BaseService,
@@ -16,9 +16,20 @@ import path from "path"
import fs from "fs" import fs from "fs"
import { asValue, asClass, asFunction, aliasTo } from "awilix" import { asValue, asClass, asFunction, aliasTo } from "awilix"
import { sync as existsSync } from "fs-exists-cached" 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 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" import { MiddlewareService } from "../services"
type Options = { type Options = {
@@ -40,7 +51,13 @@ type PluginDetails = {
/** /**
* Registers all services in the services directory * Registers all services in the services directory
*/ */
export default async ({ rootDirectory, container, app, configModule, activityId }: Options): Promise<void> => { export default async ({
rootDirectory,
container,
app,
configModule,
activityId,
}: Options): Promise<void> => {
const resolved = getResolvedPlugins(rootDirectory, configModule) || [] const resolved = getResolvedPlugins(rootDirectory, configModule) || []
await Promise.all( 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 { plugins } = configModule
const resolved = plugins.map((plugin) => { const resolved = plugins.map((plugin) => {
@@ -85,10 +105,14 @@ function getResolvedPlugins(rootDirectory: string, configModule: ConfigModule):
} }
export async function registerPluginModels({ export async function registerPluginModels({
rootDirectory, rootDirectory,
container, container,
configModule configModule,
}: { rootDirectory: string; container: MedusaContainer; configModule: ConfigModule; }): Promise<void> { }: {
rootDirectory: string
container: MedusaContainer
configModule: ConfigModule
}): Promise<void> {
const resolved = getResolvedPlugins(rootDirectory, configModule) || [] const resolved = getResolvedPlugins(rootDirectory, configModule) || []
await Promise.all( await Promise.all(
resolved.map(async (pluginDetails) => { resolved.map(async (pluginDetails) => {
@@ -97,7 +121,10 @@ export async function registerPluginModels({
) )
} }
async function runLoaders(pluginDetails: PluginDetails, container: MedusaContainer): Promise<void> { async function runLoaders(
pluginDetails: PluginDetails,
container: MedusaContainer
): Promise<void> {
const loaderFiles = glob.sync( const loaderFiles = glob.sync(
`${pluginDetails.resolve}/loaders/[!__]*.js`, `${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) registerMedusaMiddleware(pluginDetails, container)
registerStrategies(pluginDetails, container) registerStrategies(pluginDetails, container)
} }
function registerStrategies(pluginDetails: PluginDetails, container: MedusaContainer): void { function registerStrategies(
pluginDetails: PluginDetails,
container: MedusaContainer
): void {
let module let module
try { try {
const path = `${pluginDetails.resolve}/strategies/tax-calculation` 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 let module
try { try {
module = require(`${pluginDetails.resolve}/api/medusa-middleware`).default module = require(`${pluginDetails.resolve}/api/medusa-middleware`).default
@@ -158,7 +194,8 @@ function registerMedusaMiddleware(pluginDetails: PluginDetails, container: Medus
return return
} }
const middlewareService = container.resolve<MiddlewareService>("middlewareService") const middlewareService =
container.resolve<MiddlewareService>("middlewareService")
if (module.postAuthentication) { if (module.postAuthentication) {
middlewareService.addPostAuthentication( middlewareService.addPostAuthentication(
module.postAuthentication, module.postAuthentication,
@@ -178,8 +215,12 @@ function registerMedusaMiddleware(pluginDetails: PluginDetails, container: Medus
} }
} }
function registerCoreRouters(pluginDetails: PluginDetails, container: MedusaContainer): void { function registerCoreRouters(
const middlewareService = container.resolve<MiddlewareService>("middlewareService") pluginDetails: PluginDetails,
container: MedusaContainer
): void {
const middlewareService =
container.resolve<MiddlewareService>("middlewareService")
const { resolve } = pluginDetails const { resolve } = pluginDetails
const adminFiles = glob.sync(`${resolve}/api/admin/[!__]*.js`, {}) const adminFiles = glob.sync(`${resolve}/api/admin/[!__]*.js`, {})
const storeFiles = glob.sync(`${resolve}/api/store/[!__]*.js`, {}) const storeFiles = glob.sync(`${resolve}/api/store/[!__]*.js`, {})
@@ -245,16 +286,22 @@ function registerApi(
* registered * registered
* @return {void} * @return {void}
*/ */
export async function registerServices(pluginDetails: PluginDetails, container: MedusaContainer): Promise<void> { export async function registerServices(
pluginDetails: PluginDetails,
container: MedusaContainer
): Promise<void> {
const files = glob.sync(`${pluginDetails.resolve}/services/[!__]*.js`, {}) const files = glob.sync(`${pluginDetails.resolve}/services/[!__]*.js`, {})
await Promise.all( await Promise.all(
files.map(async (fn) => { files.map(async (fn) => {
const loaded = require(fn).default const loaded = require(fn).default
const name = formatRegistrationName(fn) const name = formatRegistrationName(fn)
if (!(loaded.prototype instanceof BaseService)) { if (
!(loaded.prototype instanceof BaseService) &&
!(loaded.prototype instanceof TransactionBaseService)
) {
const logger = container.resolve<Logger>("logger") const logger = container.resolve<Logger>("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) logger.error(message)
throw new Error(message) throw new Error(message)
} }
@@ -277,7 +324,8 @@ export async function registerServices(pluginDetails: PluginDetails, container:
} else if (loaded.prototype instanceof OauthService) { } else if (loaded.prototype instanceof OauthService) {
const appDetails = loaded.getAppDetails(pluginDetails.options) const appDetails = loaded.getAppDetails(pluginDetails.options)
const oauthService = container.resolve<typeof OauthService>("oauthService") const oauthService =
container.resolve<typeof OauthService>("oauthService")
await oauthService.registerOauthApp(appDetails) await oauthService.registerOauthApp(appDetails)
const name = appDetails.application_name const name = appDetails.application_name
@@ -324,6 +372,15 @@ export async function registerServices(pluginDetails: PluginDetails, container:
), ),
[`fileService`]: aliasTo(name), [`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) { } else if (loaded.prototype instanceof SearchService) {
// Add the service directly to the container in order to make simple // Add the service directly to the container in order to make simple
// resolution if we already know which search provider we need to use // resolution if we already know which search provider we need to use
@@ -365,7 +422,10 @@ export async function registerServices(pluginDetails: PluginDetails, container:
* registered * registered
* @return {void} * @return {void}
*/ */
function registerSubscribers(pluginDetails: PluginDetails, container: MedusaContainer): void { function registerSubscribers(
pluginDetails: PluginDetails,
container: MedusaContainer
): void {
const files = glob.sync(`${pluginDetails.resolve}/subscribers/*.js`, {}) const files = glob.sync(`${pluginDetails.resolve}/subscribers/*.js`, {})
files.forEach((fn) => { files.forEach((fn) => {
const loaded = require(fn).default const loaded = require(fn).default
@@ -387,19 +447,24 @@ function registerSubscribers(pluginDetails: PluginDetails, container: MedusaCont
* registered * registered
* @return {void} * @return {void}
*/ */
function registerRepositories(pluginDetails: PluginDetails, container: MedusaContainer): void { function registerRepositories(
pluginDetails: PluginDetails,
container: MedusaContainer
): void {
const files = glob.sync(`${pluginDetails.resolve}/repositories/*.js`, {}) const files = glob.sync(`${pluginDetails.resolve}/repositories/*.js`, {})
files.forEach((fn) => { files.forEach((fn) => {
const loaded = require(fn) as ClassConstructor<unknown> const loaded = require(fn) as ClassConstructor<unknown>
Object.entries(loaded).map(([, val]: [string, ClassConstructor<unknown>]) => { Object.entries(loaded).map(
if (typeof val === "function") { ([, val]: [string, ClassConstructor<unknown>]) => {
const name = formatRegistrationName(fn) if (typeof val === "function") {
container.register({ const name = formatRegistrationName(fn)
[name]: asClass(val), container.register({
}) [name]: asClass(val),
})
}
} }
}) )
}) })
} }
@@ -414,21 +479,26 @@ function registerRepositories(pluginDetails: PluginDetails, container: MedusaCon
* registered * registered
* @return {void} * @return {void}
*/ */
function registerModels(pluginDetails: PluginDetails, container: MedusaContainer): void { function registerModels(
pluginDetails: PluginDetails,
container: MedusaContainer
): void {
const files = glob.sync(`${pluginDetails.resolve}/models/*.js`, {}) const files = glob.sync(`${pluginDetails.resolve}/models/*.js`, {})
files.forEach((fn) => { files.forEach((fn) => {
const loaded = require(fn) as ClassConstructor<unknown> | EntitySchema const loaded = require(fn) as ClassConstructor<unknown> | EntitySchema
Object.entries(loaded).map(([, val]: [string, ClassConstructor<unknown> | EntitySchema]) => { Object.entries(loaded).map(
if (typeof val === "function" || val instanceof EntitySchema) { ([, val]: [string, ClassConstructor<unknown> | EntitySchema]) => {
const name = formatRegistrationName(fn) if (typeof val === "function" || val instanceof EntitySchema) {
container.register({ const name = formatRegistrationName(fn)
[name]: asValue(val), 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 * @return {object} the plugin details
*/ */
function resolvePlugin(pluginName: string): { function resolvePlugin(pluginName: string): {
resolve: string; resolve: string
id: string; id: string
name: string; name: string
options: Record<string, unknown> options: Record<string, unknown>
version: string; version: string
} { } {
// Only find plugins when we're not given an absolute path // Only find plugins when we're not given an absolute path
if (!existsSync(pluginName)) { if (!existsSync(pluginName)) {