Skip to content

Commit

Permalink
backend: Separate util functions into several files and refactor code
Browse files Browse the repository at this point in the history
  • Loading branch information
somnisomni committed Nov 26, 2024
1 parent cf48286 commit 38c684a
Show file tree
Hide file tree
Showing 15 changed files with 326 additions and 236 deletions.
143 changes: 0 additions & 143 deletions projects/Backend/src/lib/common-functions.ts

This file was deleted.

26 changes: 0 additions & 26 deletions projects/Backend/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,3 @@ export interface IUploadStorage {
extensions?: Array<string>;
imageThumbnailBase64?: string;
}

export abstract class CacheMap<K, V> {
protected cache: Map<K, V> = new Map<K, V>();

abstract fetch(key: K): Promise<V>;

async get(key: K): Promise<V> {
if(!this.has(key)) {
this.cache.set(key, await this.fetch(key));
}

return this.cache.get(key) as V;
}

has(key: K): boolean {
return this.cache.has(key);
}

async testValue(key: K, valueToTest: V): Promise<boolean> {
return await this.get(key) === valueToTest;
}

invalidate(key: K): boolean {
return this.cache.delete(key);
}
}
33 changes: 33 additions & 0 deletions projects/Backend/src/lib/utils/cache-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Works like a Map, but if a key is not found, it will fetch the value from the fetch method.
*
* This is an abstract class that should be extended to implement the fetch method and use.
*
* @template K - Key type
* @template V - Value type
*/
export abstract class CacheMap<K, V> {
protected cache: Map<K, V> = new Map<K, V>();

abstract fetch(key: K): Promise<V>;

async get(key: K): Promise<V> {
if(!this.has(key)) {
this.cache.set(key, await this.fetch(key));
}

return this.cache.get(key) as V;
}

has(key: K): boolean {
return this.cache.has(key);
}

async testValue(key: K, valueToTest: V): Promise<boolean> {
return await this.get(key) === valueToTest;
}

invalidate(key: K): boolean {
return this.cache.delete(key);
}
}
188 changes: 188 additions & 0 deletions projects/Backend/src/lib/utils/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { SEQUELIZE_INTERNAL_KEYS } from "@myboothmanager/common";
import { InternalServerErrorException, BadRequestException } from "@nestjs/common";
import { type Model, fn, col, where, type ModelDefined, BaseError, type FindOptions, type CreateOptions, type InstanceDestroyOptions, type ModelAttributes } from "sequelize";
import type { Fn, Where } from "sequelize/types/utils";
import { EntityNotFoundException } from "../exceptions";

type FindOptionsAsParam<TAttr> = Partial<FindOptions<TAttr>> & { includeSequelizeInternalKeys?: boolean } & { attributes?: { exclude?: string[] } };
type CreateOptionsAsParam<TAttr> = Partial<CreateOptions<TAttr>> & { includeSequelizeInternalKeys?: boolean };

/* === Query builder functions === */
/**
* Build a `JSON_CONTAINS` query
*
* @template TModel - Model type
* @param column - Name of column that contains JSON
* @param searchValue - Value to search
* @returns {Fn} - Sequelize function
*/
export function jsonContains<TModel extends Model>(column: keyof TModel, searchValue: string | number): Fn {
return fn("JSON_CONTAINS", col(column as string), searchValue.toString());
}

/**
* Build a `BINARY` query for case-sensitive string comparison
*
* @template TModel - Model type
* @param column - Name of column that contains string
* @param searchValue - Value to search
* @returns {Where} - Sequelize where clause
*/
export function stringCompareCaseSensitive<TModel extends Model>(column: keyof TModel, searchValue: string): Where {
return where(fn("BINARY", col(column as string)), searchValue);
}

