diff --git a/packages/file/src/loaders/index.ts b/packages/file/src/loaders/index.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/file/src/loaders/providers.ts b/packages/file/src/loaders/providers.ts new file mode 100644 index 0000000000..eb5411784e --- /dev/null +++ b/packages/file/src/loaders/providers.ts @@ -0,0 +1,40 @@ +import { moduleProviderLoader } from "@medusajs/modules-sdk" +import { LoaderOptions, ModuleProvider, ModulesSdkTypes } from "@medusajs/types" +import { FileProviderService } from "@services" +import { FileProviderIdentifierRegistrationName } from "@types" +import { Lifetime, asFunction, asValue } from "awilix" + +const registrationFn = async (klass, container, pluginOptions) => { + Object.entries(pluginOptions.config || []).map(([name, config]) => { + const key = FileProviderService.getRegistrationIdentifier(klass, name) + + container.register({ + ["file_" + key]: asFunction((cradle) => new klass(cradle, config), { + lifetime: klass.LIFE_TIME || Lifetime.SINGLETON, + }), + }) + + container.registerAdd(FileProviderIdentifierRegistrationName, asValue(key)) + }) +} + +export default async ({ + container, + options, +}: LoaderOptions< + ( + | ModulesSdkTypes.ModuleServiceInitializeOptions + | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions + ) & { provider: ModuleProvider } +>): Promise => { + container.registerAdd( + FileProviderIdentifierRegistrationName, + asValue(undefined) + ) + + await moduleProviderLoader({ + container, + providers: options?.provider ? [options?.provider] : [], + registerServiceFn: registrationFn, + }) +} diff --git a/packages/file/src/module-definition.ts b/packages/file/src/module-definition.ts index 5c2f44f277..bee485d071 100644 --- a/packages/file/src/module-definition.ts +++ b/packages/file/src/module-definition.ts @@ -1,5 +1,7 @@ import { ModuleExports } from "@medusajs/types" import { FileModuleService } from "@services" +import loadProviders from "./loaders/providers" + export const runMigrations = () => { return Promise.resolve() } @@ -8,7 +10,7 @@ export const revertMigration = () => { } const service = FileModuleService -const loaders = [] as any +const loaders = [loadProviders] as any export const moduleDefinition: ModuleExports = { service, diff --git a/packages/file/src/services/file-module-service.ts b/packages/file/src/services/file-module-service.ts index 6cde4fcdcc..ff7f48ab06 100644 --- a/packages/file/src/services/file-module-service.ts +++ b/packages/file/src/services/file-module-service.ts @@ -6,12 +6,16 @@ import { } from "@medusajs/types" import { joinerConfig } from "../joiner-config" +import FileProviderService from "./file-provider-service" + +type InjectedDependencies = { + fileProviderService: FileProviderService +} export default class FileModuleService { - constructor() { - // @ts-ignore - // eslint-disable-next-line prefer-rest-params - super(...arguments) + protected readonly fileProviderService_: FileProviderService + constructor({ fileProviderService }: InjectedDependencies) { + this.fileProviderService_ = fileProviderService } __joinerConfig(): ModuleJoinerConfig { @@ -25,18 +29,37 @@ export default class FileModuleService { data: CreateFileDTO[] | CreateFileDTO ): Promise { const input = Array.isArray(data) ? data : [data] - const files = [] - return Array.isArray(data) ? files : files[0] + const files = await Promise.all( + input.map((file) => this.fileProviderService_.upload(file)) + ) + const result = files.map((file) => ({ + id: file.key, + url: file.url, + })) + + return Array.isArray(data) ? result : result[0] } async delete(ids: string[], sharedContext?: Context): Promise async delete(id: string, sharedContext?: Context): Promise async delete(ids: string[] | string): Promise { + const input = Array.isArray(ids) ? ids : [ids] + await Promise.all( + input.map((id) => this.fileProviderService_.delete({ fileKey: id })) + ) + return } async retrieve(id: string): Promise async retrieve(id: string): Promise { - return {} as FileDTO + const res = await this.fileProviderService_.getPresignedDownloadUrl({ + fileKey: id, + }) + + return { + id, + url: res, + } } } diff --git a/packages/file/src/services/file-provider-service.ts b/packages/file/src/services/file-provider-service.ts new file mode 100644 index 0000000000..c4a291de83 --- /dev/null +++ b/packages/file/src/services/file-provider-service.ts @@ -0,0 +1,44 @@ +import { Constructor, DAL, FileTypes } from "@medusajs/types" +import { MedusaError } from "medusa-core-utils" + +type InjectedDependencies = { + [key: `file_${string}`]: FileTypes.IFileProvider +} + +export default class FileProviderService { + protected readonly fileProvider_: FileTypes.IFileProvider + + constructor(container: InjectedDependencies) { + if (Object.keys(container).length !== 1) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `File module should only be initialized with one provider` + ) + } + + this.fileProvider_ = Object.values(container)[0] + } + + static getRegistrationIdentifier( + providerClass: Constructor, + optionName?: string + ) { + return `${(providerClass as any).identifier}_${optionName}` + } + + upload( + file: FileTypes.ProviderUploadFileDTO + ): Promise { + return this.fileProvider_.upload(file) + } + + delete(fileData: FileTypes.ProviderDeleteFileDTO): Promise { + return this.fileProvider_.delete(fileData) + } + + getPresignedDownloadUrl( + fileData: FileTypes.ProviderGetFileDTO + ): Promise { + return this.fileProvider_.getPresignedDownloadUrl(fileData) + } +} diff --git a/packages/file/src/services/index.ts b/packages/file/src/services/index.ts index e1831dc30e..cd04552176 100644 --- a/packages/file/src/services/index.ts +++ b/packages/file/src/services/index.ts @@ -1 +1,2 @@ export { default as FileModuleService } from "./file-module-service" +export { default as FileProviderService } from "./file-provider-service" diff --git a/packages/file/src/types/index.ts b/packages/file/src/types/index.ts new file mode 100644 index 0000000000..9527a7bdc9 --- /dev/null +++ b/packages/file/src/types/index.ts @@ -0,0 +1,25 @@ +import { + ModuleProviderExports, + ModuleServiceInitializeOptions, +} from "@medusajs/types" + +export const FileProviderIdentifierRegistrationName = + "file_providers_identifier" + +export type FileModuleOptions = Partial & { + /** + * Providers to be registered + */ + provider?: { + /** + * The module provider to be registered + */ + resolve: string | ModuleProviderExports + options: { + /** + * key value pair of the provider name and the configuration to be passed to the provider constructor + */ + config: Record + } + } +} diff --git a/packages/types/src/file/index.ts b/packages/types/src/file/index.ts index 0c73656566..a8cf1df979 100644 --- a/packages/types/src/file/index.ts +++ b/packages/types/src/file/index.ts @@ -1,3 +1,4 @@ export * from "./common" export * from "./mutations" export * from "./service" +export * from "./provider" diff --git a/packages/types/src/file/provider.ts b/packages/types/src/file/provider.ts new file mode 100644 index 0000000000..074b6f189a --- /dev/null +++ b/packages/types/src/file/provider.ts @@ -0,0 +1,105 @@ +/** + * @interface + * + * Details of a file upload's result. + */ +export type ProviderFileResultDTO = { + /** + * The file's URL. + */ + url: string + /** + * The file's key. This key is used in other operations, + * such as deleting a file. + */ + key: string +} + +/** + * @interface + * + * The details of a file to retrieve. + */ +export type ProviderGetFileDTO = { + /** + * The file's key. + */ + fileKey: string + /** + * Whether the file is private. + */ + isPrivate?: boolean + [x: string]: unknown +} + +/** + * @interface + * + * The details of the file to remove. + */ +export type ProviderDeleteFileDTO = { + /** + * The file's key. When uploading a file, the + * returned key is used here. + */ + fileKey: string + [x: string]: unknown +} + +/** + * @interface + * + * The details of the file to create. + */ +export type ProviderUploadFileDTO = { + /** + * The filename of the uploaded file + */ + filename: string + + /** + * The mimetype of the uploaded file + */ + mimeType: string + + /** + * The file content + */ + content: Blob +} + +/** + * ## Overview + * + * File provider interface for the file module. + * + */ +export interface IFileProvider { + /** + * This method is used to upload a file + * + * @param {ProviderUploadFileDTO} file - The contents and metadata of the file. + * Among the file’s details, you can access the file’s path in the `path` property of the file object. + * @returns {Promise} The details of the upload's result. + * + */ + upload(file: ProviderUploadFileDTO): Promise + /** + * This method is used to delete a file from storage. + * + * @param {ProviderDeleteFileDTO} fileData - The details of the file to remove. + * @returns {Promise} Resolves when the file is deleted successfully. + * + */ + delete(fileData: ProviderDeleteFileDTO): Promise + /** + * This 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. + * + * @param {ProviderGetFileDTO} fileData - The details of the file. + * @returns {Promise} The presigned URL to download the file + * + */ + getPresignedDownloadUrl(fileData: ProviderGetFileDTO): Promise +}