From 8add02f9e9829620eaded608319d4ddd815aaf11 Mon Sep 17 00:00:00 2001 From: geoffreyfarel Date: Tue, 21 Oct 2025 11:15:32 +0800 Subject: [PATCH 01/12] merge --- .../storage-base-nest-js-module/README.md | 98 +++++++++++++++++++ .../storage-base-nest-js-module/package.json | 29 ++++++ .../storage-base-nest-js-module/project.json | 23 +++++ .../storage-base-nest-js-module/src/index.ts | 3 + .../src/services/storage.service.ts | 13 +++ .../src/storages-base.module.ts | 67 +++++++++++++ .../storage-base-module-options.interface.ts | 33 +++++++ .../typings/storages-base-module-providers.ts | 3 + .../tsconfig.build.json | 10 ++ 9 files changed, 279 insertions(+) create mode 100644 packages/storage-base-nest-js-module/README.md create mode 100644 packages/storage-base-nest-js-module/package.json create mode 100644 packages/storage-base-nest-js-module/project.json create mode 100644 packages/storage-base-nest-js-module/src/index.ts create mode 100644 packages/storage-base-nest-js-module/src/services/storage.service.ts create mode 100644 packages/storage-base-nest-js-module/src/storages-base.module.ts create mode 100644 packages/storage-base-nest-js-module/src/typings/storage-base-module-options.interface.ts create mode 100644 packages/storage-base-nest-js-module/src/typings/storages-base-module-providers.ts create mode 100644 packages/storage-base-nest-js-module/tsconfig.build.json diff --git a/packages/storage-base-nest-js-module/README.md b/packages/storage-base-nest-js-module/README.md new file mode 100644 index 00000000..da24eaa8 --- /dev/null +++ b/packages/storage-base-nest-js-module/README.md @@ -0,0 +1,98 @@ +

+ Nest Logo +

