feat: Add local file provider and wire everything up in the file module (#7134)

This commit is contained in:
Stevche Radevski
2024-04-24 10:59:58 +02:00
committed by GitHub
parent fe68b5c0f2
commit 614d659a59
35 changed files with 684 additions and 49 deletions
@@ -0,0 +1,32 @@
import { FileTypes } from "@medusajs/types"
import { AbstractFileProviderService } from "@medusajs/utils"
export class FileProviderServiceFixtures extends AbstractFileProviderService {
static identifier = "fixtures-file-provider"
protected storage = {}
async upload(
file: FileTypes.ProviderUploadFileDTO
): Promise<FileTypes.ProviderFileResultDTO> {
this.storage[file.filename] = file.content
return {
url: file.filename,
key: file.filename,
}
}
async delete(file: FileTypes.ProviderDeleteFileDTO): Promise<void> {
delete this.storage[file.fileKey]
return
}
async getPresignedDownloadUrl(
fileData: FileTypes.ProviderGetFileDTO
): Promise<string> {
if (this.storage[fileData.fileKey]) {
return this.storage[fileData.fileKey]
}
return ""
}
}
export const services = [FileProviderServiceFixtures]
@@ -0,0 +1 @@
export * from "./default-provider"
@@ -1,5 +1,56 @@
import { resolve } from "path"
import { Modules } from "@medusajs/utils"
import { SuiteOptions, moduleIntegrationTestRunner } from "medusa-test-utils"
import { Entity, PrimaryKey } from "@mikro-orm/core"
jest.setTimeout(100000)
describe("File Module Service", () => {
it("noop", function () {})
// The test runner throws if a model is not passed, so we create a dummy entity
@Entity({ tableName: "dummy_file_entity" })
export default class DummyEntity {
@PrimaryKey()
id: string
}
const moduleOptions = {
providers: [
{
resolve: resolve(
process.cwd() +
"/integration-tests/__fixtures__/providers/default-provider"
),
options: {
config: {
"default-provider": {},
},
},
},
],
}
moduleIntegrationTestRunner({
moduleName: Modules.FILE,
moduleOptions: moduleOptions,
moduleModels: [DummyEntity],
// TODO: Fix the type of service, it complains for some reason if we pass IFileModuleService
testSuite: ({ service }: SuiteOptions<any>) => {
describe("File Module Service", () => {
it("creates and gets a file", async () => {
const res = await service.create({
filename: "test.jpg",
mimeType: "image/jpeg",
content: Buffer.from("test"),
})
expect(res).toEqual({
id: "test.jpg",
url: "test.jpg",
})
// The fake provider returns the file content as the url
const downloadUrl = await service.retrieve("test.jpg")
expect(await new Response(downloadUrl.url).text()).toEqual("test")
})
})
},
})
+12 -9
View File
@@ -1,10 +1,13 @@
import {
moduleDefinition,
revertMigration,
runMigrations,
} from "./module-definition"
export default moduleDefinition
export { revertMigration, runMigrations }
import { moduleDefinition } from "./module-definition"
import { initializeFactory, Modules } from "@medusajs/modules-sdk"
export * from "./types"
export * from "./services"
export const initialize = initializeFactory({
moduleName: Modules.FILE,
moduleDefinition,
})
export const runMigrations = moduleDefinition.runMigrations
export const revertMigration = moduleDefinition.revertMigration
export default moduleDefinition
+12 -11
View File
@@ -1,7 +1,10 @@
import { moduleProviderLoader } from "@medusajs/modules-sdk"
import { LoaderOptions, ModuleProvider, ModulesSdkTypes } from "@medusajs/types"
import { FileProviderService } from "@services"
import { FileProviderIdentifierRegistrationName } from "@types"
import {
FileProviderIdentifierRegistrationName,
FileProviderRegistrationPrefix,
} from "@types"
import { Lifetime, asFunction, asValue } from "awilix"
const registrationFn = async (klass, container, pluginOptions) => {
@@ -9,9 +12,12 @@ const registrationFn = async (klass, container, pluginOptions) => {
const key = FileProviderService.getRegistrationIdentifier(klass, name)
container.register({
["file_" + key]: asFunction((cradle) => new klass(cradle, config), {
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
}),
[FileProviderRegistrationPrefix + key]: asFunction(
(cradle) => new klass(cradle, config),
{
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
}
),
})
container.registerAdd(FileProviderIdentifierRegistrationName, asValue(key))
@@ -25,16 +31,11 @@ export default async ({
(
| ModulesSdkTypes.ModuleServiceInitializeOptions
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
) & { provider: ModuleProvider }
) & { providers: ModuleProvider[] }
>): Promise<void> => {
container.registerAdd(
FileProviderIdentifierRegistrationName,
asValue(undefined)
)
await moduleProviderLoader({
container,
providers: options?.provider ? [options?.provider] : [],
providers: options?.providers || [],
registerServiceFn: registrationFn,
})
}
+10 -2
View File
@@ -1,6 +1,8 @@
import { ModuleExports } from "@medusajs/types"
import { FileModuleService } from "@services"
import loadProviders from "./loaders/providers"
import * as ModuleServices from "@services"
import { ModulesSdkUtils } from "@medusajs/utils"
export const runMigrations = () => {
return Promise.resolve()
@@ -9,9 +11,15 @@ export const revertMigration = () => {
return Promise.resolve()
}
const service = FileModuleService
const loaders = [loadProviders] as any
const containerLoader = ModulesSdkUtils.moduleContainerLoaderFactory({
moduleModels: {},
moduleRepositories: {},
moduleServices: ModuleServices,
})
const loaders = [containerLoader, loadProviders] as any
const service = FileModuleService
export const moduleDefinition: ModuleExports = {
service,
loaders,
@@ -3,16 +3,20 @@ import {
CreateFileDTO,
FileDTO,
ModuleJoinerConfig,
FileTypes,
FilterableFileProps,
FindConfig,
} from "@medusajs/types"
import { joinerConfig } from "../joiner-config"
import FileProviderService from "./file-provider-service"
import { MedusaError } from "medusa-core-utils"
type InjectedDependencies = {
fileProviderService: FileProviderService
}
export default class FileModuleService {
export default class FileModuleService implements FileTypes.IFileModuleService {
protected readonly fileProviderService_: FileProviderService
constructor({ fileProviderService }: InjectedDependencies) {
this.fileProviderService_ = fileProviderService
@@ -51,7 +55,6 @@ export default class FileModuleService {
return
}
async retrieve(id: string): Promise<FileDTO>
async retrieve(id: string): Promise<FileDTO> {
const res = await this.fileProviderService_.getPresignedDownloadUrl({
fileKey: id,
@@ -62,4 +65,65 @@ export default class FileModuleService {
url: res,
}
}
async list(
filters?: FilterableFileProps,
config?: FindConfig<FileDTO>,
sharedContext?: Context
): Promise<FileDTO[]> {
const id = Array.isArray(filters?.id) ? filters?.id?.[0] : filters?.id
if (!id) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Listing of files is only supported when filtering by ID."
)
}
const res = await this.fileProviderService_.getPresignedDownloadUrl({
fileKey: id,
})
if (!res) {
return []
}
return [
{
id,
url: res,
},
]
}
async listAndCount(
filters?: FilterableFileProps,
config?: FindConfig<FileDTO>,
sharedContext?: Context
): Promise<[FileDTO[], number]> {
const id = Array.isArray(filters?.id) ? filters?.id?.[0] : filters?.id
if (!id) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Listing and counting of files is only supported when filtering by ID."
)
}
const res = await this.fileProviderService_.getPresignedDownloadUrl({
fileKey: id,
})
if (!res) {
return [[], 0]
}
return [
[
{
id,
url: res,
},
],
1,
]
}
}
@@ -1,22 +1,29 @@
import { Constructor, DAL, FileTypes } from "@medusajs/types"
import { MedusaError } from "medusa-core-utils"
import { FileProviderRegistrationPrefix } from "@types"
type InjectedDependencies = {
[key: `file_${string}`]: FileTypes.IFileProvider
[
key: `${typeof FileProviderRegistrationPrefix}${string}`
]: FileTypes.IFileProvider
}
export default class FileProviderService {
protected readonly fileProvider_: FileTypes.IFileProvider
constructor(container: InjectedDependencies) {
if (Object.keys(container).length !== 1) {
const fileProviderKeys = Object.keys(container).filter((k) =>
k.startsWith(FileProviderRegistrationPrefix)
)
if (fileProviderKeys.length !== 1) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`File module should only be initialized with one provider`
`File module should be initialized with exactly one provider`
)
}
this.fileProvider_ = Object.values(container)[0]
this.fileProvider_ = container[fileProviderKeys[0]]
}
static getRegistrationIdentifier(
+2
View File
@@ -6,6 +6,8 @@ import {
export const FileProviderIdentifierRegistrationName =
"file_providers_identifier"
export const FileProviderRegistrationPrefix = "fs_"
export type FileModuleOptions = Partial<ModuleServiceInitializeOptions> & {
/**
* Providers to be registered