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/README.md b/packages/storages-base-nestjs-module/README.md new file mode 100644 index 00000000..da24eaa8 --- /dev/null +++ b/packages/storages-base-nestjs-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/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..ffaa82bb --- /dev/null +++ b/packages/storages-base-nestjs-module/__tests__/storages-base-module.spec.ts @@ -0,0 +1,224 @@ +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 { StorageService } from '@rytass/storages-base-nestjs-module'; +import { StorageGCSService } from '@rytass/storages-adapter-gcs'; + +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(), +})); + +describe('Storages Base Nestjs Module', () => { + @Injectable() + class mockAdapter implements IStorageAdapter { + 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) {} + } + + 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('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, + }); + }); + + 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', () => { + @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/__tests__/storages-base.service.spec.ts b/packages/storages-base-nestjs-module/__tests__/storages-base.service.spec.ts new file mode 100644 index 00000000..c4c7409c --- /dev/null +++ b/packages/storages-base-nestjs-module/__tests__/storages-base.service.spec.ts @@ -0,0 +1,252 @@ +import { StorageService } from '../src/services/storages-base.service'; +import { Test, TestingModule } from '@nestjs/testing'; +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'; + +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[], options?: WriteFileOptions[]) => Promise>; + remove: jest.Mock<(key: string) => Promise>; + isExists: jest.Mock<(key: string) => Promise>; + + 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.isExists = jest.fn(); + } +} + +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 does not support URL generation', async () => { + const localAdapter = new MockLocalAdapter(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + StorageService, + { + provide: STORAGE_ADAPTER, + useValue: localAdapter, + }, + { + provide: STORAGE_MODULE_OPTIONS, + useValue: { + adapter: MockLocalAdapter as Type, + config: {}, + }, + }, + ], + }).compile(); + + service = module.get(StorageService); + + expect(() => service.url('my-key')).toThrow('This storage adapter does not support URL generation'); + }); + + 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: gcsAdapter, + }, + { + provide: STORAGE_MODULE_OPTIONS, + useValue: { + adapter: MockGCSAdapter as Type, + config: {}, + }, + }, + ], + }).compile(); + + service = module.get(StorageService); + adapter = module.get(STORAGE_ADAPTER); + + await service.url('my-key', 3600); + + expect(adapter.url).toHaveBeenCalledWith('my-key', 3600); + expect(adapter.url).toHaveBeenCalledTimes(1); + }); + + 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: s3Adapter, + }, + { + provide: STORAGE_MODULE_OPTIONS, + useValue: { + adapter: MockS3Adapter as Type, + config: {}, + }, + }, + ], + }).compile(); + + service = module.get(StorageService); + adapter = module.get(STORAGE_ADAPTER); + + // 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 parameter - R2-like adapter', async () => { + const r2Adapter = new MockR2Adapter(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + StorageService, + { + provide: STORAGE_ADAPTER, + useValue: r2Adapter, + }, + { + provide: STORAGE_MODULE_OPTIONS, + useValue: { + adapter: MockR2Adapter as Type, + config: {}, + }, + }, + ], + }).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 }); + expect(adapter.url).toHaveBeenCalledTimes(1); + }); + }); + + describe('Other Functions', () => { + const mockFile = Buffer.from('This is a test file content'); + + const writeOptions: WriteFileOptions = { + filename: 'my-file-name', + contentType: 'application/pdf', + }; + + beforeEach(async () => { + jest.clearAllMocks(); + + const gcsAdapter = new MockGCSAdapter(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + StorageService, + { + provide: STORAGE_ADAPTER, + useValue: gcsAdapter, + }, + { + provide: STORAGE_MODULE_OPTIONS, + useValue: { + adapter: MockGCSAdapter as Type, + config: {}, + }, + }, + ], + }).compile(); + + service = module.get(StorageService); + adapter = module.get(STORAGE_ADAPTER); + }); + + it('should write file', async () => { + await service.write(mockFile, writeOptions); + + expect(adapter.write).toHaveBeenCalledWith(mockFile, writeOptions); + expect(adapter.write).toHaveBeenCalledTimes(1); + }); + + it('should batch write files', async () => { + await service.batchWrite([mockFile], [writeOptions]); + + expect(adapter.batchWrite).toHaveBeenCalledWith([mockFile], [writeOptions]); + 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/package.json b/packages/storages-base-nestjs-module/package.json new file mode 100644 index 00000000..5d029029 --- /dev/null +++ b/packages/storages-base-nestjs-module/package.json @@ -0,0 +1,32 @@ +{ + "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" + }, + "optionalDependencies": { + "@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/project.json b/packages/storages-base-nestjs-module/project.json new file mode 100644 index 00000000..4d2cb371 --- /dev/null +++ b/packages/storages-base-nestjs-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/storages-base-nestjs-module/src/index.ts b/packages/storages-base-nestjs-module/src/index.ts new file mode 100644 index 00000000..6076219c --- /dev/null +++ b/packages/storages-base-nestjs-module/src/index.ts @@ -0,0 +1,3 @@ +export * from './storages-base.module'; + +export * from './services/storages-base.service'; 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 new file mode 100644 index 00000000..a4da6fab --- /dev/null +++ b/packages/storages-base-nestjs-module/src/services/storages-base.service.ts @@ -0,0 +1,64 @@ +import { Inject, Injectable, Type } from '@nestjs/common'; +import { STORAGE_ADAPTER, STORAGE_MODULE_OPTIONS } from '../typings/storages-base-module-providers'; +import type { + IStorageAdapter, + StorageBaseModuleOptions, + StorageModuleCommonOptions, +} 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 _commonOptions: StorageModuleCommonOptions; + + constructor( + @Inject(STORAGE_ADAPTER) + private readonly _adapter: A, + @Inject(STORAGE_MODULE_OPTIONS) + private readonly _options: StorageBaseModuleOptions>, + ) { + const { commonOptions = {} } = this._options; + + this._commonOptions = { + formDataFieldName: commonOptions.formDataFieldName ?? 'files', + allowMultiple: commonOptions.allowMultiple ?? true, + MaxFileSizeInBytes: commonOptions.MaxFileSizeInBytes ?? 10 * 1024 * 1024, + defaultPublic: commonOptions.defaultPublic ?? false, + }; + } + + url(...args: ParametersOfUrl): ReturnTypeOfUrl { + if (!this._adapter.url || typeof this._adapter.url !== 'function') { + throw new Error('This storage adapter does not support URL generation'); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return this._adapter.url.apply(this._adapter, args as any) as ReturnTypeOfUrl; + } + + get commonOptions(): StorageModuleCommonOptions { + return this._commonOptions; + } + + write(file: InputFile, options?: WriteFileOptions): Promise { + return this._adapter.write(file, options); + } + + 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/storages-base-nestjs-module/src/storages-base.module.ts b/packages/storages-base-nestjs-module/src/storages-base.module.ts new file mode 100644 index 00000000..f1f8352f --- /dev/null +++ b/packages/storages-base-nestjs-module/src/storages-base.module.ts @@ -0,0 +1,100 @@ +import { DynamicModule, Global, Module, Provider, Type } from '@nestjs/common'; +import { + IStorageAdapter, + StorageBaseModuleAsyncOptions, + StorageBaseModuleOptions, + StorageBaseModuleOptionsFactory, +} from './typings/storage-base-module-options.interface'; +import { STORAGE_ADAPTER, STORAGE_MODULE_OPTIONS } from './typings/storages-base-module-providers'; +import { StorageService } from './services/storages-base.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: (): InstanceType => { + const AdapterClass = options.adapter; + + if (!AdapterClass) { + throw new Error('No storage adapter class was provided in forRoot!'); + } + + return new AdapterClass(options.config) as InstanceType; + }, + inject: [STORAGE_MODULE_OPTIONS], + }; + + return { + module: StorageBaseModule, + providers: [optionsProvider, adapterProvider, StorageService], + exports: [StorageService], + }; + } + static forRootAsync>(options: StorageBaseModuleAsyncOptions): DynamicModule { + const asyncAdapterProviders: Provider = { + provide: STORAGE_ADAPTER, + 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) as InstanceType; + }, + inject: [STORAGE_MODULE_OPTIONS], + }; + + return { + module: StorageBaseModule, + imports: [...(options?.imports ?? [])], + providers: [...this.createAsyncProvider(options), asyncAdapterProviders, 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/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 new file mode 100644 index 00000000..6bd9830c --- /dev/null +++ b/packages/storages-base-nestjs-module/src/typings/storage-base-module-options.interface.ts @@ -0,0 +1,41 @@ +import { InjectionToken, ModuleMetadata, OptionalFactoryDependency, Type } from '@nestjs/common'; +import { InputFile, StorageFile, WriteFileOptions } from 'storages/lib'; + +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 IStorageAdapterUrlOptions { + expires?: number; +} + +export interface IStorageAdapter { + // 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; + batchWrite(files: InputFile[], options?: WriteFileOptions[]): Promise; +} diff --git a/packages/storages-base-nestjs-module/src/typings/storages-base-module-providers.ts b/packages/storages-base-nestjs-module/src/typings/storages-base-module-providers.ts new file mode 100644 index 00000000..5440f177 --- /dev/null +++ b/packages/storages-base-nestjs-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/storages-base-nestjs-module/tsconfig.build.json b/packages/storages-base-nestjs-module/tsconfig.build.json new file mode 100644 index 00000000..06d1953d --- /dev/null +++ b/packages/storages-base-nestjs-module/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.node.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declaration": true + }, + "exclude": ["**/*.spec.*"], + "include": ["./src"] +} 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"] } }, diff --git a/yarn.lock b/yarn.lock index 35bd45a5..40e1107f 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: @@ -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: @@ -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,30 @@ __metadata: languageName: unknown linkType: soft +"@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-nestjs-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-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 + "@rytass/storages@npm:^0.2.4, @rytass/storages@workspace:packages/storages": version: 0.0.0-use.local resolution: "@rytass/storages@workspace:packages/storages"