Skip to content

Commit

Permalink
Merge branch 'BC-7561-batch-delete-fix' of https://github.com/hpi-sch…
Browse files Browse the repository at this point in the history
  • Loading branch information
virgilchiriac committed Jan 24, 2025
2 parents e80920a + ec23c1d commit 2ec2d7d
Show file tree
Hide file tree
Showing 284 changed files with 4,195 additions and 2,368 deletions.
93 changes: 60 additions & 33 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ module.exports = {
'arrow-parens': ['error', 'always'],
'arrow-body-style': ['error', 'as-needed', { requireReturnForObjectLiteral: true }],
'no-only-tests/no-only-tests': 'error',
'max-classes-per-file': ['warn', 1],
'max-classes-per-file': 'off',
},
plugins: ['import', 'prettier', 'promise', 'no-only-tests', 'filename-rules'],
env: {
Expand Down Expand Up @@ -117,17 +117,6 @@ module.exports = {
allowSingleExtends: true,
},
],
'@typescript-eslint/no-restricted-imports': [
'warn',
{
patterns: [
{
group: ['@infra/*/*', '@modules/*/*', '!@modules/*/testing', '!*.module'],
message: 'Do not deep import from a module',
},
],
},
],
},
overrides: [
{
Expand All @@ -142,7 +131,6 @@ module.exports = {
'jest/prefer-spy-on': 'warn',
'jest/unbound-method': 'error',
'@typescript-eslint/explicit-function-return-type': 'off',
'max-classes-per-file': 'off',
'@typescript-eslint/explicit-member-accessibility': 'off',
},
},
Expand All @@ -154,8 +142,8 @@ module.exports = {
{
patterns: [
{
group: ['@apps/**', '@infra/**', '@shared/**', 'apps/server/src/migrations/**'],
message: 'apps/server/src/migrations may NOT import from @apps, @infra, @shared, or migrations',
group: ['**/apps/**', '@infra/**', '@shared/**', '**/migrations/**'],
message: 'apps/server/src/migrations may NOT import from apps, @infra, @shared, or migrations',
},
],
},
Expand All @@ -172,8 +160,12 @@ module.exports = {
{
patterns: [
{
group: ['@apps/**', '@infra/**', '@shared/**', 'apps/server/src/migrations/**'],
message: 'apps-modules may NOT import from @apps, @infra, @shared, or migrations',
group: ['**/apps/**', '@infra/**', '@shared/**', '**/migrations/**'],
message: 'apps-modules may NOT import from apps, @infra, @shared, or migrations',
},
{
group: ['@infra/*/*', '@modules/*/*', '!@modules/*/testing', '!*.module'],
message: 'Do not deep import from a module',
},
],
},
Expand All @@ -188,8 +180,12 @@ module.exports = {
{
patterns: [
{
group: ['@apps/**', '@core/**', '@infra/**', '@modules/**'],
message: 'core-modules may NOT import from @apps, @core, @infra, or @modules',
group: ['**/apps/**', '@core/**', '@infra/**', '@modules/**'],
message: 'core-modules may NOT import from apps, @core, @infra, or @modules',
},
{
group: ['@infra/*/*', '@modules/*/*', '!@modules/*/testing', '!*.module'],
message: 'Do not deep import from a module',
},
],
},
Expand All @@ -204,8 +200,12 @@ module.exports = {
{
patterns: [
{
group: ['@apps/**', '@core/**', '@modules/**', 'apps/server/src/migrations/**'],
message: 'infra-modules may NOT import from @apps, @core, @modules, or migrations',
group: ['**/apps/**', '@core/**', '@modules/**', '**/migrations/**'],
message: 'infra-modules may NOT import from apps, @core, @modules, or migrations',
},
{
group: ['@infra/*/*', '@modules/*/*', '!@modules/*/testing', '!*.module'],
message: 'Do not deep import from a module',
},
],
},
Expand All @@ -220,8 +220,12 @@ module.exports = {
{
patterns: [
{
group: ['@apps/**'],
message: 'modules-modules may NOT import from @apps',
group: ['**/apps/**'],
message: 'modules-modules may NOT import from apps',
},
{
group: ['@infra/*/*', '@modules/*/*', '!@modules/*/testing', '!*.module'],
message: 'Do not deep import from a module',
},
],
},
Expand All @@ -236,16 +240,28 @@ module.exports = {
{
patterns: [
{
group: [
'@apps/**',
'@core/**',
'@infra/**',
'@modules/**',
'@shared/**',
'apps/server/src/migrations/**',
],
message:
'shared modules may NOT import from @apps, @core, @infra, @modules, @shared, or migrations',
group: ['**/apps/**', '@core/**', '@infra/**', '@modules/**', '@shared/**', '**/migrations/**'],
message: 'shared modules may NOT import from apps, @core, @infra, @modules, @shared, or migrations',
},
{
group: ['@infra/*/*', '@modules/*/*', '!@modules/*/testing', '!*.module'],
message: 'Do not deep import from a module',
},
],
},
],
},
},
{
files: ['apps/server/src/testing/**/*.ts'],
rules: {
'@typescript-eslint/no-restricted-imports': [
'error',
{
patterns: [
{
group: ['@modules/*', '!@modules/account'],
message: 'testing may NOT import from @modules',
},
],
},
Expand All @@ -270,6 +286,17 @@ module.exports = {
],
},
},
{
files: [
'apps/server/src/**/*.repo.ts',
'apps/server/src/**/*.service.ts',
'apps/server/src/**/*.controller.ts',
'apps/server/src/**/*.uc.ts',
],
rules: {
'max-classes-per-file': ['warn', 1],
},
},
],
},
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { faker } from '@faker-js/faker';
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { REQUEST } from '@nestjs/core';
import { Test, TestingModule } from '@nestjs/testing';
import { ErrorLogger, Logger } from '@core/logger';
import type { Request } from 'express';
import { from, throwError } from 'rxjs';
import { axiosResponseFactory } from '@testing/factory/axios-response.factory';
import { FilesStorageClientAdapter } from './files-storage-client.adapter';
import { FileApi } from './generated';

describe(FilesStorageClientAdapter.name, () => {
let module: TestingModule;
let sut: FilesStorageClientAdapter;
let httpServiceMock: DeepMocked<HttpService>;
let errorLoggerMock: DeepMocked<ErrorLogger>;
let configServiceMock: DeepMocked<ConfigService>;

beforeAll(async () => {
module = await Test.createTestingModule({
providers: [
FilesStorageClientAdapter,
{
provide: FileApi,
useValue: createMock<FileApi>(),
},
{
provide: Logger,
useValue: createMock<Logger>(),
},
{
provide: ErrorLogger,
useValue: createMock<ErrorLogger>(),
},
{
provide: HttpService,
useValue: createMock<HttpService>(),
},
{
provide: ConfigService,
useValue: createMock<ConfigService>(),
},
{
provide: REQUEST,
useValue: createMock<Request>({
headers: {
authorization: `Bearer ${faker.string.alphanumeric(42)}`,
},
}),
},
],
}).compile();

sut = module.get(FilesStorageClientAdapter);
httpServiceMock = module.get(HttpService);
errorLoggerMock = module.get(ErrorLogger);
configServiceMock = module.get(ConfigService);
});

afterAll(async () => {
await module.close();
});

beforeEach(() => {
jest.clearAllMocks();
});

it('should be defined', () => {
expect(sut).toBeDefined();
});

describe('download', () => {
describe('when download succeeds', () => {
const setup = () => {
const fileRecordId = faker.string.uuid();
const fileName = faker.system.fileName();
const observable = from([axiosResponseFactory.build({ data: Buffer.from('') })]);

httpServiceMock.get.mockReturnValue(observable);
configServiceMock.getOrThrow.mockReturnValue(faker.internet.url());

return {
fileRecordId,
fileName,
};
};

it('should return the response buffer', async () => {
const { fileRecordId, fileName } = setup();

const result = await sut.download(fileRecordId, fileName);

expect(result).toEqual(Buffer.from(''));
expect(httpServiceMock.get).toBeCalledWith(expect.any(String), {
responseType: 'arraybuffer',
headers: {
Authorization: expect.any(String),
},
});
});
});

describe('when download fails', () => {
const setup = () => {
const fileRecordId = faker.string.uuid();
const fileName = faker.system.fileName();
const observable = throwError(() => new Error('error'));

httpServiceMock.get.mockReturnValue(observable);

return {
fileRecordId,
fileName,
};
};

it('should return null', async () => {
const { fileRecordId, fileName } = setup();

const result = await sut.download(fileRecordId, fileName);

expect(result).toBeNull();
expect(errorLoggerMock.error).toBeCalled();
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { HttpService } from '@nestjs/axios';
import { Inject, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { REQUEST } from '@nestjs/core';
import { JwtExtractor } from '@shared/common/utils/jwt';
import { AxiosErrorLoggable } from '@core/error/loggable';
import { ErrorLogger, Logger } from '@core/logger';
import { AxiosError } from 'axios';
import type { Request } from 'express';
import { lastValueFrom } from 'rxjs';
import { FilesStorageClientConfig } from './files-storage-client.config';
import { FileApi } from './generated';

@Injectable()
export class FilesStorageClientAdapter {
constructor(
private readonly api: FileApi,
private readonly logger: Logger,
private readonly errorLogger: ErrorLogger,
// these should be removed when the generated client supports downloading files as arraybuffer
private readonly httpService: HttpService,
private readonly configService: ConfigService<FilesStorageClientConfig, true>,
@Inject(REQUEST) private readonly req: Request
) {
this.logger.setContext(FilesStorageClientAdapter.name);
}

public async download(fileRecordId: string, fileName: string): Promise<Buffer | null> {
try {
// INFO: we need to download the file from the files storage service without using the generated client,
// because the generated client does not support downloading files as arraybuffer. Otherwise files with
// binary content would be corrupted like pdfs, zip files, etc. Setting the responseType to 'arraybuffer'
// will not work with the generated client.
// const response = await this.api.download(fileRecordId, fileName, undefined, {
// responseType: 'arraybuffer',
// });
const token = JwtExtractor.extractJwtFromRequest(this.req);
const url = new URL(
`${this.configService.getOrThrow<string>(
'FILES_STORAGE__SERVICE_BASE_URL'
)}/api/v3/file/download/${fileRecordId}/${fileName}`
);
const observable = this.httpService.get(url.toString(), {
responseType: 'arraybuffer',
headers: {
Authorization: `Bearer ${token}`,
},
});
const response = await lastValueFrom(observable);
const data = Buffer.isBuffer(response.data) ? response.data : null;

return data;
} catch (error: unknown) {
this.errorLogger.error(new AxiosErrorLoggable(error as AxiosError, 'FilesStorageClientAdapter.download'));

return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface FilesStorageClientConfig {
FILES_STORAGE__SERVICE_BASE_URL: string;
}
Loading

0 comments on commit 2ec2d7d

Please sign in to comment.