Skip to content

Commit

Permalink
Add TypeGuard operations and use it to remove eslint exceptions.
Browse files Browse the repository at this point in the history
  • Loading branch information
CeEv committed Jul 25, 2024
1 parent 74a104b commit d39c7f9
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export class EtherpadClientAdapter {
const response = await this.tryGetPadsOfAuthor(authorId);
const pads = this.handleEtherpadResponse<ListPadsUsingGET200Response>(response, { authorId });

if (!TypeGuard.isObject(pads)) {
if (!TypeGuard.isDefinedObject(pads)) {
throw new InternalServerErrorException('Etherpad listPadsOfAuthor response is not an object');
}

Expand Down Expand Up @@ -280,7 +280,7 @@ export class EtherpadClientAdapter {
const response = await this.tryGetAuthorsOfPad(padId);
const authors = this.handleEtherpadResponse<ListAuthorsOfPadUsingGET200Response>(response, { padId });

if (!TypeGuard.isObject(authors)) {
if (!TypeGuard.isDefinedObject(authors)) {
throw new InternalServerErrorException('Etherpad listAuthorsOfPad response is not an object');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class EtherpadResponseMapper {

static mapEtherpadSessionsToSessions(etherpadSessions: unknown): Session[] {
try {
const isObject = TypeGuard.isObject(etherpadSessions);
const isObject = TypeGuard.isDefinedObject(etherpadSessions);
if (!isObject) return [];

const sessions = Object.entries(etherpadSessions)
Expand All @@ -83,7 +83,7 @@ export class EtherpadResponseMapper {

static mapEtherpadSessionToSession([etherpadId, etherpadSession]: [string, unknown | undefined]): Session {
if (
!TypeGuard.isObject(etherpadSession) ||
!TypeGuard.isDefinedObject(etherpadSession) ||
!('groupID' in etherpadSession) ||
!('authorID' in etherpadSession) ||
!('validUntil' in etherpadSession)
Expand Down
35 changes: 17 additions & 18 deletions apps/server/src/infra/s3-client/s3-client.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
CopyObjectCommand,
CopyObjectCommandOutput,
CreateBucketCommand,
DeleteObjectCommandOutput,
DeleteObjectsCommand,
GetObjectCommand,
HeadObjectCommand,
Expand All @@ -15,6 +16,7 @@ import { Inject, Injectable, InternalServerErrorException, NotFoundException } f
import { ErrorUtils } from '@src/core/error/utils';
import { LegacyLogger } from '@src/core/logger';
import { Readable } from 'stream';
import { TypeGuard } from '@shared/common';
import { S3_CLIENT, S3_CONFIG } from './constants';
import { CopyFiles, File, GetFile, ListFiles, ObjectKeysRecursive, S3Config } from './interface';

Expand All @@ -31,14 +33,14 @@ export class S3ClientAdapter {
}

// is public but only used internally
public async createBucket() {
public async createBucket(): Promise<void> {
try {
this.logger.debug({ action: 'create bucket', params: { bucket: this.config.bucket } });

const req = new CreateBucketCommand({ Bucket: this.config.bucket });
await this.client.send(req);
} catch (err) {
if (err instanceof Error) {
if (TypeGuard.isError(err)) {
this.logger.error(`${err.message} "${this.config.bucket}"`);
}
throw new InternalServerErrorException(
Expand Down Expand Up @@ -70,9 +72,8 @@ export class S3ClientAdapter {
contentRange: data.ContentRange,
etag: data.ETag,
};
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (err?.Code === 'NoSuchKey') {
} catch (err: unknown) {
if (TypeGuard.getValueFromObjectKey(err, 'Code') === 'NoSuchKey') {
this.logger.warn(`Could not get file with id ${path}`);
throw new NotFoundException('NoSuchKey', ErrorUtils.createHttpExceptionOptions(err));
} else {
Expand All @@ -97,10 +98,10 @@ export class S3ClientAdapter {
});

const commandOutput = await upload.done();

return commandOutput;
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (err?.Code === 'NoSuchBucket') {
} catch (err: unknown) {
if (TypeGuard.getValueFromObjectKey(err, 'Code') === 'NoSuchBucket') {
await this.createBucket();

return await this.create(path, file);
Expand All @@ -123,10 +124,10 @@ export class S3ClientAdapter {
await this.delete(paths);

return result;
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (err?.cause?.name === 'NoSuchKey') {
} catch (err: unknown) {
if (TypeGuard.getValueFromDeepObjectKey(err, ['cause', 'name']) === 'NoSuchKey') {
this.logger.warn(`could not find one of the files for deletion with ids ${paths.join(',')}`);

return [];
}
throw new InternalServerErrorException('S3ClientAdapter:delete', ErrorUtils.createHttpExceptionOptions(err));
Expand Down Expand Up @@ -154,7 +155,7 @@ export class S3ClientAdapter {
}
}

public async copy(paths: CopyFiles[]) {
public async copy(paths: CopyFiles[]): Promise<CopyObjectCommandOutput[]> {
try {
this.logger.debug({ action: 'copy', params: { paths, bucket: this.config.bucket } });

Expand All @@ -178,7 +179,7 @@ export class S3ClientAdapter {
}
}

public async delete(paths: string[]) {
public async delete(paths: string[]): Promise<DeleteObjectCommandOutput> {
try {
this.logger.debug({ action: 'delete', params: { paths, bucket: this.config.bucket } });

Expand Down Expand Up @@ -253,16 +254,15 @@ export class S3ClientAdapter {

return headResponse;
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (err.message && err.message === 'NoSuchKey') {
if (TypeGuard.getValueFromObjectKey(err, 'message') === 'NoSuchKey') {
this.logger.warn(`could not find the file for head with id ${path}`);
throw new NotFoundException(null, ErrorUtils.createHttpExceptionOptions(err, 'NoSuchKey'));
}
throw new InternalServerErrorException(null, ErrorUtils.createHttpExceptionOptions(err, 'S3ClientAdapter:head'));
}
}

public async deleteDirectory(path: string) {
public async deleteDirectory(path: string): Promise<void> {
try {
this.logger.debug({ action: 'deleteDirectory', params: { path, bucket: this.config.bucket } });

Expand All @@ -289,10 +289,9 @@ export class S3ClientAdapter {
}

/* istanbul ignore next */
private checkStreamResponsive(stream: Readable, context: string) {
private checkStreamResponsive(stream: Readable, context: string): void {
let timer: NodeJS.Timeout;
const refreshTimeout = () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
this.logger.log(`Stream unresponsive: S3 object key ${context}`);
Expand Down
28 changes: 14 additions & 14 deletions apps/server/src/shared/common/guards/type.guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,67 +88,67 @@ describe('TypeGuard', () => {
describe('isObject', () => {
describe('when passing type of value is an object', () => {
it('should be return true', () => {
expect(TypeGuard.isObject({})).toBe(true);
expect(TypeGuard.isDefinedObject({})).toBe(true);
});

it('should be return true', () => {
expect(TypeGuard.isObject({ a: 1 })).toBe(true);
expect(TypeGuard.isDefinedObject({ a: 1 })).toBe(true);
});

it('should be return true', () => {
expect(TypeGuard.isObject({ a: { b: 1 } })).toBe(true);
expect(TypeGuard.isDefinedObject({ a: { b: 1 } })).toBe(true);
});
});

describe('when passing type of value is NOT an object', () => {
it('should be return false', () => {
expect(TypeGuard.isObject(undefined)).toBe(false);
expect(TypeGuard.isDefinedObject(undefined)).toBe(false);
});

it('should be return false', () => {
expect(TypeGuard.isObject(null)).toBe(false);
expect(TypeGuard.isDefinedObject(null)).toBe(false);
});

it('should be return false', () => {
expect(TypeGuard.isObject([])).toBe(false);
expect(TypeGuard.isDefinedObject([])).toBe(false);
});

it('should be return false', () => {
expect(TypeGuard.isObject('string')).toBe(false);
expect(TypeGuard.isDefinedObject('string')).toBe(false);
});
});
});

describe('checkObject', () => {
describe('when passing type of value is an object', () => {
it('should be return value', () => {
expect(TypeGuard.checkObject({})).toEqual({});
expect(TypeGuard.checkDefinedObject({})).toEqual({});
});

it('should be return value', () => {
expect(TypeGuard.checkObject({ a: 1 })).toEqual({ a: 1 });
expect(TypeGuard.checkDefinedObject({ a: 1 })).toEqual({ a: 1 });
});

it('should be return value', () => {
expect(TypeGuard.checkObject({ a: { b: 1 } })).toEqual({ a: { b: 1 } });
expect(TypeGuard.checkDefinedObject({ a: { b: 1 } })).toEqual({ a: { b: 1 } });
});
});

describe('when passing type of value is NOT an object', () => {
it('should be return false', () => {
expect(() => TypeGuard.checkObject(undefined)).toThrowError('Type is not an object');
expect(() => TypeGuard.checkDefinedObject(undefined)).toThrowError('Type is not an object');
});

it('should be return false', () => {
expect(() => TypeGuard.checkObject(null)).toThrowError('Type is not an object');
expect(() => TypeGuard.checkDefinedObject(null)).toThrowError('Type is not an object');
});

it('should be return false', () => {
expect(() => TypeGuard.checkObject([])).toThrowError('Type is not an object');
expect(() => TypeGuard.checkDefinedObject([])).toThrowError('Type is not an object');
});

it('should be return false', () => {
expect(() => TypeGuard.checkObject('string')).toThrowError('Type is not an object');
expect(() => TypeGuard.checkDefinedObject('string')).toThrowError('Type is not an object');
});
});
});
Expand Down
83 changes: 71 additions & 12 deletions apps/server/src/shared/common/guards/type.guard.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,22 @@
export class TypeGuard {
static isError(value: unknown): value is Error {
const isError = value instanceof Error;

return isError;
}

static isNull(value: unknown): value is null {
const isNull = value === null;

return isNull;
}

static isUndefined(value: unknown): value is undefined {
const isUndefined = value === undefined;

return isUndefined;
}

static checkNumber(value: unknown): number {
if (!TypeGuard.isNumber(value)) {
throw new Error('Type is not a number');
Expand Down Expand Up @@ -27,36 +45,77 @@ export class TypeGuard {
return isString;
}

static isArray(value: unknown): value is [] {
const isArray = Array.isArray(value);

return isArray;
}

static checkArray(value: unknown): [] {
if (!TypeGuard.isArray(value)) {
throw new Error('Type is not an array');
}

return value;
}

static isArrayWithElements(value: unknown): value is [] {
const isArrayWithElements = Array.isArray(value) && value.length > 0;
const isArrayWithElements = TypeGuard.isArray(value) && value.length > 0;

return isArrayWithElements;
}

static checkObject(value: unknown): object {
if (!TypeGuard.isObject(value)) {
throw new Error('Type is not an object');
static checkArrayWithElements(value: unknown): [] {
if (!TypeGuard.isArrayWithElements(value)) {
throw new Error('Type is not an array with elements.');
}

return value;
}

static isObject(value: unknown): value is object {
const isObject = typeof value === 'object' && !Array.isArray(value) && value !== null;
static isDefinedObject(value: unknown): value is object {
const isObject = typeof value === 'object' && !TypeGuard.isArray(value) && !TypeGuard.isNull(value);

return isObject;
}

static isNull(value: unknown): value is null {
const isNull = value === null;
static checkDefinedObject(value: unknown): object {
if (!TypeGuard.isDefinedObject(value)) {
throw new Error('Type is not an object.');
}

return isNull;
return value;
}

static isUndefined(value: unknown): value is undefined {
const isUndefined = value === undefined;
/** @return undefined if no object or key do not exists, otherwise the value of the key. */
static getValueFromObjectKey(value: unknown, key: string): unknown {
if (!TypeGuard.checkDefinedObject(value)) {
return undefined;
}

return isUndefined;
const result: unknown = (value as object)[key];

return result;
}

static getValueFromDeepObjectKey(value: unknown, keyPath: string[]): unknown {
let result: unknown = value;

keyPath.forEach((key) => {
result = TypeGuard.getValueFromObjectKey(result, key);
});

return result;
}

/** @return value of requested key in object. */
static checkKeyInObject(value: unknown, key: string): unknown {
const object = TypeGuard.checkDefinedObject(value);
if (!(key in object)) {
throw new Error(`Object has no ${key}.`);
}

return object[key];
}

static checkNotNullOrUndefined<T>(value: T | null | undefined, toThrow?: Error): T {
Expand Down

0 comments on commit d39c7f9

Please sign in to comment.