/* === Execution functions === */
/**
* Wrapper function for handling Sequelize actions
*
* This is for convenient error handling and to reduce code duplication. This should not be exported and used directly.
*/
async function actionWrapper<TResult extends (Model | Model[])>(action: () => Promise<TResult | null>): Promise<TResult> {
let target: TResult | null = null;

try {
target = await action();
} catch(error) {
console.error(error);

if(error instanceof BaseError) {
// DB error
throw new InternalServerErrorException(`DB error: ${error.name}`);
} else {
// Unknown error
throw new BadRequestException(`Unknown error: ${error instanceof Error ? error.name : "Unknwon"}`);
}
}

if(!target) throw new EntityNotFoundException();
return target;
}

/**
* Find one record by primary key
*
* @template TModel - Model type
* @param model - Model class
* @param pk - Primary key to search
* @param options - Additional options to be passed to Sequelize
* @returns {Promise<TModel>} - Found record
*/
export async function findOneByPk<TModel extends Model>(
model: { new (): TModel },
pk: number,
options: Omit<FindOptionsAsParam<TModel>, "where"> = { includeSequelizeInternalKeys: false }): Promise<TModel> {
return await actionWrapper(async () => await (model as unknown as ModelDefined<any, any>).findByPk(pk, {
...options,

attributes: {
...options.attributes,
exclude: [
...(options.attributes?.exclude ?? []),
...(options.includeSequelizeInternalKeys ? [] : SEQUELIZE_INTERNAL_KEYS),
],
},
}) as TModel);
}

/**
* Find all records matching the condition
*
* @template TModel - Model type
* @param model - Model class
* @param options - Additional options (including `where` clause) to be passed to Sequelize
* @returns {Promise<TModel[]>} - Found records
*/
export async function findAll<TModel extends Model>(
model: { new (): TModel },
options: FindOptionsAsParam<TModel> = { includeSequelizeInternalKeys: false }): Promise<TModel[]> {
return await actionWrapper(async () => await (model as unknown as ModelDefined<any, any>).findAll({
...options,

attributes: {
...options.attributes,
exclude: [
...(options.attributes?.exclude ?? []),
...(options.includeSequelizeInternalKeys ? [] : SEQUELIZE_INTERNAL_KEYS),
],
},
}) as TModel[]);
}

/**
* Create a new record
*
* @template TModel - Model type
* @param model - Model class
* @param values - Data to be inserted
* @param options - Additional options to be passed to Sequelize
* @returns {Promise<TModel>} - Created record
*/
export async function create<TModel extends Model>(
model: { new (): TModel },
values: Partial<Record<keyof TModel["dataValues"], any>>,
options: CreateOptionsAsParam<TModel> = { includeSequelizeInternalKeys: false }): Promise<TModel> {
return await actionWrapper(async () => await (model as unknown as ModelDefined<any, any>).create({
...Object.fromEntries(
Object.entries(values).filter(([key]) => {
if(!options.includeSequelizeInternalKeys) {
return !SEQUELIZE_INTERNAL_KEYS.some(internalKey => internalKey === key);
}

return true;
}),
),
}, { ...options }) as TModel);
}

/**
* Remove specified single record(model instance)
*
* @template TModel - Model type
* @param model - Model instance
* @param options - Additional options to be passed to Sequelize
* @returns {Promise<true>} - `true` if the record is successfully removed
*/
export async function removeInstance<TModel extends Model>(
model: TModel,
options?: InstanceDestroyOptions): Promise<true> {
try {
await model.destroy();
await model.save({ transaction: options?.transaction });
} catch(error) {
console.error(error);

throw new BadRequestException("Can't be deleted");
}

return true;
}

/**
* Remove specified single record by primary key
*
* @template TModel - Model type
* @param model - Model class
* @param pk - Primary key to search
* @param options - Additional options to be passed to Sequelize
* @returns {Promise<true>} - `true` if the record is successfully removed
*/
export async function removeByPk<TModel extends Model>(
model: { new (): TModel },
pk: number,
options?: InstanceDestroyOptions): Promise<true> {
try {
await (model as unknown as ModelDefined<any, any>).destroy({
where: { [(model as unknown as ModelDefined<any, any>).primaryKeyAttribute]: pk },
...options,
});
} catch(error) {
console.error(error);

throw new BadRequestException("Can't be deleted");
}

return true;
}
Loading

0 comments on commit 38c684a

Please sign in to comment.