+ +[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 +[circleci-url]: https://circleci.com/gh/nestjs/nest + +

A progressive Node.js framework for building efficient and scalable server-side applications.

+

+NPM Version +Package License +NPM Downloads +CircleCI +Discord +Backers on Open Collective +Sponsors on Open Collective + Donate us + Support us + Follow us on Twitter +

+ + +## Description + +[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. + +## Project setup + +```bash +$ yarn install +``` + +## Compile and run the project + +```bash +# development +$ yarn run start + +# watch mode +$ yarn run start:dev + +# production mode +$ yarn run start:prod +``` + +## Run tests + +```bash +# unit tests +$ yarn run test + +# e2e tests +$ yarn run test:e2e + +# test coverage +$ yarn run test:cov +``` + +## Deployment + +When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information. + +If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps: + +```bash +$ yarn install -g @nestjs/mau +$ mau deploy +``` + +With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure. + +## Resources + +Check out a few resources that may come in handy when working with NestJS: + +- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework. +- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy). +- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/). +- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks. +- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com). +- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com). +- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs). +- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com). + +## Support + +Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). + +## Stay in touch + +- Author - [Kamil Myƛliwiec](https://twitter.com/kammysliwiec) +- Website - [https://nestjs.com](https://nestjs.com/) +- Twitter - [@nestframework](https://twitter.com/nestframework) + +## License + +Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE). diff --git a/packages/storage-base-nest-js-module/package.json b/packages/storage-base-nest-js-module/package.json new file mode 100644 index 00000000..bb6553f0 --- /dev/null +++ b/packages/storage-base-nest-js-module/package.json @@ -0,0 +1,29 @@ +{ + "name": "@rytass/storages-base-nestjs-module", + "version": "0.0.1", + "description": "Rytass Utils Storage NestJS Base Module", + "keywords": [ + "rytass", + "storages", + "nestjs" + ], + "author": "", + "license": "MIT", + "scripts": { + "clean": "nx clean", + "build": "nx build", + "lint": "nx lint", + "test": "nx test", + "test:watch": "nx test:watch" + }, + "bugs": { + "url": "https://github.com/Rytass/Utils/issues" + }, + "dependencies": { + "@rytass/storages": "^0.2.4", + "@rytass/storages-adapter-azure-blob": "^0.1.6", + "@rytass/storages-adapter-gcs": "^0.2.7", + "@rytass/storages-adapter-local": "0.2.7", + "@rytass/storages-adapter-s3": "^0.3.7" + } +} diff --git a/packages/storage-base-nest-js-module/project.json b/packages/storage-base-nest-js-module/project.json new file mode 100644 index 00000000..4d2cb371 --- /dev/null +++ b/packages/storage-base-nest-js-module/project.json @@ -0,0 +1,23 @@ +{ + "name": "@rytass/storages-base-nestjs-module", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/storages-base-nestjs-module/src", + "targets": { + "test": { + "executor": "@nx/jest:jest" + }, + "test:watch": { + "executor": "@nx/jest:jest" + }, + "lint": { + "executor": "nx:run-commands" + }, + "clean": { + "executor": "nx:run-commands" + }, + "build": { + "executor": "nx:run-commands" + } + } +} diff --git a/packages/storage-base-nest-js-module/src/index.ts b/packages/storage-base-nest-js-module/src/index.ts new file mode 100644 index 00000000..745af1d1 --- /dev/null +++ b/packages/storage-base-nest-js-module/src/index.ts @@ -0,0 +1,3 @@ +export * from './storages-base.module'; + +export * from './services/storage.service'; diff --git a/packages/storage-base-nest-js-module/src/services/storage.service.ts b/packages/storage-base-nest-js-module/src/services/storage.service.ts new file mode 100644 index 00000000..d3fae543 --- /dev/null +++ b/packages/storage-base-nest-js-module/src/services/storage.service.ts @@ -0,0 +1,13 @@ +import { Inject, Injectable, Logger, Type } from '@nestjs/common'; +import { STORAGE_ADAPTER } from '../typings/storages-base-module-providers'; +import { StorageAdapter } from '../typings/storage-base-module-options.interface'; + +@Injectable() +export class StorageService { + private readonly logger = new Logger(StorageService.name); + + // constructor( + // // @Inject(STORAGE_ADAPTER) + // // private readonly adapter: Type, + // ) {} +} diff --git a/packages/storage-base-nest-js-module/src/storages-base.module.ts b/packages/storage-base-nest-js-module/src/storages-base.module.ts new file mode 100644 index 00000000..3279d2f4 --- /dev/null +++ b/packages/storage-base-nest-js-module/src/storages-base.module.ts @@ -0,0 +1,67 @@ +import { DynamicModule, Module, Provider, Type } from '@nestjs/common'; +import { + StorageBaseModuleAsyncOptions, + StorageBaseModuleOptions, + StorageBaseModuleOptionsFactory, +} from './typings/storage-base-module-options.interface'; +import { STORAGE_MODULE_OPTIONS } from './typings/storages-base-module-providers'; +import { StorageService } from './services/storage.service'; + +@Module({}) +export class StorageBaseModule { + static forRoot(options: StorageBaseModuleOptions): DynamicModule { + return { + module: StorageBaseModule, + providers: [ + { + provide: STORAGE_MODULE_OPTIONS, + useValue: options, + }, + StorageService, + ], + exports: [StorageService], + }; + } + static forRootAsync(options: StorageBaseModuleAsyncOptions): DynamicModule { + return { + module: StorageBaseModule, + imports: [...(options?.imports ?? [])], + providers: [...this.createAsyncProvider(options), StorageService], + exports: [StorageService], + }; + } + + private static createAsyncProvider(options: StorageBaseModuleAsyncOptions): Provider[] { + if (options.useExisting || options.useFactory) { + return [this.createAsyncOptionsProvider(options)]; + } + + return [ + this.createAsyncOptionsProvider(options), + ...(options.useClass + ? [ + { + provide: options.useClass, + useClass: options.useClass, + }, + ] + : []), + ]; + } + private static createAsyncOptionsProvider(options: StorageBaseModuleAsyncOptions): Provider { + if (options.useFactory) { + return { + provide: STORAGE_MODULE_OPTIONS, + useFactory: options.useFactory, + inject: options.inject || [], + }; + } + + return { + provide: STORAGE_MODULE_OPTIONS, + useFactory: async (optionsFactory: StorageBaseModuleOptionsFactory) => + await optionsFactory.createStorageBaseModuleOptions(), + inject: [(options.useExisting || options.useClass) as Type], + }; + } +} diff --git a/packages/storage-base-nest-js-module/src/typings/storage-base-module-options.interface.ts b/packages/storage-base-nest-js-module/src/typings/storage-base-module-options.interface.ts new file mode 100644 index 00000000..586af1d6 --- /dev/null +++ b/packages/storage-base-nest-js-module/src/typings/storage-base-module-options.interface.ts @@ -0,0 +1,33 @@ +import { InjectionToken, ModuleMetadata, OptionalFactoryDependency, Type } from '@nestjs/common'; +import { StorageAzureBlobService } from '@rytass/storages-adapter-azure-blob'; +import { StorageGCSService } from '@rytass/storages-adapter-gcs'; +import { LocalStorage } from '@rytass/storages-adapter-local'; +import { StorageR2Service } from '@rytass/storages-adapter-r2'; +import { StorageS3Service } from '@rytass/storages-adapter-s3'; + +export type StorageAdapter = + | LocalStorage + | StorageAzureBlobService + | StorageGCSService + | StorageR2Service + | StorageS3Service; + +export interface StorageBaseModuleOptions { + adapter: StorageAdapter; + formDataFieldName?: string; // default: 'files' + allowMultiple?: boolean; // default: true + MaxFileSizeInBytes?: number; // default: 10 * 1024 * 1024 + defaultPublic?: boolean; // default: false +} + +export interface StorageBaseModuleOptionsFactory { + createStorageBaseModuleOptions(): Promise | StorageBaseModuleOptions; +} + +export interface StorageBaseModuleAsyncOptions extends Pick { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + useFactory?: (...args: any[]) => Promise | StorageBaseModuleOptions; + inject?: (InjectionToken | OptionalFactoryDependency)[]; + useClass?: Type; + useExisting?: Type; +} diff --git a/packages/storage-base-nest-js-module/src/typings/storages-base-module-providers.ts b/packages/storage-base-nest-js-module/src/typings/storages-base-module-providers.ts new file mode 100644 index 00000000..5440f177 --- /dev/null +++ b/packages/storage-base-nest-js-module/src/typings/storages-base-module-providers.ts @@ -0,0 +1,3 @@ +export const STORAGE_MODULE_OPTIONS = Symbol('STORAGE_BASE_MODULE_OPTIONS'); + +export const STORAGE_ADAPTER = Symbol('STORAGE_ADAPTER'); diff --git a/packages/storage-base-nest-js-module/tsconfig.build.json b/packages/storage-base-nest-js-module/tsconfig.build.json new file mode 100644 index 00000000..06d1953d --- /dev/null +++ b/packages/storage-base-nest-js-module/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.node.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declaration": true + }, + "exclude": ["**/*.spec.*"], + "include": ["./src"] +} From 4b31c05708c4c6b9043cc8a65c6a300a04c8dd70 Mon Sep 17 00:00:00 2001 From: geoffreyfarel Date: Tue, 21 Oct 2025 17:43:32 +0800 Subject: [PATCH 02/12] feat: storages-base-service, update storages-base-module --- .../src/services/storage.service.ts | 13 ---- .../README.md | 0 .../package.json | 0 .../project.json | 0 .../src/index.ts | 0 .../src/services/storages-base.service.ts | 64 +++++++++++++++++++ .../src/storages-base.module.ts | 48 +++++++++++--- .../storage-base-module-options.interface.ts | 3 +- .../typings/storages-base-module-providers.ts | 0 .../tsconfig.build.json | 0 tsconfig.json | 1 + 11 files changed, 105 insertions(+), 24 deletions(-) delete mode 100644 packages/storage-base-nest-js-module/src/services/storage.service.ts rename packages/{storage-base-nest-js-module => storages-base-nest-js-module}/README.md (100%) rename packages/{storage-base-nest-js-module => storages-base-nest-js-module}/package.json (100%) rename packages/{storage-base-nest-js-module => storages-base-nest-js-module}/project.json (100%) rename packages/{storage-base-nest-js-module => storages-base-nest-js-module}/src/index.ts (100%) create mode 100644 packages/storages-base-nest-js-module/src/services/storages-base.service.ts rename packages/{storage-base-nest-js-module => storages-base-nest-js-module}/src/storages-base.module.ts (57%) rename packages/{storage-base-nest-js-module => storages-base-nest-js-module}/src/typings/storage-base-module-options.interface.ts (95%) rename packages/{storage-base-nest-js-module => storages-base-nest-js-module}/src/typings/storages-base-module-providers.ts (100%) rename packages/{storage-base-nest-js-module => storages-base-nest-js-module}/tsconfig.build.json (100%) diff --git a/packages/storage-base-nest-js-module/src/services/storage.service.ts b/packages/storage-base-nest-js-module/src/services/storage.service.ts deleted file mode 100644 index d3fae543..00000000 --- a/packages/storage-base-nest-js-module/src/services/storage.service.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Inject, Injectable, Logger, Type } from '@nestjs/common'; -import { STORAGE_ADAPTER } from '../typings/storages-base-module-providers'; -import { StorageAdapter } from '../typings/storage-base-module-options.interface'; - -@Injectable() -export class StorageService { - private readonly logger = new Logger(StorageService.name); - - // constructor( - // // @Inject(STORAGE_ADAPTER) - // // private readonly adapter: Type, - // ) {} -} diff --git a/packages/storage-base-nest-js-module/README.md b/packages/storages-base-nest-js-module/README.md similarity index 100% rename from packages/storage-base-nest-js-module/README.md rename to packages/storages-base-nest-js-module/README.md diff --git a/packages/storage-base-nest-js-module/package.json b/packages/storages-base-nest-js-module/package.json similarity index 100% rename from packages/storage-base-nest-js-module/package.json rename to packages/storages-base-nest-js-module/package.json diff --git a/packages/storage-base-nest-js-module/project.json b/packages/storages-base-nest-js-module/project.json similarity index 100% rename from packages/storage-base-nest-js-module/project.json rename to packages/storages-base-nest-js-module/project.json diff --git a/packages/storage-base-nest-js-module/src/index.ts b/packages/storages-base-nest-js-module/src/index.ts similarity index 100% rename from packages/storage-base-nest-js-module/src/index.ts rename to packages/storages-base-nest-js-module/src/index.ts diff --git a/packages/storages-base-nest-js-module/src/services/storages-base.service.ts b/packages/storages-base-nest-js-module/src/services/storages-base.service.ts new file mode 100644 index 00000000..13a73713 --- /dev/null +++ b/packages/storages-base-nest-js-module/src/services/storages-base.service.ts @@ -0,0 +1,64 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { STORAGE_ADAPTER, STORAGE_MODULE_OPTIONS } from '../typings/storages-base-module-providers'; +import type { StorageAdapter, StorageBaseModuleOptions } from '../typings/storage-base-module-options.interface'; +import { InputFile, StorageFile, WriteFileOptions } from '@rytass/storages'; +import { LocalStorage } from '@rytass/storages-adapter-local'; +import { StorageAzureBlobService } from '@rytass/storages-adapter-azure-blob'; +import { StorageGCSService } from '@rytass/storages-adapter-gcs'; +import { StorageS3Service } from '@rytass/storages-adapter-s3'; +import { StorageR2Service } from '@rytass/storages-adapter-r2'; +import { PresignedURLOptions } from 'storages-adapter-r2/src/typings'; + +@Injectable() +export class StorageService { + private readonly logger = new Logger(StorageService.name); + + constructor( + @Inject(STORAGE_ADAPTER) + private readonly _adapter: StorageAdapter, + @Inject(STORAGE_MODULE_OPTIONS) + private readonly _options: StorageBaseModuleOptions, + ) { + this.logger.log(`Storage adapter: ${this._adapter.constructor.name}`); + } + + async url(key: string): Promise; + async url(key: string, expires: number): Promise; + async url(key: string, options?: PresignedURLOptions): Promise; + + async url(key: string, params?: number | PresignedURLOptions): Promise { + if (this._adapter instanceof LocalStorage) { + throw new Error('LocalStorage does not support URL generation'); + } + + if (this._adapter instanceof StorageAzureBlobService || this._adapter instanceof StorageGCSService) { + const expires = params as number; + + return this._adapter.url(key, expires); + } else if (this._adapter instanceof StorageS3Service) { + return this._adapter.url(key); + } else if (this._adapter instanceof StorageR2Service) { + const options = params as PresignedURLOptions; + + return this._adapter.url(key, options); + } else { + throw new Error('Unknown storage adapter'); + } + } + + async write(file: InputFile, options?: WriteFileOptions): Promise { + return this._adapter.write(file, options); + } + + async batchWrite(files: InputFile[], options?: WriteFileOptions[]): Promise { + return this._adapter.batchWrite(files, options); + } + + async remove(key: string): Promise { + return this._adapter.remove(key); + } + + async isExists(key: string): Promise { + return this._adapter.isExists(key); + } +} diff --git a/packages/storage-base-nest-js-module/src/storages-base.module.ts b/packages/storages-base-nest-js-module/src/storages-base.module.ts similarity index 57% rename from packages/storage-base-nest-js-module/src/storages-base.module.ts rename to packages/storages-base-nest-js-module/src/storages-base.module.ts index 3279d2f4..497c9eeb 100644 --- a/packages/storage-base-nest-js-module/src/storages-base.module.ts +++ b/packages/storages-base-nest-js-module/src/storages-base.module.ts @@ -1,32 +1,60 @@ -import { DynamicModule, Module, Provider, Type } from '@nestjs/common'; +import { DynamicModule, Global, Module, Provider, Type } from '@nestjs/common'; import { + StorageAdapter, StorageBaseModuleAsyncOptions, StorageBaseModuleOptions, StorageBaseModuleOptionsFactory, } from './typings/storage-base-module-options.interface'; -import { STORAGE_MODULE_OPTIONS } from './typings/storages-base-module-providers'; +import { STORAGE_ADAPTER, STORAGE_MODULE_OPTIONS } from './typings/storages-base-module-providers'; import { StorageService } from './services/storage.service'; +@Global() @Module({}) export class StorageBaseModule { static forRoot(options: StorageBaseModuleOptions): DynamicModule { + const optionsProvider = { + provide: STORAGE_MODULE_OPTIONS, + useValue: options, + }; + + const adapterProvider = { + provide: STORAGE_ADAPTER, + useFactory: (): StorageAdapter => { + const AdapterClass = options.adapter; + + if (!AdapterClass) { + throw new Error('No storage adapter class was provided in forRoot!'); + } + + return new AdapterClass(options.config); + }, + }; + return { module: StorageBaseModule, - providers: [ - { - provide: STORAGE_MODULE_OPTIONS, - useValue: options, - }, - StorageService, - ], + providers: [optionsProvider, adapterProvider, StorageService], exports: [StorageService], }; } static forRootAsync(options: StorageBaseModuleAsyncOptions): DynamicModule { + const asyncAdapterProviders: Provider = { + provide: STORAGE_ADAPTER, + useFactory: (options: StorageBaseModuleOptions): StorageAdapter => { + const AdapterClass = options.adapter; + + if (!AdapterClass) { + throw new Error('No storage adapter class was provided in forRootAsync!'); + } + + return new AdapterClass(options.config); + }, + inject: [STORAGE_MODULE_OPTIONS], + }; + return { module: StorageBaseModule, imports: [...(options?.imports ?? [])], - providers: [...this.createAsyncProvider(options), StorageService], + providers: [...this.createAsyncProvider(options), asyncAdapterProviders, StorageService], exports: [StorageService], }; } diff --git a/packages/storage-base-nest-js-module/src/typings/storage-base-module-options.interface.ts b/packages/storages-base-nest-js-module/src/typings/storage-base-module-options.interface.ts similarity index 95% rename from packages/storage-base-nest-js-module/src/typings/storage-base-module-options.interface.ts rename to packages/storages-base-nest-js-module/src/typings/storage-base-module-options.interface.ts index 586af1d6..b174bdc6 100644 --- a/packages/storage-base-nest-js-module/src/typings/storage-base-module-options.interface.ts +++ b/packages/storages-base-nest-js-module/src/typings/storage-base-module-options.interface.ts @@ -13,7 +13,8 @@ export type StorageAdapter = | StorageS3Service; export interface StorageBaseModuleOptions { - adapter: StorageAdapter; + adapter: new (config: unknown) => StorageAdapter; + config: unknown; formDataFieldName?: string; // default: 'files' allowMultiple?: boolean; // default: true MaxFileSizeInBytes?: number; // default: 10 * 1024 * 1024 diff --git a/packages/storage-base-nest-js-module/src/typings/storages-base-module-providers.ts b/packages/storages-base-nest-js-module/src/typings/storages-base-module-providers.ts similarity index 100% rename from packages/storage-base-nest-js-module/src/typings/storages-base-module-providers.ts rename to packages/storages-base-nest-js-module/src/typings/storages-base-module-providers.ts diff --git a/packages/storage-base-nest-js-module/tsconfig.build.json b/packages/storages-base-nest-js-module/tsconfig.build.json similarity index 100% rename from packages/storage-base-nest-js-module/tsconfig.build.json rename to packages/storages-base-nest-js-module/tsconfig.build.json diff --git a/tsconfig.json b/tsconfig.json index d3c6f93c..80a04366 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,6 +34,7 @@ "@rytass/storages-adapter-local": ["storages-adapter-local/src"], "@rytass/storages-adapter-r2": ["storages-adapter-r2/src"], "@rytass/storages-adapter-s3": ["storages-adapter-s3/src"], + "@rytass/storages-base-nestjs-module": ["storages-base-nestjs-module/src"], "@rytass/wms-base-nestjs-module": ["wms-base-nestjs-module/src"] } }, From d4317b3f6176cab620b864c1e90e2767ea6757a6 Mon Sep 17 00:00:00 2001 From: geoffreyfarel Date: Tue, 21 Oct 2025 19:23:17 +0800 Subject: [PATCH 03/12] chore: update yarn.lock --- .../storages-base-nest-js-module/src/index.ts | 2 +- .../src/services/storages-base.service.ts | 8 ++++++-- .../src/storages-base.module.ts | 2 +- yarn.lock | 20 +++++++++++++++---- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/storages-base-nest-js-module/src/index.ts b/packages/storages-base-nest-js-module/src/index.ts index 745af1d1..6076219c 100644 --- a/packages/storages-base-nest-js-module/src/index.ts +++ b/packages/storages-base-nest-js-module/src/index.ts @@ -1,3 +1,3 @@ export * from './storages-base.module'; -export * from './services/storage.service'; +export * from './services/storages-base.service'; diff --git a/packages/storages-base-nest-js-module/src/services/storages-base.service.ts b/packages/storages-base-nest-js-module/src/services/storages-base.service.ts index 13a73713..f0fe82be 100644 --- a/packages/storages-base-nest-js-module/src/services/storages-base.service.ts +++ b/packages/storages-base-nest-js-module/src/services/storages-base.service.ts @@ -46,11 +46,11 @@ export class StorageService { } } - async write(file: InputFile, options?: WriteFileOptions): Promise { + write(file: InputFile, options?: WriteFileOptions): Promise { return this._adapter.write(file, options); } - async batchWrite(files: InputFile[], options?: WriteFileOptions[]): Promise { + batchWrite(files: InputFile[], options?: WriteFileOptions[]): Promise { return this._adapter.batchWrite(files, options); } @@ -58,6 +58,10 @@ export class StorageService { return this._adapter.remove(key); } + removeSync(key: string): Promise { + return this._adapter.remove(key); + } + async isExists(key: string): Promise { return this._adapter.isExists(key); } diff --git a/packages/storages-base-nest-js-module/src/storages-base.module.ts b/packages/storages-base-nest-js-module/src/storages-base.module.ts index 497c9eeb..6a9cedca 100644 --- a/packages/storages-base-nest-js-module/src/storages-base.module.ts +++ b/packages/storages-base-nest-js-module/src/storages-base.module.ts @@ -6,7 +6,7 @@ import { StorageBaseModuleOptionsFactory, } from './typings/storage-base-module-options.interface'; import { STORAGE_ADAPTER, STORAGE_MODULE_OPTIONS } from './typings/storages-base-module-providers'; -import { StorageService } from './services/storage.service'; +import { StorageService } from './services/storages-base.service'; @Global() @Module({}) diff --git a/yarn.lock b/yarn.lock index 35bd45a5..94bf6066 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6808,7 +6808,7 @@ __metadata: languageName: unknown linkType: soft -"@rytass/storages-adapter-azure-blob@workspace:packages/storages-adapter-azure-blob": +"@rytass/storages-adapter-azure-blob@npm:^0.1.6, @rytass/storages-adapter-azure-blob@workspace:packages/storages-adapter-azure-blob": version: 0.0.0-use.local resolution: "@rytass/storages-adapter-azure-blob@workspace:packages/storages-adapter-azure-blob" dependencies: @@ -6818,7 +6818,7 @@ __metadata: languageName: unknown linkType: soft -"@rytass/storages-adapter-gcs@workspace:packages/storages-adapter-gcs": +"@rytass/storages-adapter-gcs@npm:^0.2.7, @rytass/storages-adapter-gcs@workspace:packages/storages-adapter-gcs": version: 0.0.0-use.local resolution: "@rytass/storages-adapter-gcs@workspace:packages/storages-adapter-gcs" dependencies: @@ -6829,7 +6829,7 @@ __metadata: languageName: unknown linkType: soft -"@rytass/storages-adapter-local@workspace:packages/storages-adapter-local": +"@rytass/storages-adapter-local@npm:0.2.7, @rytass/storages-adapter-local@workspace:packages/storages-adapter-local": version: 0.0.0-use.local resolution: "@rytass/storages-adapter-local@workspace:packages/storages-adapter-local" dependencies: @@ -6852,7 +6852,7 @@ __metadata: languageName: unknown linkType: soft -"@rytass/storages-adapter-s3@workspace:packages/storages-adapter-s3": +"@rytass/storages-adapter-s3@npm:^0.3.7, @rytass/storages-adapter-s3@workspace:packages/storages-adapter-s3": version: 0.0.0-use.local resolution: "@rytass/storages-adapter-s3@workspace:packages/storages-adapter-s3" dependencies: @@ -6865,6 +6865,18 @@ __metadata: languageName: unknown linkType: soft +"@rytass/storages-base-nestjs-module@workspace:packages/storages-base-nest-js-module": + version: 0.0.0-use.local + resolution: "@rytass/storages-base-nestjs-module@workspace:packages/storages-base-nest-js-module" + dependencies: + "@rytass/storages": "npm:^0.2.4" + "@rytass/storages-adapter-azure-blob": "npm:^0.1.6" + "@rytass/storages-adapter-gcs": "npm:^0.2.7" + "@rytass/storages-adapter-local": "npm:0.2.7" + "@rytass/storages-adapter-s3": "npm:^0.3.7" + languageName: unknown + linkType: soft + "@rytass/storages@npm:^0.2.4, @rytass/storages@workspace:packages/storages": version: 0.0.0-use.local resolution: "@rytass/storages@workspace:packages/storages" From 9ce10a423edbb0dfff79371a0a6c3c2cca5b9357 Mon Sep 17 00:00:00 2001 From: geoffreyfarel Date: Tue, 28 Oct 2025 11:42:32 +0800 Subject: [PATCH 04/12] fix: rename folder name --- .../README.md | 0 .../package.json | 0 .../project.json | 0 .../src/index.ts | 0 .../src/services/storages-base.service.ts | 0 .../src/storages-base.module.ts | 0 .../src/typings/storage-base-module-options.interface.ts | 0 .../src/typings/storages-base-module-providers.ts | 0 .../tsconfig.build.json | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename packages/{storages-base-nest-js-module => storages-base-nestjs-module}/README.md (100%) rename packages/{storages-base-nest-js-module => storages-base-nestjs-module}/package.json (100%) rename packages/{storages-base-nest-js-module => storages-base-nestjs-module}/project.json (100%) rename packages/{storages-base-nest-js-module => storages-base-nestjs-module}/src/index.ts (100%) rename packages/{storages-base-nest-js-module => storages-base-nestjs-module}/src/services/storages-base.service.ts (100%) rename packages/{storages-base-nest-js-module => storages-base-nestjs-module}/src/storages-base.module.ts (100%) rename packages/{storages-base-nest-js-module => storages-base-nestjs-module}/src/typings/storage-base-module-options.interface.ts (100%) rename packages/{storages-base-nest-js-module => storages-base-nestjs-module}/src/typings/storages-base-module-providers.ts (100%) rename packages/{storages-base-nest-js-module => storages-base-nestjs-module}/tsconfig.build.json (100%) diff --git a/packages/storages-base-nest-js-module/README.md b/packages/storages-base-nestjs-module/README.md similarity index 100% rename from packages/storages-base-nest-js-module/README.md rename to packages/storages-base-nestjs-module/README.md diff --git a/packages/storages-base-nest-js-module/package.json b/packages/storages-base-nestjs-module/package.json similarity index 100% rename from packages/storages-base-nest-js-module/package.json rename to packages/storages-base-nestjs-module/package.json diff --git a/packages/storages-base-nest-js-module/project.json b/packages/storages-base-nestjs-module/project.json similarity index 100% rename from packages/storages-base-nest-js-module/project.json rename to packages/storages-base-nestjs-module/project.json diff --git a/packages/storages-base-nest-js-module/src/index.ts b/packages/storages-base-nestjs-module/src/index.ts similarity index 100% rename from packages/storages-base-nest-js-module/src/index.ts rename to packages/storages-base-nestjs-module/src/index.ts diff --git a/packages/storages-base-nest-js-module/src/services/storages-base.service.ts b/packages/storages-base-nestjs-module/src/services/storages-base.service.ts similarity index 100% rename from packages/storages-base-nest-js-module/src/services/storages-base.service.ts rename to packages/storages-base-nestjs-module/src/services/storages-base.service.ts diff --git a/packages/storages-base-nest-js-module/src/storages-base.module.ts b/packages/storages-base-nestjs-module/src/storages-base.module.ts similarity index 100% rename from packages/storages-base-nest-js-module/src/storages-base.module.ts rename to packages/storages-base-nestjs-module/src/storages-base.module.ts diff --git a/packages/storages-base-nest-js-module/src/typings/storage-base-module-options.interface.ts b/packages/storages-base-nestjs-module/src/typings/storage-base-module-options.interface.ts similarity index 100% rename from packages/storages-base-nest-js-module/src/typings/storage-base-module-options.interface.ts rename to packages/storages-base-nestjs-module/src/typings/storage-base-module-options.interface.ts diff --git a/packages/storages-base-nest-js-module/src/typings/storages-base-module-providers.ts b/packages/storages-base-nestjs-module/src/typings/storages-base-module-providers.ts similarity index 100% rename from packages/storages-base-nest-js-module/src/typings/storages-base-module-providers.ts rename to packages/storages-base-nestjs-module/src/typings/storages-base-module-providers.ts diff --git a/packages/storages-base-nest-js-module/tsconfig.build.json b/packages/storages-base-nestjs-module/tsconfig.build.json similarity index 100% rename from packages/storages-base-nest-js-module/tsconfig.build.json rename to packages/storages-base-nestjs-module/tsconfig.build.json From c866f8a708eb02a0b544e1cdb31481a026d5b538 Mon Sep 17 00:00:00 2001 From: geoffreyfarel Date: Tue, 28 Oct 2025 11:45:50 +0800 Subject: [PATCH 05/12] fix: yarn.lock name for storages-base-nestjs-module --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 94bf6066..7acd8a59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6865,9 +6865,9 @@ __metadata: languageName: unknown linkType: soft -"@rytass/storages-base-nestjs-module@workspace:packages/storages-base-nest-js-module": +"@rytass/storages-base-nestjs-module@workspace:packages/storages-base-nestjs-module": version: 0.0.0-use.local - resolution: "@rytass/storages-base-nestjs-module@workspace:packages/storages-base-nest-js-module" + resolution: "@rytass/storages-base-nestjs-module@workspace:packages/storages-base-nestjs-module" dependencies: "@rytass/storages": "npm:^0.2.4" "@rytass/storages-adapter-azure-blob": "npm:^0.1.6" From e8f0fab78d753b370dd8702268edc518c099bbf1 Mon Sep 17 00:00:00 2001 From: geoffreyfarel Date: Tue, 28 Oct 2025 12:43:25 +0800 Subject: [PATCH 06/12] fix: import misnamed --- .../src/services/storages-base.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/storages-base-nestjs-module/src/services/storages-base.service.ts b/packages/storages-base-nestjs-module/src/services/storages-base.service.ts index f0fe82be..58d49c99 100644 --- a/packages/storages-base-nestjs-module/src/services/storages-base.service.ts +++ b/packages/storages-base-nestjs-module/src/services/storages-base.service.ts @@ -7,7 +7,7 @@ import { StorageAzureBlobService } from '@rytass/storages-adapter-azure-blob'; import { StorageGCSService } from '@rytass/storages-adapter-gcs'; import { StorageS3Service } from '@rytass/storages-adapter-s3'; import { StorageR2Service } from '@rytass/storages-adapter-r2'; -import { PresignedURLOptions } from 'storages-adapter-r2/src/typings'; +import { PresignedURLOptions } from '@rytass/storages-adapter-r2/src/typings'; @Injectable() export class StorageService { From c7023c8e5d4016fd93a99bc5fa7a505e0b1ee662 Mon Sep 17 00:00:00 2001 From: geoffreyfarel Date: Tue, 28 Oct 2025 13:07:43 +0800 Subject: [PATCH 07/12] fix:replace PresignedURL to generic type --- .../src/services/storages-base.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/storages-base-nestjs-module/src/services/storages-base.service.ts b/packages/storages-base-nestjs-module/src/services/storages-base.service.ts index 58d49c99..dc66b75c 100644 --- a/packages/storages-base-nestjs-module/src/services/storages-base.service.ts +++ b/packages/storages-base-nestjs-module/src/services/storages-base.service.ts @@ -7,7 +7,6 @@ import { StorageAzureBlobService } from '@rytass/storages-adapter-azure-blob'; import { StorageGCSService } from '@rytass/storages-adapter-gcs'; import { StorageS3Service } from '@rytass/storages-adapter-s3'; import { StorageR2Service } from '@rytass/storages-adapter-r2'; -import { PresignedURLOptions } from '@rytass/storages-adapter-r2/src/typings'; @Injectable() export class StorageService { @@ -24,9 +23,9 @@ export class StorageService { async url(key: string): Promise; async url(key: string, expires: number): Promise; - async url(key: string, options?: PresignedURLOptions): Promise; + async url(key: string, options?: unknown): Promise; - async url(key: string, params?: number | PresignedURLOptions): Promise { + async url(key: string, params?: number | unknown): Promise { if (this._adapter instanceof LocalStorage) { throw new Error('LocalStorage does not support URL generation'); } @@ -38,7 +37,8 @@ export class StorageService { } else if (this._adapter instanceof StorageS3Service) { return this._adapter.url(key); } else if (this._adapter instanceof StorageR2Service) { - const options = params as PresignedURLOptions; + type R2Options = Parameters[1]; + const options = params as R2Options; return this._adapter.url(key, options); } else { From a672e3ab995003571187da36794c933ef7fcbf50 Mon Sep 17 00:00:00 2001 From: geoffreyfarel Date: Fri, 31 Oct 2025 16:06:10 +0800 Subject: [PATCH 08/12] feat: GCSAdapter, refactor StorageService --- nx.json | 3 +- .../__tests__/storages-base-module.spec.ts | 214 ++++++++++++++++++ .../storages-base-nestjs-module/package.json | 1 + .../src/services/storages-base.service.ts | 51 +++-- .../src/storages-base.module.ts | 23 +- .../storage-base-module-options.interface.ts | 41 +++- .../src/wrappers/storages-base-gcs-wrapper.ts | 37 +++ yarn.lock | 3 +- 8 files changed, 335 insertions(+), 38 deletions(-) create mode 100644 packages/storages-base-nestjs-module/__tests__/storages-base-module.spec.ts create mode 100644 packages/storages-base-nestjs-module/src/wrappers/storages-base-gcs-wrapper.ts diff --git a/nx.json b/nx.json index 1f19cc94..cb9b163d 100644 --- a/nx.json +++ b/nx.json @@ -99,5 +99,6 @@ "projectChangelogs": true }, "projectsRelationship": "independent" - } + }, + "nxCloudId": "69008e1b106f8b44b91adf92" } diff --git a/packages/storages-base-nestjs-module/__tests__/storages-base-module.spec.ts b/packages/storages-base-nestjs-module/__tests__/storages-base-module.spec.ts new file mode 100644 index 00000000..3890f5b9 --- /dev/null +++ b/packages/storages-base-nestjs-module/__tests__/storages-base-module.spec.ts @@ -0,0 +1,214 @@ +import { Injectable, Module, Type } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { StorageBaseModule } from '../src/storages-base.module'; +import { + IStorageAdapter, + StorageBaseModuleOptions, + StorageBaseModuleOptionsFactory, +} from '../src/typings/storage-base-module-options.interface'; +import { STORAGE_ADAPTER, STORAGE_MODULE_OPTIONS } from '../src/typings/storages-base-module-providers'; +import { GCSAdapter } from '../src/wrappers/storages-base-gcs-wrapper'; +import { StorageService } from '@rytass/storages-base-nestjs-module'; + +describe('Storages Base Nestjs Module', () => { + @Injectable() + class mockAdapter implements IStorageAdapter { + static config: unknown; + url = jest.fn(async (_key: string, _options?: unknown) => 'https://mockURL.com'); + + write = jest.fn(); + batchWrite = jest.fn(); + remove = jest.fn(); + removeSync = jest.fn(); + isExists = jest.fn(); + + constructor(config: unknown) { + mockAdapter.config = config; + } + } + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const mockOptions = { + adapter: mockAdapter, + config: { test: true }, + commonOptions: { + formDataFieldName: 'files', + allowMultiple: true, + MaxFileSizeInBytes: 10 * 1024 * 1024, + defaultPublic: false, + }, + }; + + const mockCommonOptions = { + formDataFieldName: 'files', + allowMultiple: true, + MaxFileSizeInBytes: 10 * 1024 * 1024, + defaultPublic: false, + }; + + describe('integration test', () => { + it('GCS', async () => { + const options = { + adapter: GCSAdapter, + config: { + bucket: 'test-bucket', + projectId: 'test-projectId', + credentials: { client_email: 'test-client_email', private_key: 'test-private_key' }, + }, + }; + + const module: TestingModule = await Test.createTestingModule({ + imports: [StorageBaseModule.forRoot(options)], + }).compile(); + + expect(module.get(STORAGE_MODULE_OPTIONS)).toEqual(options); + + const adapter = module.get(STORAGE_ADAPTER); + + expect(adapter).toBeInstanceOf(GCSAdapter); + }); + }); + + describe('forRoot', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should throw Error if adapter is not provided', async () => { + await expect( + Test.createTestingModule({ + imports: [StorageBaseModule.forRoot({ adapter: undefined as unknown as Type, config: {} })], + }).compile(), + ).rejects.toThrow('No storage adapter class was provided in forRoot!'); + }); + + it('should apply default commonOptions when none are provided', async () => { + const options = { ...mockOptions, commonOptions: undefined }; + + const module: TestingModule = await Test.createTestingModule({ + imports: [StorageBaseModule.forRoot(options)], + }).compile(); + + const adapter = module.get(STORAGE_ADAPTER); + const service = module.get(StorageService); + + expect(adapter).toBeInstanceOf(mockAdapter); + + expect(service.commonOptions).toEqual(mockCommonOptions); + }); + + it('should merge provided commonOptions with defaults', async () => { + const options = { ...mockOptions, commonOptions: { MaxFileSizeInBytes: 999 } }; + + const module: TestingModule = await Test.createTestingModule({ + imports: [StorageBaseModule.forRoot(options)], + }).compile(); + + const service = module.get(StorageService); + + expect(service.commonOptions).toEqual({ + ...mockCommonOptions, + MaxFileSizeInBytes: 999, + }); + }); + }); + + describe('forRootAsync', () => { + @Injectable() + class MockConfigService implements StorageBaseModuleOptionsFactory> { + createStorageBaseModuleOptions(): StorageBaseModuleOptions> { + return mockOptions; + } + } + + @Module({ + providers: [MockConfigService], + exports: [MockConfigService], + }) + class MockConfigModule {} + + it('should throw Error if adapter is not provided', async () => { + await expect( + Test.createTestingModule({ + imports: [ + StorageBaseModule.forRootAsync({ + useFactory: () => ({ + adapter: undefined as unknown as Type, + config: {}, + }), + }), + ], + }).compile(), + ).rejects.toThrow('No storage adapter class was provided in forRootAsync!'); + }); + + it('should work with useFactory', async () => { + const options = mockOptions; + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + StorageBaseModule.forRootAsync({ + useFactory: () => options, + }), + ], + }).compile(); + + const adapter = module.get(STORAGE_ADAPTER); + const service = module.get(StorageService); + + expect(adapter).toBeInstanceOf(mockAdapter); + expect(module.get(STORAGE_MODULE_OPTIONS)).toEqual(options); + expect(service.commonOptions).toEqual(mockCommonOptions); + }); + + it('should work with useClass', async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + StorageBaseModule.forRootAsync({ + useClass: MockConfigService, + }), + ], + providers: [MockConfigService], + }).compile(); + + const adapter = module.get(STORAGE_ADAPTER); + const service = module.get(StorageService); + + expect(adapter).toBeInstanceOf(mockAdapter); + expect(service.commonOptions).toEqual(mockCommonOptions); + }); + + it('should work with useExisting', async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + MockConfigModule, + StorageBaseModule.forRootAsync({ + imports: [MockConfigModule], + useExisting: MockConfigService, + }), + ], + }).compile(); + + const adapter = module.get(STORAGE_ADAPTER); + const service = module.get(StorageService); + + expect(adapter).toBeInstanceOf(mockAdapter); + expect(service.commonOptions).toEqual(mockCommonOptions); + }); + + it('should throw an error if no async provider (useFactory, useClass, or useExisting) is given', async () => { + await expect( + Test.createTestingModule({ + imports: [ + StorageBaseModule.forRootAsync({ + imports: [], + }), + ], + }).compile(), + ).rejects.toThrow(); + }); + }); +}); diff --git a/packages/storages-base-nestjs-module/package.json b/packages/storages-base-nestjs-module/package.json index bb6553f0..da0d672a 100644 --- a/packages/storages-base-nestjs-module/package.json +++ b/packages/storages-base-nestjs-module/package.json @@ -24,6 +24,7 @@ "@rytass/storages-adapter-azure-blob": "^0.1.6", "@rytass/storages-adapter-gcs": "^0.2.7", "@rytass/storages-adapter-local": "0.2.7", + "@rytass/storages-adapter-r2": "^0.4.9", "@rytass/storages-adapter-s3": "^0.3.7" } } diff --git a/packages/storages-base-nestjs-module/src/services/storages-base.service.ts b/packages/storages-base-nestjs-module/src/services/storages-base.service.ts index dc66b75c..98e6d326 100644 --- a/packages/storages-base-nestjs-module/src/services/storages-base.service.ts +++ b/packages/storages-base-nestjs-module/src/services/storages-base.service.ts @@ -1,24 +1,34 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { STORAGE_ADAPTER, STORAGE_MODULE_OPTIONS } from '../typings/storages-base-module-providers'; -import type { StorageAdapter, StorageBaseModuleOptions } from '../typings/storage-base-module-options.interface'; +import type { IStorageAdapter, StorageModuleCommonOptions } from '../typings/storage-base-module-options.interface'; import { InputFile, StorageFile, WriteFileOptions } from '@rytass/storages'; -import { LocalStorage } from '@rytass/storages-adapter-local'; -import { StorageAzureBlobService } from '@rytass/storages-adapter-azure-blob'; -import { StorageGCSService } from '@rytass/storages-adapter-gcs'; -import { StorageS3Service } from '@rytass/storages-adapter-s3'; -import { StorageR2Service } from '@rytass/storages-adapter-r2'; +import type { StorageBaseModuleOptions } from 'storages-base-nestjs-module/lib/typings/storage-base-module-options.interface'; @Injectable() export class StorageService { private readonly logger = new Logger(StorageService.name); + private readonly _commonOptions: StorageModuleCommonOptions; constructor( @Inject(STORAGE_ADAPTER) - private readonly _adapter: StorageAdapter, + private readonly _adapter: IStorageAdapter, @Inject(STORAGE_MODULE_OPTIONS) private readonly _options: StorageBaseModuleOptions, ) { this.logger.log(`Storage adapter: ${this._adapter.constructor.name}`); + + const { commonOptions = {} } = this._options; + + this._commonOptions = { + formDataFieldName: commonOptions.formDataFieldName ?? 'files', + allowMultiple: commonOptions.allowMultiple ?? true, + MaxFileSizeInBytes: commonOptions.MaxFileSizeInBytes ?? 10 * 1024 * 1024, + defaultPublic: commonOptions.defaultPublic ?? false, + }; + } + + get commonOptions(): StorageModuleCommonOptions { + return this._commonOptions; } async url(key: string): Promise; @@ -26,21 +36,30 @@ export class StorageService { async url(key: string, options?: unknown): Promise; async url(key: string, params?: number | unknown): Promise { - if (this._adapter instanceof LocalStorage) { + const adapterName = this._adapter.constructor.name; + + if (adapterName === 'LocalStorage') { throw new Error('LocalStorage does not support URL generation'); } - if (this._adapter instanceof StorageAzureBlobService || this._adapter instanceof StorageGCSService) { + if (adapterName === 'StorageAzureBlobService' || adapterName === 'StorageGCSService') { const expires = params as number; - return this._adapter.url(key, expires); - } else if (this._adapter instanceof StorageS3Service) { - return this._adapter.url(key); - } else if (this._adapter instanceof StorageR2Service) { - type R2Options = Parameters[1]; - const options = params as R2Options; + type UrlWithExpires = { url: (k: string, e: number) => Promise }; + const adapter = this._adapter as unknown as UrlWithExpires; + + return adapter.url(key, expires); + } else if (adapterName === 'StorageS3Service') { + type UrlNoOptions = { url: (k: string) => Promise }; + const adapter = this._adapter as unknown as UrlNoOptions; + + return adapter.url(key); + } else if (adapterName === 'StorageR2Service') { + type R2Like = { url: (k: string, o?: unknown) => Promise }; + const r2 = this._adapter as unknown as R2Like; + const options = params as unknown; - return this._adapter.url(key, options); + return r2.url(key, options); } else { throw new Error('Unknown storage adapter'); } diff --git a/packages/storages-base-nestjs-module/src/storages-base.module.ts b/packages/storages-base-nestjs-module/src/storages-base.module.ts index 6a9cedca..4aadd773 100644 --- a/packages/storages-base-nestjs-module/src/storages-base.module.ts +++ b/packages/storages-base-nestjs-module/src/storages-base.module.ts @@ -1,6 +1,6 @@ import { DynamicModule, Global, Module, Provider, Type } from '@nestjs/common'; import { - StorageAdapter, + IStorageAdapter, StorageBaseModuleAsyncOptions, StorageBaseModuleOptions, StorageBaseModuleOptionsFactory, @@ -11,7 +11,7 @@ import { StorageService } from './services/storages-base.service'; @Global() @Module({}) export class StorageBaseModule { - static forRoot(options: StorageBaseModuleOptions): DynamicModule { + static forRoot>(options: StorageBaseModuleOptions): DynamicModule { const optionsProvider = { provide: STORAGE_MODULE_OPTIONS, useValue: options, @@ -19,7 +19,7 @@ export class StorageBaseModule { const adapterProvider = { provide: STORAGE_ADAPTER, - useFactory: (): StorageAdapter => { + useFactory: (): IStorageAdapter => { const AdapterClass = options.adapter; if (!AdapterClass) { @@ -28,6 +28,7 @@ export class StorageBaseModule { return new AdapterClass(options.config); }, + inject: [STORAGE_MODULE_OPTIONS], }; return { @@ -36,10 +37,10 @@ export class StorageBaseModule { exports: [StorageService], }; } - static forRootAsync(options: StorageBaseModuleAsyncOptions): DynamicModule { + static forRootAsync>(options: StorageBaseModuleAsyncOptions): DynamicModule { const asyncAdapterProviders: Provider = { provide: STORAGE_ADAPTER, - useFactory: (options: StorageBaseModuleOptions): StorageAdapter => { + useFactory: (options: StorageBaseModuleOptions): IStorageAdapter => { const AdapterClass = options.adapter; if (!AdapterClass) { @@ -59,7 +60,9 @@ export class StorageBaseModule { }; } - private static createAsyncProvider(options: StorageBaseModuleAsyncOptions): Provider[] { + private static createAsyncProvider>( + options: StorageBaseModuleAsyncOptions, + ): Provider[] { if (options.useExisting || options.useFactory) { return [this.createAsyncOptionsProvider(options)]; } @@ -76,7 +79,9 @@ export class StorageBaseModule { : []), ]; } - private static createAsyncOptionsProvider(options: StorageBaseModuleAsyncOptions): Provider { + private static createAsyncOptionsProvider>( + options: StorageBaseModuleAsyncOptions, + ): Provider { if (options.useFactory) { return { provide: STORAGE_MODULE_OPTIONS, @@ -87,9 +92,9 @@ export class StorageBaseModule { return { provide: STORAGE_MODULE_OPTIONS, - useFactory: async (optionsFactory: StorageBaseModuleOptionsFactory) => + useFactory: async (optionsFactory: StorageBaseModuleOptionsFactory) => await optionsFactory.createStorageBaseModuleOptions(), - inject: [(options.useExisting || options.useClass) as Type], + inject: [(options.useExisting || options.useClass) as Type>], }; } } diff --git a/packages/storages-base-nestjs-module/src/typings/storage-base-module-options.interface.ts b/packages/storages-base-nestjs-module/src/typings/storage-base-module-options.interface.ts index b174bdc6..1c85eece 100644 --- a/packages/storages-base-nestjs-module/src/typings/storage-base-module-options.interface.ts +++ b/packages/storages-base-nestjs-module/src/typings/storage-base-module-options.interface.ts @@ -4,6 +4,7 @@ import { StorageGCSService } from '@rytass/storages-adapter-gcs'; import { LocalStorage } from '@rytass/storages-adapter-local'; import { StorageR2Service } from '@rytass/storages-adapter-r2'; import { StorageS3Service } from '@rytass/storages-adapter-s3'; +import { InputFile, StorageFile, WriteFileOptions } from 'storages/lib'; export type StorageAdapter = | LocalStorage @@ -12,23 +13,41 @@ export type StorageAdapter = | StorageR2Service | StorageS3Service; -export interface StorageBaseModuleOptions { - adapter: new (config: unknown) => StorageAdapter; - config: unknown; +export interface StorageBaseModuleOptions> { + adapter: A; + config: ConstructorParameters[0]; + commonOptions?: StorageModuleCommonOptions; +} + +export interface StorageBaseModuleOptionsFactory> { + createStorageBaseModuleOptions(): Promise> | StorageBaseModuleOptions; +} + +export interface StorageBaseModuleAsyncOptions> + extends Pick { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + useFactory?: (...args: any[]) => Promise> | StorageBaseModuleOptions; + inject?: (InjectionToken | OptionalFactoryDependency)[]; + useClass?: Type>; + useExisting?: Type>; +} + +export interface StorageModuleCommonOptions { formDataFieldName?: string; // default: 'files' allowMultiple?: boolean; // default: true MaxFileSizeInBytes?: number; // default: 10 * 1024 * 1024 defaultPublic?: boolean; // default: false } -export interface StorageBaseModuleOptionsFactory { - createStorageBaseModuleOptions(): Promise | StorageBaseModuleOptions; +export interface IStorageAdapterUrlOptions { + expires?: number; + [key: string]: unknown; } -export interface StorageBaseModuleAsyncOptions extends Pick { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - useFactory?: (...args: any[]) => Promise | StorageBaseModuleOptions; - inject?: (InjectionToken | OptionalFactoryDependency)[]; - useClass?: Type; - useExisting?: Type; +export interface IStorageAdapter { + url(key: string, options?: IStorageAdapterUrlOptions): Promise; + write(file: InputFile, options?: WriteFileOptions): Promise; + remove(key: string): Promise; + isExists(key: string): Promise; + batchWrite(files: InputFile[], options?: WriteFileOptions[]): Promise; } diff --git a/packages/storages-base-nestjs-module/src/wrappers/storages-base-gcs-wrapper.ts b/packages/storages-base-nestjs-module/src/wrappers/storages-base-gcs-wrapper.ts new file mode 100644 index 00000000..f619b7cd --- /dev/null +++ b/packages/storages-base-nestjs-module/src/wrappers/storages-base-gcs-wrapper.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; +import { IStorageAdapter, IStorageAdapterUrlOptions } from '../typings/storage-base-module-options.interface'; +import { StorageGCSService } from '@rytass/storages-adapter-gcs'; +import type { GCSOptions } from 'storages-adapter-gcs/src/typings'; +import { InputFile, StorageFile, WriteFileOptions } from '@rytass/storages'; + +@Injectable() +export class GCSAdapter implements IStorageAdapter { + private readonly gcsService: StorageGCSService; + + // The wrapper's constructor takes the *real* config + constructor(config: GCSOptions) { + this.gcsService = new StorageGCSService(config); + } + + // --- This is the "translation" --- + // Your interface expects an 'options' object. + url(key: string, options?: IStorageAdapterUrlOptions): Promise { + return this.gcsService.url(key, options?.expires); + } + + write(file: InputFile, options?: WriteFileOptions): Promise { + return this.gcsService.write(file, options); + } + + batchWrite(files: InputFile[]): Promise { + return this.gcsService.batchWrite(files); + } + + remove(key: string): Promise { + return this.gcsService.remove(key); + } + + isExists(key: string): Promise { + return this.gcsService.isExists(key); + } +} diff --git a/yarn.lock b/yarn.lock index 7acd8a59..91d37e62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6839,7 +6839,7 @@ __metadata: languageName: unknown linkType: soft -"@rytass/storages-adapter-r2@workspace:packages/storages-adapter-r2": +"@rytass/storages-adapter-r2@npm:^0.4.9, @rytass/storages-adapter-r2@workspace:packages/storages-adapter-r2": version: 0.0.0-use.local resolution: "@rytass/storages-adapter-r2@workspace:packages/storages-adapter-r2" dependencies: @@ -6873,6 +6873,7 @@ __metadata: "@rytass/storages-adapter-azure-blob": "npm:^0.1.6" "@rytass/storages-adapter-gcs": "npm:^0.2.7" "@rytass/storages-adapter-local": "npm:0.2.7" + "@rytass/storages-adapter-r2": "npm:^0.4.9" "@rytass/storages-adapter-s3": "npm:^0.3.7" languageName: unknown linkType: soft From fda296303fa70b2cf4c6afc4c9f80db5204eb6c1 Mon Sep 17 00:00:00 2001 From: geoffreyfarel Date: Fri, 31 Oct 2025 16:19:02 +0800 Subject: [PATCH 09/12] fix: type error StorageService --- .../src/services/storages-base.service.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/storages-base-nestjs-module/src/services/storages-base.service.ts b/packages/storages-base-nestjs-module/src/services/storages-base.service.ts index 98e6d326..f8677853 100644 --- a/packages/storages-base-nestjs-module/src/services/storages-base.service.ts +++ b/packages/storages-base-nestjs-module/src/services/storages-base.service.ts @@ -1,8 +1,11 @@ -import { Inject, Injectable, Logger } from '@nestjs/common'; +import { Inject, Injectable, Logger, Type } from '@nestjs/common'; import { STORAGE_ADAPTER, STORAGE_MODULE_OPTIONS } from '../typings/storages-base-module-providers'; -import type { IStorageAdapter, StorageModuleCommonOptions } from '../typings/storage-base-module-options.interface'; +import type { + IStorageAdapter, + StorageBaseModuleOptions, + StorageModuleCommonOptions, +} from '../typings/storage-base-module-options.interface'; import { InputFile, StorageFile, WriteFileOptions } from '@rytass/storages'; -import type { StorageBaseModuleOptions } from 'storages-base-nestjs-module/lib/typings/storage-base-module-options.interface'; @Injectable() export class StorageService { @@ -13,7 +16,7 @@ export class StorageService { @Inject(STORAGE_ADAPTER) private readonly _adapter: IStorageAdapter, @Inject(STORAGE_MODULE_OPTIONS) - private readonly _options: StorageBaseModuleOptions, + private readonly _options: StorageBaseModuleOptions>, ) { this.logger.log(`Storage adapter: ${this._adapter.constructor.name}`); From 1739551d1854f93bc4d355af8cee737e9d796231 Mon Sep 17 00:00:00 2001 From: geoffreyfarel Date: Sat, 1 Nov 2025 00:36:57 +0800 Subject: [PATCH 10/12] chore: unit test for storage--base-nestjs-module --- .../__tests__/storages-base-module.spec.ts | 338 +++++++++++++++++- .../__tests__/storages-base.service.spec.ts | 247 +++++++++++++ .../src/services/storages-base.service.ts | 12 +- .../storage-base-module-options.interface.ts | 3 +- .../src/wrappers/azure-blob-wrapper.ts | 34 ++ ...ges-base-gcs-wrapper.ts => gcs-wrapper.ts} | 3 - .../src/wrappers/local-wrapper.ts | 30 ++ .../src/wrappers/r2-wrapper.ts | 34 ++ .../src/wrappers/s3-wrapper.ts | 34 ++ 9 files changed, 706 insertions(+), 29 deletions(-) create mode 100644 packages/storages-base-nestjs-module/__tests__/storages-base.service.spec.ts create mode 100644 packages/storages-base-nestjs-module/src/wrappers/azure-blob-wrapper.ts rename packages/storages-base-nestjs-module/src/wrappers/{storages-base-gcs-wrapper.ts => gcs-wrapper.ts} (88%) create mode 100644 packages/storages-base-nestjs-module/src/wrappers/local-wrapper.ts create mode 100644 packages/storages-base-nestjs-module/src/wrappers/r2-wrapper.ts create mode 100644 packages/storages-base-nestjs-module/src/wrappers/s3-wrapper.ts diff --git a/packages/storages-base-nestjs-module/__tests__/storages-base-module.spec.ts b/packages/storages-base-nestjs-module/__tests__/storages-base-module.spec.ts index 3890f5b9..34ab9195 100644 --- a/packages/storages-base-nestjs-module/__tests__/storages-base-module.spec.ts +++ b/packages/storages-base-nestjs-module/__tests__/storages-base-module.spec.ts @@ -7,8 +7,51 @@ import { StorageBaseModuleOptionsFactory, } from '../src/typings/storage-base-module-options.interface'; import { STORAGE_ADAPTER, STORAGE_MODULE_OPTIONS } from '../src/typings/storages-base-module-providers'; -import { GCSAdapter } from '../src/wrappers/storages-base-gcs-wrapper'; +import { GCSAdapter } from '../src/wrappers/gcs-wrapper'; import { StorageService } from '@rytass/storages-base-nestjs-module'; +import { StorageGCSService } from '@rytass/storages-adapter-gcs'; +import { WriteFileOptions } from '@rytass/storages'; +import { AzureBlobAdapter } from '../src/wrappers/azure-blob-wrapper'; +import { StorageAzureBlobService } from '@rytass/storages-adapter-azure-blob'; +import { S3Adapter } from '../src/wrappers/s3-wrapper'; +import { StorageS3Service } from '@rytass/storages-adapter-s3/src'; +import { StorageR2Service } from '@rytass/storages-adapter-r2/src'; +import { R2Adapter } from '../src/wrappers/r2-wrapper'; +import { LocalAdapter } from '../src/wrappers/local-wrapper'; +import { LocalStorage } from '@rytass/storages-adapter-local'; + +const mockInstance = { + url: jest.fn(), + write: jest.fn(), + batchWrite: jest.fn(), + remove: jest.fn(), + isExists: jest.fn(), +}; + +jest.mock('@rytass/storages-adapter-gcs', () => ({ + StorageGCSService: jest.fn(() => mockInstance), + GCSOptions: jest.fn(), +})); + +jest.mock('@rytass/storages-adapter-azure-blob', () => ({ + StorageAzureBlobService: jest.fn(() => mockInstance), + AzureBlobOptions: jest.fn(), +})); + +jest.mock('@rytass/storages-adapter-s3/src', () => ({ + StorageS3Service: jest.fn(() => mockInstance), + StorageS3Options: jest.fn(), +})); + +jest.mock('@rytass/storages-adapter-r2/src', () => ({ + StorageR2Service: jest.fn(() => mockInstance), + StorageR2Options: jest.fn(), +})); + +jest.mock('@rytass/storages-adapter-local', () => ({ + LocalStorage: jest.fn(() => mockInstance), + StorageLocalOptions: jest.fn(), +})); describe('Storages Base Nestjs Module', () => { @Injectable() @@ -19,7 +62,6 @@ describe('Storages Base Nestjs Module', () => { write = jest.fn(); batchWrite = jest.fn(); remove = jest.fn(); - removeSync = jest.fn(); isExists = jest.fn(); constructor(config: unknown) { @@ -49,26 +91,290 @@ describe('Storages Base Nestjs Module', () => { defaultPublic: false, }; - describe('integration test', () => { - it('GCS', async () => { - const options = { - adapter: GCSAdapter, - config: { - bucket: 'test-bucket', - projectId: 'test-projectId', - credentials: { client_email: 'test-client_email', private_key: 'test-private_key' }, + describe('Integration test', () => { + describe('GCSAdapter Wrapper Unit Test', () => { + let adapter: GCSAdapter; + const mockConfig = { + bucket: 'test-bucket', + projectId: 'test-project', + credentials: { + client_email: 'test@test.iam.gserviceaccount.com', + private_key: 'test-private-key', }, }; - const module: TestingModule = await Test.createTestingModule({ - imports: [StorageBaseModule.forRoot(options)], - }).compile(); + beforeEach(() => { + jest.clearAllMocks(); - expect(module.get(STORAGE_MODULE_OPTIONS)).toEqual(options); + adapter = new GCSAdapter(mockConfig); + }); - const adapter = module.get(STORAGE_ADAPTER); + it('should create and configure the real GCS service on construction', () => { + expect(StorageGCSService).toHaveBeenCalledWith(mockConfig); + }); + + it('should cover the "url" wrapper method', async () => { + mockInstance.url.mockResolvedValue('http://mocked-url.com'); + + const url = await adapter.url('test.txt', { expires: 3600 }); + + expect(mockInstance.url).toHaveBeenCalledWith('test.txt', 3600); + expect(url).toBe('http://mocked-url.com'); + }); + + it('should cover the "write" wrapper method', async () => { + const mockFile = Buffer.from('This is a test file content'); + const mockOptions: WriteFileOptions = { + filename: 'my-file-name', + contentType: 'application/pdf', + }; + + await adapter.write(mockFile, mockOptions); + + expect(mockInstance.write).toHaveBeenCalledWith(mockFile, mockOptions); + }); + + it('should cover the "batchWrite" wrapper method', async () => { + const mockFile = Buffer.from('This is a test file content'); + + await adapter.batchWrite([mockFile]); + expect(mockInstance.batchWrite).toHaveBeenCalledWith([mockFile]); + }); + + it('should cover the "remove" wrapper method', async () => { + await adapter.remove('test.txt'); + expect(mockInstance.remove).toHaveBeenCalledWith('test.txt'); + }); + + it('should cover the "isExists" wrapper method', async () => { + await adapter.isExists('test.txt'); + expect(mockInstance.isExists).toHaveBeenCalledWith('test.txt'); + }); + }); + + describe('AzureBlobAdapter Wrapper Unit Test', () => { + let adapter: AzureBlobAdapter; + const mockConfig = { + connectionString: + 'DefaultEndpointsProtocol=https;AccountName=test;AccountKey=test;EndpointSuffix=core.windows.net', + container: 'test-container', + key: 'test-key', + }; + + beforeEach(() => { + jest.clearAllMocks(); + + adapter = new AzureBlobAdapter(mockConfig); + }); + + it('should create and configure the real AzureBlob service on construction', () => { + expect(StorageAzureBlobService).toHaveBeenCalledWith(mockConfig); + }); + + it('should cover the "url" wrapper method', async () => { + mockInstance.url.mockResolvedValue('http://mocked-url.com'); + + const url = await adapter.url('test.txt', { expires: 3600 }); + + expect(mockInstance.url).toHaveBeenCalledWith('test.txt', 3600); + expect(url).toBe('http://mocked-url.com'); + }); + + it('should cover the "write" wrapper method', async () => { + const mockFile = Buffer.from('This is a test file content'); + const mockOptions: WriteFileOptions = { + filename: 'my-file-name', + contentType: 'application/pdf', + }; + + await adapter.write(mockFile, mockOptions); + + expect(mockInstance.write).toHaveBeenCalledWith(mockFile, mockOptions); + }); + + it('should cover the "batchWrite" wrapper method', async () => { + const mockFile = Buffer.from('This is a test file content'); + + await adapter.batchWrite([mockFile]); + expect(mockInstance.batchWrite).toHaveBeenCalledWith([mockFile]); + }); + + it('should cover the "remove" wrapper method', async () => { + await adapter.remove('test.txt'); + expect(mockInstance.remove).toHaveBeenCalledWith('test.txt'); + }); + + it('should cover the "isExists" wrapper method', async () => { + await adapter.isExists('test.txt'); + expect(mockInstance.isExists).toHaveBeenCalledWith('test.txt'); + }); + }); + + describe('S3Adapter Wrapper Unit Test', () => { + let adapter: S3Adapter; + const mockConfig = { + accessKey: 'test-access-key', + secretKey: 'test-secret-key', + bucket: 'test-bucket', + region: 'test-region', + endpoint: 'test-endpoint', + key: 'test-key', + }; + + beforeEach(() => { + jest.clearAllMocks(); + + adapter = new S3Adapter(mockConfig); + }); + + it('should create and configure the real S3 service on construction', () => { + expect(StorageS3Service).toHaveBeenCalledWith(mockConfig); + }); + + it('should cover the "url" wrapper method', async () => { + mockInstance.url.mockResolvedValue('http://mocked-url.com'); + + const url = await adapter.url('test.txt'); - expect(adapter).toBeInstanceOf(GCSAdapter); + expect(mockInstance.url).toHaveBeenCalledWith('test.txt'); + expect(url).toBe('http://mocked-url.com'); + }); + + it('should cover the "write" wrapper method', async () => { + const mockFile = Buffer.from('This is a test file content'); + const mockOptions: WriteFileOptions = { + filename: 'my-file-name', + contentType: 'application/pdf', + }; + + await adapter.write(mockFile, mockOptions); + + expect(mockInstance.write).toHaveBeenCalledWith(mockFile, mockOptions); + }); + + it('should cover the "batchWrite" wrapper method', async () => { + const mockFile = Buffer.from('This is a test file content'); + + await adapter.batchWrite([mockFile]); + expect(mockInstance.batchWrite).toHaveBeenCalledWith([mockFile]); + }); + + it('should cover the "remove" wrapper method', async () => { + await adapter.remove('test.txt'); + expect(mockInstance.remove).toHaveBeenCalledWith('test.txt'); + }); + + it('should cover the "isExists" wrapper method', async () => { + await adapter.isExists('test.txt'); + expect(mockInstance.isExists).toHaveBeenCalledWith('test.txt'); + }); + }); + + describe('R2Adapter Wrapper Unit Test', () => { + let adapter: R2Adapter; + const mockConfig = { + accessKey: 'test-access-key', + secretKey: 'test-secret-key', + bucket: 'test-bucket', + account: 'test-account', + customeDomain: 'test-custom-domain', + key: 'test-key', + }; + + beforeEach(() => { + jest.clearAllMocks(); + + adapter = new R2Adapter(mockConfig); + }); + + it('should create and configure the real R2 service on construction', () => { + expect(StorageR2Service).toHaveBeenCalledWith(mockConfig); + }); + + it('should cover the "url" wrapper method', async () => { + mockInstance.url.mockResolvedValue('http://mocked-url.com'); + + const url = await adapter.url('test.txt', { expires: 3600 }); + + expect(mockInstance.url).toHaveBeenCalledWith('test.txt', { expires: 3600 }); + expect(url).toBe('http://mocked-url.com'); + }); + + it('should cover the "write" wrapper method', async () => { + const mockFile = Buffer.from('This is a test file content'); + const mockOptions: WriteFileOptions = { + filename: 'my-file-name', + contentType: 'application/pdf', + }; + + await adapter.write(mockFile, mockOptions); + + expect(mockInstance.write).toHaveBeenCalledWith(mockFile, mockOptions); + }); + + it('should cover the "batchWrite" wrapper method', async () => { + const mockFile = Buffer.from('This is a test file content'); + + await adapter.batchWrite([mockFile]); + expect(mockInstance.batchWrite).toHaveBeenCalledWith([mockFile]); + }); + + it('should cover the "remove" wrapper method', async () => { + await adapter.remove('test.txt'); + expect(mockInstance.remove).toHaveBeenCalledWith('test.txt'); + }); + + it('should cover the "isExists" wrapper method', async () => { + await adapter.isExists('test.txt'); + expect(mockInstance.isExists).toHaveBeenCalledWith('test.txt'); + }); + }); + + describe('LocalAdapter Wrapper Unit Test', () => { + let adapter: LocalAdapter; + const mockConfig = { + directory: 'test-directory', + autoMkdir: true, + }; + + beforeEach(() => { + jest.clearAllMocks(); + + adapter = new LocalAdapter(mockConfig); + }); + + it('should create and configure the real Local service on construction', () => { + expect(LocalStorage).toHaveBeenCalledWith(mockConfig); + }); + + it('should cover the "write" wrapper method', async () => { + const mockFile = Buffer.from('This is a test file content'); + const mockOptions: WriteFileOptions = { + filename: 'my-file-name', + contentType: 'application/pdf', + }; + + await adapter.write(mockFile, mockOptions); + + expect(mockInstance.write).toHaveBeenCalledWith(mockFile, mockOptions); + }); + + it('should cover the "batchWrite" wrapper method', async () => { + const mockFile = Buffer.from('This is a test file content'); + + await adapter.batchWrite([mockFile]); + expect(mockInstance.batchWrite).toHaveBeenCalledWith([mockFile]); + }); + + it('should cover the "remove" wrapper method', async () => { + await adapter.remove('test.txt'); + expect(mockInstance.remove).toHaveBeenCalledWith('test.txt'); + }); + + it('should cover the "isExists" wrapper method', async () => { + await adapter.isExists('test.txt'); + expect(mockInstance.isExists).toHaveBeenCalledWith('test.txt'); + }); }); }); diff --git a/packages/storages-base-nestjs-module/__tests__/storages-base.service.spec.ts b/packages/storages-base-nestjs-module/__tests__/storages-base.service.spec.ts new file mode 100644 index 00000000..9490fca3 --- /dev/null +++ b/packages/storages-base-nestjs-module/__tests__/storages-base.service.spec.ts @@ -0,0 +1,247 @@ +import { StorageService } from '../src/services/storages-base.service'; +import { Test, TestingModule } from '@nestjs/testing'; +import { + IStorageAdapter, + IStorageAdapterUrlOptions, + StorageBaseModuleOptions, +} from '../src/typings/storage-base-module-options.interface'; +import { Injectable, Type } from '@nestjs/common'; +import { STORAGE_ADAPTER, STORAGE_MODULE_OPTIONS } from '../src/typings/storages-base-module-providers'; +import { jest } from '@jest/globals'; +import { InputFile, StorageFile, WriteFileOptions } from '@rytass/storages'; + +@Injectable() +class mockAdapter implements IStorageAdapter { + url: jest.Mock<(key: string, options?: IStorageAdapterUrlOptions) => Promise>; + write: jest.Mock<(file: InputFile, options?: WriteFileOptions) => Promise>; + batchWrite: jest.Mock<(files: InputFile[]) => Promise>; + remove: jest.Mock<(key: string) => Promise>; + removeSync: jest.Mock<(key: string) => Promise>; + isExists: jest.Mock<(key: string) => Promise>; + + constructor(_config: unknown) { + this.url = jest.fn(async (key: string, _options?: IStorageAdapterUrlOptions) => `http://mock-url.com/${key}`); + this.write = jest.fn(); + this.batchWrite = jest.fn(); + this.remove = jest.fn(); + this.removeSync = jest.fn(); + this.isExists = jest.fn(); + } +} + +const mockOptions: StorageBaseModuleOptions> = { + adapter: mockAdapter, + config: {}, + commonOptions: { + MaxFileSizeInBytes: 100, + }, +}; + +describe('Storages Base Service', () => { + let service: StorageService; + let adapter: IStorageAdapter; + + describe('StorageService.url', () => { + it('should throw error if adapter is not in the list', async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + StorageService, + { + provide: STORAGE_ADAPTER, + useValue: new mockAdapter({}), + }, + { + provide: STORAGE_MODULE_OPTIONS, + useValue: mockOptions, + }, + ], + }).compile(); + + service = module.get(StorageService); + + await expect(service.url('my-key', 3600)).rejects.toThrow('Unknown storage adapter'); + }); + + it('should throw error if adapter is LocalAdapter', async () => { + const localMockInstance = new mockAdapter({}); + + Object.defineProperty(localMockInstance, 'constructor', { + value: { name: 'LocalAdapter' }, + }); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + StorageService, + { + provide: STORAGE_ADAPTER, + useValue: localMockInstance, + }, + { + provide: STORAGE_MODULE_OPTIONS, + useValue: mockOptions, + }, + ], + }).compile(); + + service = module.get(StorageService); + adapter = module.get(STORAGE_ADAPTER); + + await expect(service.url('my-key', 3600)).rejects.toThrow('LocalStorage does not support URL generation'); + }); + + it('should call url() with a number - GCS', async () => { + const gcsMockInstance = new mockAdapter({}); + + Object.defineProperty(gcsMockInstance, 'constructor', { + value: { name: 'GCSAdapter' }, + }); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + StorageService, + { + provide: STORAGE_ADAPTER, + useValue: gcsMockInstance, + }, + { + provide: STORAGE_MODULE_OPTIONS, + useValue: mockOptions, + }, + ], + }).compile(); + + service = module.get(StorageService); + adapter = module.get(STORAGE_ADAPTER); + + await service.url('my-key', 3600); + + expect(adapter.url).toHaveBeenCalledWith('my-key', 3600); + }); + + it('should call url() with no options - S3', async () => { + const s3MockInstance = new mockAdapter({}); + + Object.defineProperty(s3MockInstance, 'constructor', { + value: { name: 'S3Adapter' }, + }); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + StorageService, + { + provide: STORAGE_ADAPTER, + useValue: s3MockInstance, + }, + { + provide: STORAGE_MODULE_OPTIONS, + useValue: mockOptions, + }, + ], + }).compile(); + + service = module.get(StorageService); + adapter = module.get(STORAGE_ADAPTER); + + await service.url('my-key', 3600); + + expect(adapter.url).toHaveBeenCalledWith('my-key'); + }); + + it('should call url() with options - R2', async () => { + const r2MockInstance = new mockAdapter({}); + + Object.defineProperty(r2MockInstance, 'constructor', { + value: { name: 'R2Adapter' }, + }); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + StorageService, + { + provide: STORAGE_ADAPTER, + useValue: r2MockInstance, + }, + { + provide: STORAGE_MODULE_OPTIONS, + useValue: mockOptions, + }, + ], + }).compile(); + + service = module.get(StorageService); + adapter = module.get(STORAGE_ADAPTER); + + await service.url('my-key', { + expires: 3600, + }); + + expect(adapter.url).toHaveBeenCalledWith('my-key', { + expires: 3600, + }); + }); + }); + + describe('Other Functions', () => { + const mockFile = Buffer.from('This is a test file content'); + + const mockOptions: WriteFileOptions = { + filename: 'my-file-name', + contentType: 'application/pdf', + }; + + beforeEach(async () => { + jest.clearAllMocks(); + + const gcsMockInstance = new mockAdapter({}); + + Object.defineProperty(gcsMockInstance, 'constructor', { + value: { name: 'GCSAdapter' }, + }); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + StorageService, + { + provide: STORAGE_ADAPTER, + useValue: gcsMockInstance, + }, + { + provide: STORAGE_MODULE_OPTIONS, + useValue: mockOptions, + }, + ], + }).compile(); + + service = module.get(StorageService); + adapter = module.get(STORAGE_ADAPTER); + }); + + it('should write file', async () => { + await service.write(mockFile, mockOptions); + + expect(adapter.write).toHaveBeenCalledWith(mockFile, mockOptions); + expect(adapter.write).toHaveBeenCalledTimes(1); + }); + + it('should batch write files', async () => { + await service.batchWrite([mockFile], [mockOptions]); + + expect(adapter.batchWrite).toHaveBeenCalledWith([mockFile], [mockOptions]); + expect(adapter.batchWrite).toHaveBeenCalledTimes(1); + }); + + it('should remove file', async () => { + await service.remove('my-key'); + + expect(adapter.remove).toHaveBeenCalledWith('my-key'); + expect(adapter.remove).toHaveBeenCalledTimes(1); + }); + + it('should call isExists with correct key', async () => { + await service.isExists('my-key'); + + expect(adapter.isExists).toHaveBeenCalledWith('my-key'); + expect(adapter.isExists).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/storages-base-nestjs-module/src/services/storages-base.service.ts b/packages/storages-base-nestjs-module/src/services/storages-base.service.ts index f8677853..2708f1d0 100644 --- a/packages/storages-base-nestjs-module/src/services/storages-base.service.ts +++ b/packages/storages-base-nestjs-module/src/services/storages-base.service.ts @@ -41,23 +41,23 @@ export class StorageService { async url(key: string, params?: number | unknown): Promise { const adapterName = this._adapter.constructor.name; - if (adapterName === 'LocalStorage') { + if (adapterName === 'LocalAdapter') { throw new Error('LocalStorage does not support URL generation'); } - if (adapterName === 'StorageAzureBlobService' || adapterName === 'StorageGCSService') { + if (adapterName === 'AzureBlobAdapter' || adapterName === 'GCSAdapter') { const expires = params as number; type UrlWithExpires = { url: (k: string, e: number) => Promise }; const adapter = this._adapter as unknown as UrlWithExpires; return adapter.url(key, expires); - } else if (adapterName === 'StorageS3Service') { + } else if (adapterName === 'S3Adapter') { type UrlNoOptions = { url: (k: string) => Promise }; const adapter = this._adapter as unknown as UrlNoOptions; return adapter.url(key); - } else if (adapterName === 'StorageR2Service') { + } else if (adapterName === 'R2Adapter') { type R2Like = { url: (k: string, o?: unknown) => Promise }; const r2 = this._adapter as unknown as R2Like; const options = params as unknown; @@ -80,10 +80,6 @@ export class StorageService { return this._adapter.remove(key); } - removeSync(key: string): Promise { - return this._adapter.remove(key); - } - async isExists(key: string): Promise { return this._adapter.isExists(key); } diff --git a/packages/storages-base-nestjs-module/src/typings/storage-base-module-options.interface.ts b/packages/storages-base-nestjs-module/src/typings/storage-base-module-options.interface.ts index 1c85eece..5ebaa289 100644 --- a/packages/storages-base-nestjs-module/src/typings/storage-base-module-options.interface.ts +++ b/packages/storages-base-nestjs-module/src/typings/storage-base-module-options.interface.ts @@ -41,11 +41,10 @@ export interface StorageModuleCommonOptions { export interface IStorageAdapterUrlOptions { expires?: number; - [key: string]: unknown; } export interface IStorageAdapter { - url(key: string, options?: IStorageAdapterUrlOptions): Promise; + url?(key?: string, options?: IStorageAdapterUrlOptions): Promise; write(file: InputFile, options?: WriteFileOptions): Promise; remove(key: string): Promise; isExists(key: string): Promise; diff --git a/packages/storages-base-nestjs-module/src/wrappers/azure-blob-wrapper.ts b/packages/storages-base-nestjs-module/src/wrappers/azure-blob-wrapper.ts new file mode 100644 index 00000000..f1636028 --- /dev/null +++ b/packages/storages-base-nestjs-module/src/wrappers/azure-blob-wrapper.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { IStorageAdapter, IStorageAdapterUrlOptions } from '../typings/storage-base-module-options.interface'; +import { InputFile, WriteFileOptions, StorageFile } from '@rytass/storages'; +import { StorageAzureBlobService } from '@rytass/storages-adapter-azure-blob'; +import type { AzureBlobOptions } from 'storages-adapter-azure-blob/lib'; + +@Injectable() +export class AzureBlobAdapter implements IStorageAdapter { + private readonly azureBlobService: StorageAzureBlobService; + + constructor(config: AzureBlobOptions) { + this.azureBlobService = new StorageAzureBlobService(config); + } + + url(key: string, options?: IStorageAdapterUrlOptions): Promise { + return this.azureBlobService.url(key, options?.expires); + } + + write(file: InputFile, options?: WriteFileOptions): Promise { + return this.azureBlobService.write(file, options); + } + + batchWrite(files: InputFile[]): Promise { + return this.azureBlobService.batchWrite(files); + } + + remove(key: string): Promise { + return this.azureBlobService.remove(key); + } + + isExists(key: string): Promise { + return this.azureBlobService.isExists(key); + } +} diff --git a/packages/storages-base-nestjs-module/src/wrappers/storages-base-gcs-wrapper.ts b/packages/storages-base-nestjs-module/src/wrappers/gcs-wrapper.ts similarity index 88% rename from packages/storages-base-nestjs-module/src/wrappers/storages-base-gcs-wrapper.ts rename to packages/storages-base-nestjs-module/src/wrappers/gcs-wrapper.ts index f619b7cd..abbc43bd 100644 --- a/packages/storages-base-nestjs-module/src/wrappers/storages-base-gcs-wrapper.ts +++ b/packages/storages-base-nestjs-module/src/wrappers/gcs-wrapper.ts @@ -8,13 +8,10 @@ import { InputFile, StorageFile, WriteFileOptions } from '@rytass/storages'; export class GCSAdapter implements IStorageAdapter { private readonly gcsService: StorageGCSService; - // The wrapper's constructor takes the *real* config constructor(config: GCSOptions) { this.gcsService = new StorageGCSService(config); } - // --- This is the "translation" --- - // Your interface expects an 'options' object. url(key: string, options?: IStorageAdapterUrlOptions): Promise { return this.gcsService.url(key, options?.expires); } diff --git a/packages/storages-base-nestjs-module/src/wrappers/local-wrapper.ts b/packages/storages-base-nestjs-module/src/wrappers/local-wrapper.ts new file mode 100644 index 00000000..520e5571 --- /dev/null +++ b/packages/storages-base-nestjs-module/src/wrappers/local-wrapper.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { IStorageAdapter } from '../typings/storage-base-module-options.interface'; +import { InputFile, WriteFileOptions, StorageFile } from '@rytass/storages'; +import { LocalStorage } from '@rytass/storages-adapter-local'; +import type { StorageLocalOptions } from 'storages-adapter-local/lib'; + +@Injectable() +export class LocalAdapter implements IStorageAdapter { + private readonly localStorage: LocalStorage; + + constructor(options: StorageLocalOptions) { + this.localStorage = new LocalStorage(options); + } + + write(file: InputFile, options?: WriteFileOptions): Promise { + return this.localStorage.write(file, options); + } + + batchWrite(files: InputFile[]): Promise { + return this.localStorage.batchWrite(files); + } + + remove(key: string): Promise { + return this.localStorage.remove(key); + } + + isExists(key: string): Promise { + return this.localStorage.isExists(key); + } +} diff --git a/packages/storages-base-nestjs-module/src/wrappers/r2-wrapper.ts b/packages/storages-base-nestjs-module/src/wrappers/r2-wrapper.ts new file mode 100644 index 00000000..592a5cf3 --- /dev/null +++ b/packages/storages-base-nestjs-module/src/wrappers/r2-wrapper.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { IStorageAdapter, IStorageAdapterUrlOptions } from '../typings/storage-base-module-options.interface'; +import { InputFile, WriteFileOptions, StorageFile } from '@rytass/storages'; +import { StorageR2Service } from '@rytass/storages-adapter-r2/src'; +import type { StorageR2Options } from 'storages-adapter-r2/src/typings'; + +@Injectable() +export class R2Adapter implements IStorageAdapter { + private readonly r2Service: StorageR2Service; + + constructor(config: StorageR2Options) { + this.r2Service = new StorageR2Service(config); + } + + url(key: string, options: IStorageAdapterUrlOptions): Promise { + return this.r2Service.url(key, options); + } + + write(file: InputFile, options?: WriteFileOptions): Promise { + return this.r2Service.write(file, options); + } + + batchWrite(files: InputFile[]): Promise { + return this.r2Service.batchWrite(files); + } + + remove(key: string): Promise { + return this.r2Service.remove(key); + } + + isExists(key: string): Promise { + return this.r2Service.isExists(key); + } +} diff --git a/packages/storages-base-nestjs-module/src/wrappers/s3-wrapper.ts b/packages/storages-base-nestjs-module/src/wrappers/s3-wrapper.ts new file mode 100644 index 00000000..f95a7266 --- /dev/null +++ b/packages/storages-base-nestjs-module/src/wrappers/s3-wrapper.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { IStorageAdapter } from '../typings/storage-base-module-options.interface'; +import { StorageS3Service } from '@rytass/storages-adapter-s3/src'; +import { InputFile, WriteFileOptions, StorageFile } from '@rytass/storages'; +import type { StorageS3Options } from 'storages-adapter-s3/src/typings'; + +@Injectable() +export class S3Adapter implements IStorageAdapter { + private readonly s3Service: StorageS3Service; + + constructor(config: StorageS3Options) { + this.s3Service = new StorageS3Service(config); + } + + url(key: string): Promise { + return this.s3Service.url(key); + } + + write(file: InputFile, options?: WriteFileOptions): Promise { + return this.s3Service.write(file, options); + } + + batchWrite(files: InputFile[]): Promise { + return this.s3Service.batchWrite(files); + } + + remove(key: string): Promise { + return this.s3Service.remove(key); + } + + isExists(key: string): Promise { + return this.s3Service.isExists(key); + } +} From a67251916befe9719e1fc78c4e65b9d443cf9f7e Mon Sep 17 00:00:00 2001 From: geoffreyfarel Date: Tue, 11 Nov 2025 12:16:23 +0800 Subject: [PATCH 11/12] fix: adapters are in optionalDependencies, removed wrapper classes, replaced constructor.name checking with generic type, no conditionals --- .../__tests__/storages-base-module.spec.ts | 348 ++---------------- .../__tests__/storages-base.service.spec.ts | 189 +++++----- .../storages-base-nestjs-module/package.json | 4 +- .../src/services/storages-base.service.ts | 58 +-- .../src/storages-base.module.ts | 8 +- .../storage-base-module-options.interface.ts | 15 +- .../src/wrappers/azure-blob-wrapper.ts | 34 -- .../src/wrappers/gcs-wrapper.ts | 34 -- .../src/wrappers/local-wrapper.ts | 30 -- .../src/wrappers/r2-wrapper.ts | 34 -- .../src/wrappers/s3-wrapper.ts | 34 -- 11 files changed, 150 insertions(+), 638 deletions(-) delete mode 100644 packages/storages-base-nestjs-module/src/wrappers/azure-blob-wrapper.ts delete mode 100644 packages/storages-base-nestjs-module/src/wrappers/gcs-wrapper.ts delete mode 100644 packages/storages-base-nestjs-module/src/wrappers/local-wrapper.ts delete mode 100644 packages/storages-base-nestjs-module/src/wrappers/r2-wrapper.ts delete mode 100644 packages/storages-base-nestjs-module/src/wrappers/s3-wrapper.ts diff --git a/packages/storages-base-nestjs-module/__tests__/storages-base-module.spec.ts b/packages/storages-base-nestjs-module/__tests__/storages-base-module.spec.ts index 34ab9195..ffaa82bb 100644 --- a/packages/storages-base-nestjs-module/__tests__/storages-base-module.spec.ts +++ b/packages/storages-base-nestjs-module/__tests__/storages-base-module.spec.ts @@ -7,18 +7,8 @@ import { StorageBaseModuleOptionsFactory, } from '../src/typings/storage-base-module-options.interface'; import { STORAGE_ADAPTER, STORAGE_MODULE_OPTIONS } from '../src/typings/storages-base-module-providers'; -import { GCSAdapter } from '../src/wrappers/gcs-wrapper'; import { StorageService } from '@rytass/storages-base-nestjs-module'; import { StorageGCSService } from '@rytass/storages-adapter-gcs'; -import { WriteFileOptions } from '@rytass/storages'; -import { AzureBlobAdapter } from '../src/wrappers/azure-blob-wrapper'; -import { StorageAzureBlobService } from '@rytass/storages-adapter-azure-blob'; -import { S3Adapter } from '../src/wrappers/s3-wrapper'; -import { StorageS3Service } from '@rytass/storages-adapter-s3/src'; -import { StorageR2Service } from '@rytass/storages-adapter-r2/src'; -import { R2Adapter } from '../src/wrappers/r2-wrapper'; -import { LocalAdapter } from '../src/wrappers/local-wrapper'; -import { LocalStorage } from '@rytass/storages-adapter-local'; const mockInstance = { url: jest.fn(), @@ -33,40 +23,17 @@ jest.mock('@rytass/storages-adapter-gcs', () => ({ GCSOptions: jest.fn(), })); -jest.mock('@rytass/storages-adapter-azure-blob', () => ({ - StorageAzureBlobService: jest.fn(() => mockInstance), - AzureBlobOptions: jest.fn(), -})); - -jest.mock('@rytass/storages-adapter-s3/src', () => ({ - StorageS3Service: jest.fn(() => mockInstance), - StorageS3Options: jest.fn(), -})); - -jest.mock('@rytass/storages-adapter-r2/src', () => ({ - StorageR2Service: jest.fn(() => mockInstance), - StorageR2Options: jest.fn(), -})); - -jest.mock('@rytass/storages-adapter-local', () => ({ - LocalStorage: jest.fn(() => mockInstance), - StorageLocalOptions: jest.fn(), -})); - describe('Storages Base Nestjs Module', () => { @Injectable() class mockAdapter implements IStorageAdapter { - static config: unknown; - url = jest.fn(async (_key: string, _options?: unknown) => 'https://mockURL.com'); + url = jest.fn(async (key: string, _expires?: number) => `http://mock-url.com/${key}`); write = jest.fn(); batchWrite = jest.fn(); remove = jest.fn(); isExists = jest.fn(); - constructor(config: unknown) { - mockAdapter.config = config; - } + constructor(_config: unknown) {} } beforeEach(() => { @@ -91,293 +58,6 @@ describe('Storages Base Nestjs Module', () => { defaultPublic: false, }; - describe('Integration test', () => { - describe('GCSAdapter Wrapper Unit Test', () => { - let adapter: GCSAdapter; - const mockConfig = { - bucket: 'test-bucket', - projectId: 'test-project', - credentials: { - client_email: 'test@test.iam.gserviceaccount.com', - private_key: 'test-private-key', - }, - }; - - beforeEach(() => { - jest.clearAllMocks(); - - adapter = new GCSAdapter(mockConfig); - }); - - it('should create and configure the real GCS service on construction', () => { - expect(StorageGCSService).toHaveBeenCalledWith(mockConfig); - }); - - it('should cover the "url" wrapper method', async () => { - mockInstance.url.mockResolvedValue('http://mocked-url.com'); - - const url = await adapter.url('test.txt', { expires: 3600 }); - - expect(mockInstance.url).toHaveBeenCalledWith('test.txt', 3600); - expect(url).toBe('http://mocked-url.com'); - }); - - it('should cover the "write" wrapper method', async () => { - const mockFile = Buffer.from('This is a test file content'); - const mockOptions: WriteFileOptions = { - filename: 'my-file-name', - contentType: 'application/pdf', - }; - - await adapter.write(mockFile, mockOptions); - - expect(mockInstance.write).toHaveBeenCalledWith(mockFile, mockOptions); - }); - - it('should cover the "batchWrite" wrapper method', async () => { - const mockFile = Buffer.from('This is a test file content'); - - await adapter.batchWrite([mockFile]); - expect(mockInstance.batchWrite).toHaveBeenCalledWith([mockFile]); - }); - - it('should cover the "remove" wrapper method', async () => { - await adapter.remove('test.txt'); - expect(mockInstance.remove).toHaveBeenCalledWith('test.txt'); - }); - - it('should cover the "isExists" wrapper method', async () => { - await adapter.isExists('test.txt'); - expect(mockInstance.isExists).toHaveBeenCalledWith('test.txt'); - }); - }); - - describe('AzureBlobAdapter Wrapper Unit Test', () => { - let adapter: AzureBlobAdapter; - const mockConfig = { - connectionString: - 'DefaultEndpointsProtocol=https;AccountName=test;AccountKey=test;EndpointSuffix=core.windows.net', - container: 'test-container', - key: 'test-key', - }; - - beforeEach(() => { - jest.clearAllMocks(); - - adapter = new AzureBlobAdapter(mockConfig); - }); - - it('should create and configure the real AzureBlob service on construction', () => { - expect(StorageAzureBlobService).toHaveBeenCalledWith(mockConfig); - }); - - it('should cover the "url" wrapper method', async () => { - mockInstance.url.mockResolvedValue('http://mocked-url.com'); - - const url = await adapter.url('test.txt', { expires: 3600 }); - - expect(mockInstance.url).toHaveBeenCalledWith('test.txt', 3600); - expect(url).toBe('http://mocked-url.com'); - }); - - it('should cover the "write" wrapper method', async () => { - const mockFile = Buffer.from('This is a test file content'); - const mockOptions: WriteFileOptions = { - filename: 'my-file-name', - contentType: 'application/pdf', - }; - - await adapter.write(mockFile, mockOptions); - - expect(mockInstance.write).toHaveBeenCalledWith(mockFile, mockOptions); - }); - - it('should cover the "batchWrite" wrapper method', async () => { - const mockFile = Buffer.from('This is a test file content'); - - await adapter.batchWrite([mockFile]); - expect(mockInstance.batchWrite).toHaveBeenCalledWith([mockFile]); - }); - - it('should cover the "remove" wrapper method', async () => { - await adapter.remove('test.txt'); - expect(mockInstance.remove).toHaveBeenCalledWith('test.txt'); - }); - - it('should cover the "isExists" wrapper method', async () => { - await adapter.isExists('test.txt'); - expect(mockInstance.isExists).toHaveBeenCalledWith('test.txt'); - }); - }); - - describe('S3Adapter Wrapper Unit Test', () => { - let adapter: S3Adapter; - const mockConfig = { - accessKey: 'test-access-key', - secretKey: 'test-secret-key', - bucket: 'test-bucket', - region: 'test-region', - endpoint: 'test-endpoint', - key: 'test-key', - }; - - beforeEach(() => { - jest.clearAllMocks(); - - adapter = new S3Adapter(mockConfig); - }); - - it('should create and configure the real S3 service on construction', () => { - expect(StorageS3Service).toHaveBeenCalledWith(mockConfig); - }); - - it('should cover the "url" wrapper method', async () => { - mockInstance.url.mockResolvedValue('http://mocked-url.com'); - - const url = await adapter.url('test.txt'); - - expect(mockInstance.url).toHaveBeenCalledWith('test.txt'); - expect(url).toBe('http://mocked-url.com'); - }); - - it('should cover the "write" wrapper method', async () => { - const mockFile = Buffer.from('This is a test file content'); - const mockOptions: WriteFileOptions = { - filename: 'my-file-name', - contentType: 'application/pdf', - }; - - await adapter.write(mockFile, mockOptions); - - expect(mockInstance.write).toHaveBeenCalledWith(mockFile, mockOptions); - }); - - it('should cover the "batchWrite" wrapper method', async () => { - const mockFile = Buffer.from('This is a test file content'); - - await adapter.batchWrite([mockFile]); - expect(mockInstance.batchWrite).toHaveBeenCalledWith([mockFile]); - }); - - it('should cover the "remove" wrapper method', async () => { - await adapter.remove('test.txt'); - expect(mockInstance.remove).toHaveBeenCalledWith('test.txt'); - }); - - it('should cover the "isExists" wrapper method', async () => { - await adapter.isExists('test.txt'); - expect(mockInstance.isExists).toHaveBeenCalledWith('test.txt'); - }); - }); - - describe('R2Adapter Wrapper Unit Test', () => { - let adapter: R2Adapter; - const mockConfig = { - accessKey: 'test-access-key', - secretKey: 'test-secret-key', - bucket: 'test-bucket', - account: 'test-account', - customeDomain: 'test-custom-domain', - key: 'test-key', - }; - - beforeEach(() => { - jest.clearAllMocks(); - - adapter = new R2Adapter(mockConfig); - }); - - it('should create and configure the real R2 service on construction', () => { - expect(StorageR2Service).toHaveBeenCalledWith(mockConfig); - }); - - it('should cover the "url" wrapper method', async () => { - mockInstance.url.mockResolvedValue('http://mocked-url.com'); - - const url = await adapter.url('test.txt', { expires: 3600 }); - - expect(mockInstance.url).toHaveBeenCalledWith('test.txt', { expires: 3600 }); - expect(url).toBe('http://mocked-url.com'); - }); - - it('should cover the "write" wrapper method', async () => { - const mockFile = Buffer.from('This is a test file content'); - const mockOptions: WriteFileOptions = { - filename: 'my-file-name', - contentType: 'application/pdf', - }; - - await adapter.write(mockFile, mockOptions); - - expect(mockInstance.write).toHaveBeenCalledWith(mockFile, mockOptions); - }); - - it('should cover the "batchWrite" wrapper method', async () => { - const mockFile = Buffer.from('This is a test file content'); - - await adapter.batchWrite([mockFile]); - expect(mockInstance.batchWrite).toHaveBeenCalledWith([mockFile]); - }); - - it('should cover the "remove" wrapper method', async () => { - await adapter.remove('test.txt'); - expect(mockInstance.remove).toHaveBeenCalledWith('test.txt'); - }); - - it('should cover the "isExists" wrapper method', async () => { - await adapter.isExists('test.txt'); - expect(mockInstance.isExists).toHaveBeenCalledWith('test.txt'); - }); - }); - - describe('LocalAdapter Wrapper Unit Test', () => { - let adapter: LocalAdapter; - const mockConfig = { - directory: 'test-directory', - autoMkdir: true, - }; - - beforeEach(() => { - jest.clearAllMocks(); - - adapter = new LocalAdapter(mockConfig); - }); - - it('should create and configure the real Local service on construction', () => { - expect(LocalStorage).toHaveBeenCalledWith(mockConfig); - }); - - it('should cover the "write" wrapper method', async () => { - const mockFile = Buffer.from('This is a test file content'); - const mockOptions: WriteFileOptions = { - filename: 'my-file-name', - contentType: 'application/pdf', - }; - - await adapter.write(mockFile, mockOptions); - - expect(mockInstance.write).toHaveBeenCalledWith(mockFile, mockOptions); - }); - - it('should cover the "batchWrite" wrapper method', async () => { - const mockFile = Buffer.from('This is a test file content'); - - await adapter.batchWrite([mockFile]); - expect(mockInstance.batchWrite).toHaveBeenCalledWith([mockFile]); - }); - - it('should cover the "remove" wrapper method', async () => { - await adapter.remove('test.txt'); - expect(mockInstance.remove).toHaveBeenCalledWith('test.txt'); - }); - - it('should cover the "isExists" wrapper method', async () => { - await adapter.isExists('test.txt'); - expect(mockInstance.isExists).toHaveBeenCalledWith('test.txt'); - }); - }); - }); - describe('forRoot', () => { beforeEach(() => { jest.clearAllMocks(); @@ -420,6 +100,30 @@ describe('Storages Base Nestjs Module', () => { MaxFileSizeInBytes: 999, }); }); + + it('should work with real adapter class', async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + StorageBaseModule.forRoot({ + adapter: StorageGCSService, + config: { + bucket: 'test-bucket', + projectId: 'test-project-id', + credentials: { + client_email: 'test-client-email', + private_key: 'test-private-key', + }, + }, + }), + ], + }).compile(); + + const service = module.get(StorageService); + const adapter = module.get(STORAGE_ADAPTER); + + expect(adapter).toBe(mockInstance); + expect(service).toBeDefined(); + }); }); describe('forRootAsync', () => { diff --git a/packages/storages-base-nestjs-module/__tests__/storages-base.service.spec.ts b/packages/storages-base-nestjs-module/__tests__/storages-base.service.spec.ts index 9490fca3..c4c7409c 100644 --- a/packages/storages-base-nestjs-module/__tests__/storages-base.service.spec.ts +++ b/packages/storages-base-nestjs-module/__tests__/storages-base.service.spec.ts @@ -1,111 +1,119 @@ import { StorageService } from '../src/services/storages-base.service'; import { Test, TestingModule } from '@nestjs/testing'; -import { - IStorageAdapter, - IStorageAdapterUrlOptions, - StorageBaseModuleOptions, -} from '../src/typings/storage-base-module-options.interface'; -import { Injectable, Type } from '@nestjs/common'; +import { IStorageAdapter, IStorageAdapterUrlOptions } from '../src/typings/storage-base-module-options.interface'; +import { Type } from '@nestjs/common'; import { STORAGE_ADAPTER, STORAGE_MODULE_OPTIONS } from '../src/typings/storages-base-module-providers'; import { jest } from '@jest/globals'; import { InputFile, StorageFile, WriteFileOptions } from '@rytass/storages'; -@Injectable() -class mockAdapter implements IStorageAdapter { +class MockGCSAdapter implements IStorageAdapter { + url: jest.Mock<(key: string, expires?: number) => Promise>; + write: jest.Mock<(file: InputFile, options?: WriteFileOptions) => Promise>; + batchWrite: jest.Mock<(files: InputFile[], options?: WriteFileOptions[]) => Promise>; + remove: jest.Mock<(key: string) => Promise>; + isExists: jest.Mock<(key: string) => Promise>; + + constructor() { + this.url = jest.fn(async (key: string, _expires?: number) => `http://mock-url.com/${key}`); + this.write = jest.fn(); + this.batchWrite = jest.fn(); + this.remove = jest.fn(); + this.isExists = jest.fn(); + } +} + +class MockS3Adapter implements IStorageAdapter { + url: jest.Mock<(key: string) => Promise>; + write: jest.Mock<(file: InputFile, options?: WriteFileOptions) => Promise>; + batchWrite: jest.Mock<(files: InputFile[], options?: WriteFileOptions[]) => Promise>; + remove: jest.Mock<(key: string) => Promise>; + isExists: jest.Mock<(key: string) => Promise>; + + constructor() { + this.url = jest.fn(async (key: string) => `http://mock-url.com/${key}`); + this.write = jest.fn(); + this.batchWrite = jest.fn(); + this.remove = jest.fn(); + this.isExists = jest.fn(); + } +} + +class MockR2Adapter implements IStorageAdapter { url: jest.Mock<(key: string, options?: IStorageAdapterUrlOptions) => Promise>; write: jest.Mock<(file: InputFile, options?: WriteFileOptions) => Promise>; - batchWrite: jest.Mock<(files: InputFile[]) => Promise>; + batchWrite: jest.Mock<(files: InputFile[], options?: WriteFileOptions[]) => Promise>; remove: jest.Mock<(key: string) => Promise>; - removeSync: jest.Mock<(key: string) => Promise>; isExists: jest.Mock<(key: string) => Promise>; - constructor(_config: unknown) { + constructor() { this.url = jest.fn(async (key: string, _options?: IStorageAdapterUrlOptions) => `http://mock-url.com/${key}`); this.write = jest.fn(); this.batchWrite = jest.fn(); this.remove = jest.fn(); - this.removeSync = jest.fn(); this.isExists = jest.fn(); } } -const mockOptions: StorageBaseModuleOptions> = { - adapter: mockAdapter, - config: {}, - commonOptions: { - MaxFileSizeInBytes: 100, - }, -}; +class MockLocalAdapter implements IStorageAdapter { + write: jest.Mock<(file: InputFile, options?: WriteFileOptions) => Promise>; + batchWrite: jest.Mock<(files: InputFile[], options?: WriteFileOptions[]) => Promise>; + remove: jest.Mock<(key: string) => Promise>; + isExists: jest.Mock<(key: string) => Promise>; + + constructor() { + this.write = jest.fn(); + this.batchWrite = jest.fn(); + this.remove = jest.fn(); + this.isExists = jest.fn(); + } +} describe('Storages Base Service', () => { let service: StorageService; let adapter: IStorageAdapter; describe('StorageService.url', () => { - it('should throw error if adapter is not in the list', async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - StorageService, - { - provide: STORAGE_ADAPTER, - useValue: new mockAdapter({}), - }, - { - provide: STORAGE_MODULE_OPTIONS, - useValue: mockOptions, - }, - ], - }).compile(); - - service = module.get(StorageService); - - await expect(service.url('my-key', 3600)).rejects.toThrow('Unknown storage adapter'); - }); - - it('should throw error if adapter is LocalAdapter', async () => { - const localMockInstance = new mockAdapter({}); - - Object.defineProperty(localMockInstance, 'constructor', { - value: { name: 'LocalAdapter' }, - }); + it('should throw error if adapter does not support URL generation', async () => { + const localAdapter = new MockLocalAdapter(); const module: TestingModule = await Test.createTestingModule({ providers: [ StorageService, { provide: STORAGE_ADAPTER, - useValue: localMockInstance, + useValue: localAdapter, }, { provide: STORAGE_MODULE_OPTIONS, - useValue: mockOptions, + useValue: { + adapter: MockLocalAdapter as Type, + config: {}, + }, }, ], }).compile(); service = module.get(StorageService); - adapter = module.get(STORAGE_ADAPTER); - await expect(service.url('my-key', 3600)).rejects.toThrow('LocalStorage does not support URL generation'); + expect(() => service.url('my-key')).toThrow('This storage adapter does not support URL generation'); }); - it('should call url() with a number - GCS', async () => { - const gcsMockInstance = new mockAdapter({}); - - Object.defineProperty(gcsMockInstance, 'constructor', { - value: { name: 'GCSAdapter' }, - }); + it('should call url() with expires parameter - GCS-like adapter', async () => { + const gcsAdapter = new MockGCSAdapter(); const module: TestingModule = await Test.createTestingModule({ providers: [ StorageService, { provide: STORAGE_ADAPTER, - useValue: gcsMockInstance, + useValue: gcsAdapter, }, { provide: STORAGE_MODULE_OPTIONS, - useValue: mockOptions, + useValue: { + adapter: MockGCSAdapter as Type, + config: {}, + }, }, ], }).compile(); @@ -116,25 +124,25 @@ describe('Storages Base Service', () => { await service.url('my-key', 3600); expect(adapter.url).toHaveBeenCalledWith('my-key', 3600); + expect(adapter.url).toHaveBeenCalledTimes(1); }); - it('should call url() with no options - S3', async () => { - const s3MockInstance = new mockAdapter({}); - - Object.defineProperty(s3MockInstance, 'constructor', { - value: { name: 'S3Adapter' }, - }); + it('should call url() with only key parameter - S3-like adapter', async () => { + const s3Adapter = new MockS3Adapter(); const module: TestingModule = await Test.createTestingModule({ providers: [ StorageService, { provide: STORAGE_ADAPTER, - useValue: s3MockInstance, + useValue: s3Adapter, }, { provide: STORAGE_MODULE_OPTIONS, - useValue: mockOptions, + useValue: { + adapter: MockS3Adapter as Type, + config: {}, + }, }, ], }).compile(); @@ -142,28 +150,29 @@ describe('Storages Base Service', () => { service = module.get(StorageService); adapter = module.get(STORAGE_ADAPTER); - await service.url('my-key', 3600); + // TypeScript should enforce this signature - only key, no expires + await service.url('my-key'); expect(adapter.url).toHaveBeenCalledWith('my-key'); + expect(adapter.url).toHaveBeenCalledTimes(1); }); - it('should call url() with options - R2', async () => { - const r2MockInstance = new mockAdapter({}); - - Object.defineProperty(r2MockInstance, 'constructor', { - value: { name: 'R2Adapter' }, - }); + it('should call url() with options parameter - R2-like adapter', async () => { + const r2Adapter = new MockR2Adapter(); const module: TestingModule = await Test.createTestingModule({ providers: [ StorageService, { provide: STORAGE_ADAPTER, - useValue: r2MockInstance, + useValue: r2Adapter, }, { provide: STORAGE_MODULE_OPTIONS, - useValue: mockOptions, + useValue: { + adapter: MockR2Adapter as Type, + config: {}, + }, }, ], }).compile(); @@ -171,20 +180,17 @@ describe('Storages Base Service', () => { service = module.get(StorageService); adapter = module.get(STORAGE_ADAPTER); - await service.url('my-key', { - expires: 3600, - }); + await service.url('my-key', { expires: 3600 }); - expect(adapter.url).toHaveBeenCalledWith('my-key', { - expires: 3600, - }); + expect(adapter.url).toHaveBeenCalledWith('my-key', { expires: 3600 }); + expect(adapter.url).toHaveBeenCalledTimes(1); }); }); describe('Other Functions', () => { const mockFile = Buffer.from('This is a test file content'); - const mockOptions: WriteFileOptions = { + const writeOptions: WriteFileOptions = { filename: 'my-file-name', contentType: 'application/pdf', }; @@ -192,22 +198,21 @@ describe('Storages Base Service', () => { beforeEach(async () => { jest.clearAllMocks(); - const gcsMockInstance = new mockAdapter({}); - - Object.defineProperty(gcsMockInstance, 'constructor', { - value: { name: 'GCSAdapter' }, - }); + const gcsAdapter = new MockGCSAdapter(); const module: TestingModule = await Test.createTestingModule({ providers: [ StorageService, { provide: STORAGE_ADAPTER, - useValue: gcsMockInstance, + useValue: gcsAdapter, }, { provide: STORAGE_MODULE_OPTIONS, - useValue: mockOptions, + useValue: { + adapter: MockGCSAdapter as Type, + config: {}, + }, }, ], }).compile(); @@ -217,16 +222,16 @@ describe('Storages Base Service', () => { }); it('should write file', async () => { - await service.write(mockFile, mockOptions); + await service.write(mockFile, writeOptions); - expect(adapter.write).toHaveBeenCalledWith(mockFile, mockOptions); + expect(adapter.write).toHaveBeenCalledWith(mockFile, writeOptions); expect(adapter.write).toHaveBeenCalledTimes(1); }); it('should batch write files', async () => { - await service.batchWrite([mockFile], [mockOptions]); + await service.batchWrite([mockFile], [writeOptions]); - expect(adapter.batchWrite).toHaveBeenCalledWith([mockFile], [mockOptions]); + expect(adapter.batchWrite).toHaveBeenCalledWith([mockFile], [writeOptions]); expect(adapter.batchWrite).toHaveBeenCalledTimes(1); }); diff --git a/packages/storages-base-nestjs-module/package.json b/packages/storages-base-nestjs-module/package.json index da0d672a..5d029029 100644 --- a/packages/storages-base-nestjs-module/package.json +++ b/packages/storages-base-nestjs-module/package.json @@ -20,7 +20,9 @@ "url": "https://github.com/Rytass/Utils/issues" }, "dependencies": { - "@rytass/storages": "^0.2.4", + "@rytass/storages": "^0.2.4" + }, + "optionalDependencies": { "@rytass/storages-adapter-azure-blob": "^0.1.6", "@rytass/storages-adapter-gcs": "^0.2.7", "@rytass/storages-adapter-local": "0.2.7", diff --git a/packages/storages-base-nestjs-module/src/services/storages-base.service.ts b/packages/storages-base-nestjs-module/src/services/storages-base.service.ts index 2708f1d0..a4da6fab 100644 --- a/packages/storages-base-nestjs-module/src/services/storages-base.service.ts +++ b/packages/storages-base-nestjs-module/src/services/storages-base.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable, Logger, Type } from '@nestjs/common'; +import { Inject, Injectable, Type } from '@nestjs/common'; import { STORAGE_ADAPTER, STORAGE_MODULE_OPTIONS } from '../typings/storages-base-module-providers'; import type { IStorageAdapter, @@ -7,19 +7,22 @@ import type { } from '../typings/storage-base-module-options.interface'; import { InputFile, StorageFile, WriteFileOptions } from '@rytass/storages'; +type ParametersOfUrl = + NonNullable extends (...params: infer P) => unknown ? P : never; + +type ReturnTypeOfUrl = + NonNullable extends (...params: unknown[]) => infer R ? R : never; + @Injectable() -export class StorageService { - private readonly logger = new Logger(StorageService.name); +export class StorageService { private readonly _commonOptions: StorageModuleCommonOptions; constructor( @Inject(STORAGE_ADAPTER) - private readonly _adapter: IStorageAdapter, + private readonly _adapter: A, @Inject(STORAGE_MODULE_OPTIONS) - private readonly _options: StorageBaseModuleOptions>, + private readonly _options: StorageBaseModuleOptions>, ) { - this.logger.log(`Storage adapter: ${this._adapter.constructor.name}`); - const { commonOptions = {} } = this._options; this._commonOptions = { @@ -30,42 +33,17 @@ export class StorageService { }; } - get commonOptions(): StorageModuleCommonOptions { - return this._commonOptions; - } - - async url(key: string): Promise; - async url(key: string, expires: number): Promise; - async url(key: string, options?: unknown): Promise; - - async url(key: string, params?: number | unknown): Promise { - const adapterName = this._adapter.constructor.name; - - if (adapterName === 'LocalAdapter') { - throw new Error('LocalStorage does not support URL generation'); + url(...args: ParametersOfUrl): ReturnTypeOfUrl { + if (!this._adapter.url || typeof this._adapter.url !== 'function') { + throw new Error('This storage adapter does not support URL generation'); } - if (adapterName === 'AzureBlobAdapter' || adapterName === 'GCSAdapter') { - const expires = params as number; - - type UrlWithExpires = { url: (k: string, e: number) => Promise }; - const adapter = this._adapter as unknown as UrlWithExpires; - - return adapter.url(key, expires); - } else if (adapterName === 'S3Adapter') { - type UrlNoOptions = { url: (k: string) => Promise }; - const adapter = this._adapter as unknown as UrlNoOptions; - - return adapter.url(key); - } else if (adapterName === 'R2Adapter') { - type R2Like = { url: (k: string, o?: unknown) => Promise }; - const r2 = this._adapter as unknown as R2Like; - const options = params as unknown; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return this._adapter.url.apply(this._adapter, args as any) as ReturnTypeOfUrl; + } - return r2.url(key, options); - } else { - throw new Error('Unknown storage adapter'); - } + get commonOptions(): StorageModuleCommonOptions { + return this._commonOptions; } write(file: InputFile, options?: WriteFileOptions): Promise { diff --git a/packages/storages-base-nestjs-module/src/storages-base.module.ts b/packages/storages-base-nestjs-module/src/storages-base.module.ts index 4aadd773..f1f8352f 100644 --- a/packages/storages-base-nestjs-module/src/storages-base.module.ts +++ b/packages/storages-base-nestjs-module/src/storages-base.module.ts @@ -19,14 +19,14 @@ export class StorageBaseModule { const adapterProvider = { provide: STORAGE_ADAPTER, - useFactory: (): IStorageAdapter => { + useFactory: (): InstanceType => { const AdapterClass = options.adapter; if (!AdapterClass) { throw new Error('No storage adapter class was provided in forRoot!'); } - return new AdapterClass(options.config); + return new AdapterClass(options.config) as InstanceType; }, inject: [STORAGE_MODULE_OPTIONS], }; @@ -40,14 +40,14 @@ export class StorageBaseModule { static forRootAsync>(options: StorageBaseModuleAsyncOptions): DynamicModule { const asyncAdapterProviders: Provider = { provide: STORAGE_ADAPTER, - useFactory: (options: StorageBaseModuleOptions): IStorageAdapter => { + useFactory: (options: StorageBaseModuleOptions): InstanceType => { const AdapterClass = options.adapter; if (!AdapterClass) { throw new Error('No storage adapter class was provided in forRootAsync!'); } - return new AdapterClass(options.config); + return new AdapterClass(options.config) as InstanceType; }, inject: [STORAGE_MODULE_OPTIONS], }; diff --git a/packages/storages-base-nestjs-module/src/typings/storage-base-module-options.interface.ts b/packages/storages-base-nestjs-module/src/typings/storage-base-module-options.interface.ts index 5ebaa289..6bd9830c 100644 --- a/packages/storages-base-nestjs-module/src/typings/storage-base-module-options.interface.ts +++ b/packages/storages-base-nestjs-module/src/typings/storage-base-module-options.interface.ts @@ -1,18 +1,6 @@ import { InjectionToken, ModuleMetadata, OptionalFactoryDependency, Type } from '@nestjs/common'; -import { StorageAzureBlobService } from '@rytass/storages-adapter-azure-blob'; -import { StorageGCSService } from '@rytass/storages-adapter-gcs'; -import { LocalStorage } from '@rytass/storages-adapter-local'; -import { StorageR2Service } from '@rytass/storages-adapter-r2'; -import { StorageS3Service } from '@rytass/storages-adapter-s3'; import { InputFile, StorageFile, WriteFileOptions } from 'storages/lib'; -export type StorageAdapter = - | LocalStorage - | StorageAzureBlobService - | StorageGCSService - | StorageR2Service - | StorageS3Service; - export interface StorageBaseModuleOptions> { adapter: A; config: ConstructorParameters[0]; @@ -44,7 +32,8 @@ export interface IStorageAdapterUrlOptions { } export interface IStorageAdapter { - url?(key?: string, options?: IStorageAdapterUrlOptions): Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + url?(key: string, ...args: any[]): Promise; write(file: InputFile, options?: WriteFileOptions): Promise; remove(key: string): Promise; isExists(key: string): Promise; diff --git a/packages/storages-base-nestjs-module/src/wrappers/azure-blob-wrapper.ts b/packages/storages-base-nestjs-module/src/wrappers/azure-blob-wrapper.ts deleted file mode 100644 index f1636028..00000000 --- a/packages/storages-base-nestjs-module/src/wrappers/azure-blob-wrapper.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { IStorageAdapter, IStorageAdapterUrlOptions } from '../typings/storage-base-module-options.interface'; -import { InputFile, WriteFileOptions, StorageFile } from '@rytass/storages'; -import { StorageAzureBlobService } from '@rytass/storages-adapter-azure-blob'; -import type { AzureBlobOptions } from 'storages-adapter-azure-blob/lib'; - -@Injectable() -export class AzureBlobAdapter implements IStorageAdapter { - private readonly azureBlobService: StorageAzureBlobService; - - constructor(config: AzureBlobOptions) { - this.azureBlobService = new StorageAzureBlobService(config); - } - - url(key: string, options?: IStorageAdapterUrlOptions): Promise { - return this.azureBlobService.url(key, options?.expires); - } - - write(file: InputFile, options?: WriteFileOptions): Promise { - return this.azureBlobService.write(file, options); - } - - batchWrite(files: InputFile[]): Promise { - return this.azureBlobService.batchWrite(files); - } - - remove(key: string): Promise { - return this.azureBlobService.remove(key); - } - - isExists(key: string): Promise { - return this.azureBlobService.isExists(key); - } -} diff --git a/packages/storages-base-nestjs-module/src/wrappers/gcs-wrapper.ts b/packages/storages-base-nestjs-module/src/wrappers/gcs-wrapper.ts deleted file mode 100644 index abbc43bd..00000000 --- a/packages/storages-base-nestjs-module/src/wrappers/gcs-wrapper.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { IStorageAdapter, IStorageAdapterUrlOptions } from '../typings/storage-base-module-options.interface'; -import { StorageGCSService } from '@rytass/storages-adapter-gcs'; -import type { GCSOptions } from 'storages-adapter-gcs/src/typings'; -import { InputFile, StorageFile, WriteFileOptions } from '@rytass/storages'; - -@Injectable() -export class GCSAdapter implements IStorageAdapter { - private readonly gcsService: StorageGCSService; - - constructor(config: GCSOptions) { - this.gcsService = new StorageGCSService(config); - } - - url(key: string, options?: IStorageAdapterUrlOptions): Promise { - return this.gcsService.url(key, options?.expires); - } - - write(file: InputFile, options?: WriteFileOptions): Promise { - return this.gcsService.write(file, options); - } - - batchWrite(files: InputFile[]): Promise { - return this.gcsService.batchWrite(files); - } - - remove(key: string): Promise { - return this.gcsService.remove(key); - } - - isExists(key: string): Promise { - return this.gcsService.isExists(key); - } -} diff --git a/packages/storages-base-nestjs-module/src/wrappers/local-wrapper.ts b/packages/storages-base-nestjs-module/src/wrappers/local-wrapper.ts deleted file mode 100644 index 520e5571..00000000 --- a/packages/storages-base-nestjs-module/src/wrappers/local-wrapper.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { IStorageAdapter } from '../typings/storage-base-module-options.interface'; -import { InputFile, WriteFileOptions, StorageFile } from '@rytass/storages'; -import { LocalStorage } from '@rytass/storages-adapter-local'; -import type { StorageLocalOptions } from 'storages-adapter-local/lib'; - -@Injectable() -export class LocalAdapter implements IStorageAdapter { - private readonly localStorage: LocalStorage; - - constructor(options: StorageLocalOptions) { - this.localStorage = new LocalStorage(options); - } - - write(file: InputFile, options?: WriteFileOptions): Promise { - return this.localStorage.write(file, options); - } - - batchWrite(files: InputFile[]): Promise { - return this.localStorage.batchWrite(files); - } - - remove(key: string): Promise { - return this.localStorage.remove(key); - } - - isExists(key: string): Promise { - return this.localStorage.isExists(key); - } -} diff --git a/packages/storages-base-nestjs-module/src/wrappers/r2-wrapper.ts b/packages/storages-base-nestjs-module/src/wrappers/r2-wrapper.ts deleted file mode 100644 index 592a5cf3..00000000 --- a/packages/storages-base-nestjs-module/src/wrappers/r2-wrapper.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { IStorageAdapter, IStorageAdapterUrlOptions } from '../typings/storage-base-module-options.interface'; -import { InputFile, WriteFileOptions, StorageFile } from '@rytass/storages'; -import { StorageR2Service } from '@rytass/storages-adapter-r2/src'; -import type { StorageR2Options } from 'storages-adapter-r2/src/typings'; - -@Injectable() -export class R2Adapter implements IStorageAdapter { - private readonly r2Service: StorageR2Service; - - constructor(config: StorageR2Options) { - this.r2Service = new StorageR2Service(config); - } - - url(key: string, options: IStorageAdapterUrlOptions): Promise { - return this.r2Service.url(key, options); - } - - write(file: InputFile, options?: WriteFileOptions): Promise { - return this.r2Service.write(file, options); - } - - batchWrite(files: InputFile[]): Promise { - return this.r2Service.batchWrite(files); - } - - remove(key: string): Promise { - return this.r2Service.remove(key); - } - - isExists(key: string): Promise { - return this.r2Service.isExists(key); - } -} diff --git a/packages/storages-base-nestjs-module/src/wrappers/s3-wrapper.ts b/packages/storages-base-nestjs-module/src/wrappers/s3-wrapper.ts deleted file mode 100644 index f95a7266..00000000 --- a/packages/storages-base-nestjs-module/src/wrappers/s3-wrapper.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { IStorageAdapter } from '../typings/storage-base-module-options.interface'; -import { StorageS3Service } from '@rytass/storages-adapter-s3/src'; -import { InputFile, WriteFileOptions, StorageFile } from '@rytass/storages'; -import type { StorageS3Options } from 'storages-adapter-s3/src/typings'; - -@Injectable() -export class S3Adapter implements IStorageAdapter { - private readonly s3Service: StorageS3Service; - - constructor(config: StorageS3Options) { - this.s3Service = new StorageS3Service(config); - } - - url(key: string): Promise { - return this.s3Service.url(key); - } - - write(file: InputFile, options?: WriteFileOptions): Promise { - return this.s3Service.write(file, options); - } - - batchWrite(files: InputFile[]): Promise { - return this.s3Service.batchWrite(files); - } - - remove(key: string): Promise { - return this.s3Service.remove(key); - } - - isExists(key: string): Promise { - return this.s3Service.isExists(key); - } -} From 4fe5a8897a4d60cf2c2d2951f25f59313ba754ac Mon Sep 17 00:00:00 2001 From: geoffreyfarel Date: Tue, 11 Nov 2025 12:23:33 +0800 Subject: [PATCH 12/12] chore: run yarn install --- yarn.lock | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/yarn.lock b/yarn.lock index 91d37e62..40e1107f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6875,6 +6875,17 @@ __metadata: "@rytass/storages-adapter-local": "npm:0.2.7" "@rytass/storages-adapter-r2": "npm:^0.4.9" "@rytass/storages-adapter-s3": "npm:^0.3.7" + dependenciesMeta: + "@rytass/storages-adapter-azure-blob": + optional: true + "@rytass/storages-adapter-gcs": + optional: true + "@rytass/storages-adapter-local": + optional: true + "@rytass/storages-adapter-r2": + optional: true + "@rytass/storages-adapter-s3": + optional: true languageName: unknown linkType: soft