diff --git a/README.md b/README.md
index 1cd7849..fc7d403 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
-# yqni13 | support
-$\texttt{\color{teal}{v1.4.4}}$
+# yqni13 | $\texttt{\color{cornflowerblue}{SUPPORT}}$
+### $\textsf{\color{brown}{v1.4.9}}$
-### Support hub - handling feedback & ratings (`/feedback`) and bug/support requests (`/tickets`) including file attachments across multiple applications via REST API built with NodeJS (Typescript), Express & PostgreSQL in Docker container. Created following Test-Driven Development (450+ tests including ephemeral database by testcontainers) and hosting env:prod via Render, Neon and Cloudflare.
+#### Support hub - handling feedback/ratings (`/feedback`) and bug/support requests (`/tickets`) including file attachments across multiple applications via REST API. Built with NodeJS (Typescript), Express & PostgreSQL in Docker container using API-Key authentication and rate-limiting. Created following Test-Driven Development (450+ tests) and hosting env:prod via Render, Neon and Cloudflare.
@@ -64,7 +64,14 @@ Alternatively run application in Docker container [(see docs)](./docs/DEVOPS.md)
-### $\textsf{\color{teal}Feedback \&\ Rating}$
+### $\textsf{\color{teal}Tickets}$
+
+Main focus on this application is the creating and handling of tickets that are used to represent bug reports or support requests. Tickets are authenticated by client (application) and user identifier and hold information in different ways: `title` and `message` are used as the main description, followed by more specific but optional information like `device`, `operational system` and `browser` or optional file attachments that are stored in the cloud.
+Tickets use status to signal the process and ensure it doesn't get deleted before solved, canceled or after a certain time when paused. Deleting a ticket also removes the respective files from the cloud that were originally attached.
+
+
+
+### $\textsf{\color{teal}Feedback and Rating}$
User can utilize a feedback & rating system to rate the application in use and send criticism or praise. For every client can exist multiple entries for the entity `Feedback` but only one `FeedbackRating` which holds the accumulated data of the pointing feedback entries.
Resubmissions are handled in the database by an `ON CONFLICT` upsert query [see upsertInTa()](./backend/src/repositories/feedback.repository.ts) on the unique `(client_id, user_id)` constraint, followed by an atomic aggregate update to the 'FeedbackRating' table entry. Both queries are executed within a single transaction to guarantee data consistency.
@@ -75,21 +82,34 @@ Furthermore, if an existing feedback entry has a message stored, but is not revi
### $\textsf{\color{teal}File handling}$
-User can attach files for any support/bug ticket to provide further information (screenshots, images, ...) on their message. Attachments are limited to upload up to `5` files and each file can be up to `1`MB [see validation](./backend/src/middleware/files/validate.files.middleware.ts). Currently only `images` (webp, jpg, jpeg, png) and `pdf` files are supported, but more will follow. Cloud in use is `Cloudflare` (see Figure 1) using S3Client for api communication and files will be deleted when a ticket is closed, canceled or expired (time check).
+User can attach files for any support/bug ticket to provide further information (screenshots, images, ...) on their message. Attachments are limited to upload up to `5` files and each file can be up to `1`MB [see validation](./backend/src/middleware/files/validate.files.middleware.ts). Currently only `images` (webp, jpg, jpeg, png) and `pdf` files are supported, but more will follow. Cloud in use is `Cloudflare` (see `Figure 1`) using S3Client for api communication and files will be deleted when a ticket is closed, canceled or expired (time check).

Figure 1 - Cloudflare upload demo, v1.0.0
+
+
+### $\textsf{\color{teal}Observation}$
+
+In terms of rate-limiting, penalties and ready-to-extend functionality, the observation middleware takes care of monitoring incoming requests by users and clients (see following workflow or `Figure 2`).
+
+A certain set of rules checks for incoming requests on a total number for the day and within a certain time range. Before the engine returns found violations, the adapter calls for an increment of the daily rate-limit count. Violations are handled by the penalty handler (setting flags/status) and the workflow ends with either throwing an exception or calling next() to pass to the next middleware.
+
+

+ Figure 2 - observation middleware workflow, v1.0.0-beta.2
+
+
+
## ๐ $\textsf{\color{salmon}Logging}$
To monitor errors the logging framework `Winston` is used in combination with Logtail from `Betterstack` as a Singleton: [config](./backend/src/logger/config.logger.ts)
-
While working within local (DEV) or test environment, error messages are logged into the consoles. For the deployed environments (STAG/PROD) the logging is set to send logtails to Betterstack (longer storage time than app-hosting service). For easy access and monitoring of error messages, the Betterstack UI client dashboard comes in handy (see Figure 2). Additional meta data (environment + version numbers) help identifying and assigning errors.
+
While working within local (DEV) or test environment, error messages are logged into the consoles. For the deployed environments (STAG/PROD) the logging is set to send logtails to Betterstack (longer storage time than app-hosting service). For easy access and monitoring of error messages, the Betterstack UI client dashboard comes in handy (see `Figure 3`). Additional meta data (environment + version numbers) help identifying and assigning errors.

- Figure 2 - Betterstack logging dashboard, v1.0.0-beta.1
+ Figure 3 - Betterstack logging dashboard, v1.0.0-beta.1
@@ -104,14 +124,14 @@ Testing of the application server can be done automatically via Jest tests (next
[PAYLOAD] { "demo_mode": DemoMode }
```
Use `https://support-0hsq.onrender.com` for {{url}} to test on live conditions.
-See Figure 3 for the different use cases & responses (Postman, v11.73.5) - from left to right:
+See Figure 4 for the different use cases & responses (Postman, v11.73.5) - from left to right:
[PAYLOAD]: { "mode_enum": "success" } => retrieve current version number as request without fail
[PAYLOAD]: undefined (none) or empty obj/array => retrieve exception for undefined body
[PAYLOAD]: { "mode_enum": "%ยง$" } => retrieve exception due to invalid value
[PAYLOAD]: { "mode_enum": "error" } => retrieve exception for intended failing db query (see data.message: SEL instead of SELECT)

- Figure 3 - /test/demo responses, v1.3.1
+ Figure 4 - /test/demo responses, v1.3.1
@@ -150,16 +170,16 @@ or simply save as script command in `package.json` to run `npm test`:
To automatically check tests before merging feature/development branch further up, a `GitHub Action` is set up, see [main.yml](.github/workflows/main.yml).
-Preventing an unwanted merge with unfinished/failed test run, the project is set up to disable merging until all tests have passed (see Figure 4 to Figure 5).
+Preventing an unwanted merge with unfinished/failed test run, the project is set up to disable merging until all tests have passed (see Figure 5 to Figure 6).

- Figure 4 - processing tests, v0.9.1
+ Figure 5 - processing tests, v0.9.1

- Figure 5 - passing tests, v0.9.1
+ Figure 6 - passing tests, v0.9.1
@@ -168,11 +188,14 @@ Preventing an unwanted merge with unfinished/failed test run, the project is set
[see changelog for all updates](/docs/CHANGELOG.md)
-$\textsf{[v1.4.1\ =>\ {\textbf{\color{brown}v1.4.4}]}}$ app
+$\textsf{[v1.4.4\ =>\ {\textbf{\color{brown}v1.4.9}]}}$ app
+- $\textsf{\color{teal}Addition:}$ Added form-data parser middleware for requests including files.
- $\textsf{\color{orange}Patch:}$ Updated:
- + entity ID's are using now nominal types instead basic string|number.
- + some model functions are renamed to keep consistency and improve readability.
- + some api routes have been shortened to keep consistency and improve readability.
+ + return types, mapping and handling (part 1).
+ + calculation for delete-permission by comparing timestamps (incorporate timezone offset on database read timestamp).
+ + 'tickets' request: more accurate check for containing files.
+ + use Promise-instance fn finally() in repository-layer to reduce code.
+ + documentation headers and display error on symbol (&).
diff --git a/assets/img/observe_middleware_diagram.png b/assets/img/observe_middleware_diagram.png
new file mode 100644
index 0000000..b41d3a1
Binary files /dev/null and b/assets/img/observe_middleware_diagram.png differ
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 10d1d07..36f4289 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "support_backend",
- "version": "1.4.4",
+ "version": "1.4.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
diff --git a/backend/package.json b/backend/package.json
index 7980648..44b6337 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "support_backend",
- "version": "1.4.4",
+ "version": "1.4.9",
"appMeta": {
"db_version": "1.6.0",
"docker_image": "yqni13/support",
diff --git a/backend/src/controllers/clients.controller.ts b/backend/src/controllers/clients.controller.ts
index 17be71e..f6f16ca 100644
--- a/backend/src/controllers/clients.controller.ts
+++ b/backend/src/controllers/clients.controller.ts
@@ -2,9 +2,9 @@ import { NextFunction, Request, Response } from "express";
import { checkValidation } from "../middleware/validation.middleware";
import {
ClientsCreateResponseDTO,
- ClientsStatusResponseDTO,
ClientsCreateDTO,
- ClientsStatusUpdateDTO
+ ClientsStatusUpdateDTO,
+ ClientsResponseDTO
} from "../dtos/clients.dto";
import clientsService from "../services/clients.service";
import { ClientsId } from "../repositories/interfaces/clients.entity.interface";
@@ -14,7 +14,7 @@ class ClientsController {
try {
checkValidation(req);
const name = req.params.name;
- const response: ClientsStatusResponseDTO | null = await clientsService.getClientStatusByName(name);
+ const response: ClientsResponseDTO | null = await clientsService.getClientStatusByName(name);
res.json(response);
} catch(err: any) {
next(err);
@@ -37,7 +37,7 @@ class ClientsController {
checkValidation(req);
const id = req.params.id as ClientsId;
const dto: ClientsStatusUpdateDTO = req.body;
- const response: ClientsStatusResponseDTO | null = await clientsService.updateClientStatus(id, dto);
+ const response: ClientsResponseDTO | null = await clientsService.updateClientStatus(id, dto);
res.json(response);
} catch(err: any) {
next(err);
diff --git a/backend/src/controllers/feedback.controller.ts b/backend/src/controllers/feedback.controller.ts
index a55595b..af97058 100644
--- a/backend/src/controllers/feedback.controller.ts
+++ b/backend/src/controllers/feedback.controller.ts
@@ -1,6 +1,6 @@
import { NextFunction, Request, Response } from "express";
import { checkValidation } from "../middleware/validation.middleware";
-import { FeedbackCreateDTO, FeedbackFilterDTO, FeedbackResponseDTO } from "../dtos/feedback.dto";
+import { FeedbackCreateDTO, FeedbackCreateResponseDTO, FeedbackFilterDTO, FeedbackResponseDTO } from "../dtos/feedback.dto";
import feedbackService from "../services/feedback.service";
import { FeedbackId } from "../repositories/interfaces/feedback.entity.interface";
@@ -35,7 +35,7 @@ class FeedbackController {
client_id: req.apiClients.client_id,
user_id: req.apiUsers.user_id
};
- const response: FeedbackResponseDTO | null = await feedbackService.createFeedback(dto);
+ const response: FeedbackCreateResponseDTO | null = await feedbackService.createFeedback(dto);
res.json(response);
} catch(err: any) {
next(err);
diff --git a/backend/src/controllers/tickets.controller.ts b/backend/src/controllers/tickets.controller.ts
index 8f34398..3aebc71 100644
--- a/backend/src/controllers/tickets.controller.ts
+++ b/backend/src/controllers/tickets.controller.ts
@@ -5,7 +5,8 @@ import {
TicketsResponseDTO,
TicketsFilterDTO,
TicketsCreateDTO,
- TicketsUpdateDTO
+ TicketsUpdateDTO,
+ TicketsCreateResponseDTO
} from "../dtos/tickets.dto";
import ticketsService from "../services/tickets.service";
import { TicketsId } from "../repositories/interfaces/tickets.entity.interface";
@@ -55,8 +56,8 @@ class TicketsController {
client_id: req.apiClients.client_id,
user_id: req.apiUsers.user_id
};
- const files = req.files as Express.Multer.File[] ?? null;
- const response: TicketsResponseDTO = await ticketsService.createTicket(dto, files);
+ const files = !req.files || req.files.length === 0 ? null : req.files as Express.Multer.File[];
+ const response: TicketsCreateResponseDTO = await ticketsService.createTicket(dto, files);
res.json(response);
} catch(err: any) {
next(err);
diff --git a/backend/src/dtos/clients.dto.ts b/backend/src/dtos/clients.dto.ts
index ab95c0a..5ef8e57 100644
--- a/backend/src/dtos/clients.dto.ts
+++ b/backend/src/dtos/clients.dto.ts
@@ -20,17 +20,6 @@ export interface ClientsLastUseUpdateDTO {
last_use: string
}
-export interface ClientsExistResponseDTO {
- client_id: ClientsId,
- name: string,
- api_key_hash: string,
- status: ApiKeyStatus,
- flag: Flag | null,
- last_use: string,
- last_modified: string,
- created_on: string
-}
-
export interface ClientsCreateResponseDTO {
client_id: ClientsId,
name: string,
@@ -42,27 +31,16 @@ export interface ClientsCreateResponseDTO {
created_on: string
}
-export interface ClientsFlagResponseDTO {
- client_id: ClientsId,
- flag: Flag | null,
- last_use: string,
- last_modified: string,
- created_on: string
-}
-
-export interface ClientsStatusResponseDTO {
+export interface ClientsResponseDTO {
client_id: ClientsId,
name: string,
status: ApiKeyStatus,
+ flag: Flag | null,
last_use: string,
last_modified: string,
created_on: string
}
-export interface ClientsLastUseResponseDTO {
- client_id: ClientsId,
- name: string,
- last_use: string,
- last_modified: string,
- created_on: string
+export interface ClientsExtendedResponseDTO extends ClientsResponseDTO{
+ api_key_hash: string
}
\ No newline at end of file
diff --git a/backend/src/dtos/feedback.dto.ts b/backend/src/dtos/feedback.dto.ts
index e042d8c..41eedf6 100644
--- a/backend/src/dtos/feedback.dto.ts
+++ b/backend/src/dtos/feedback.dto.ts
@@ -32,6 +32,17 @@ export interface FeedbackFilterDTO {
created_on?: string | string[]
}
+/**
+ * @description Reduced content as this is used as response outside of admin environment.
+ */
+export interface FeedbackCreateResponseDTO {
+ rating: number,
+ rating_old?: number,
+ rating_average_new: number,
+ blocked?: boolean,
+ created_on: string
+}
+
export interface FeedbackResponseDTO {
feedback_id: FeedbackId,
client_id: ClientsId,
diff --git a/backend/src/dtos/tickets.dto.ts b/backend/src/dtos/tickets.dto.ts
index d894b47..0e6f254 100644
--- a/backend/src/dtos/tickets.dto.ts
+++ b/backend/src/dtos/tickets.dto.ts
@@ -61,6 +61,16 @@ export interface TicketsFilterDTO {
created_on?: string[]
}
+/**
+ * @description Reduced content as this is used as response outside of admin environment.
+ */
+export interface TicketsCreateResponseDTO {
+ status: TicketStatus,
+ option: TicketOption,
+ flag: Flag | null,
+ created_on: string
+}
+
export interface TicketsResponseDTO {
ticket_id: TicketsId,
client_id: ClientsId,
diff --git a/backend/src/middleware/files/parse.files.middleware.ts b/backend/src/middleware/parser/files.parser.middleware.ts
similarity index 100%
rename from backend/src/middleware/files/parse.files.middleware.ts
rename to backend/src/middleware/parser/files.parser.middleware.ts
diff --git a/backend/src/middleware/parser/form-data.parser.middleware.ts b/backend/src/middleware/parser/form-data.parser.middleware.ts
new file mode 100644
index 0000000..24f60db
--- /dev/null
+++ b/backend/src/middleware/parser/form-data.parser.middleware.ts
@@ -0,0 +1,21 @@
+import { Request, Response, NextFunction } from "express";
+import { logError } from "../../utils/common.utils";
+
+export function parseFormData() {
+ return function(req: Request, res: Response, next: NextFunction) {
+ try {
+ if(req.body && req.body.data) {
+ req.body = JSON.parse(req.body.data);
+ }
+ next();
+ } catch(err: any) {
+ err.status = !err.status ? 404 : err.status;
+ logError(
+ "PARSE MIDDLEWARE ERROR ON FORM DATA",
+ "SUPPORT_middleware_parseFormData",
+ err
+ );
+ next(err);
+ }
+ }
+}
\ No newline at end of file
diff --git a/backend/src/models/clients.model.ts b/backend/src/models/clients.model.ts
index 4c74f10..e716a36 100644
--- a/backend/src/models/clients.model.ts
+++ b/backend/src/models/clients.model.ts
@@ -1,6 +1,8 @@
import {
ClientsCreateDTO,
ClientsCreateResponseDTO,
+ ClientsExtendedResponseDTO,
+ ClientsResponseDTO,
} from "../dtos/clients.dto";
import { Clients, ClientsId } from "../repositories/interfaces/clients.entity.interface";
import crypto from 'crypto';
@@ -8,26 +10,40 @@ import * as CommonUtils from "../utils/common.utils";
import { ApiKeyStatus } from "../utils/enums/api-key-status.enum";
class ClientsModel {
- private timeMapTargets: string[];
-
- constructor() {
- this.timeMapTargets = ['last_use', 'last_modified', 'created_on'];
- }
toClientsCreateResponseDTO(data: Clients, apiKey: string): ClientsCreateResponseDTO {
- data = CommonUtils.mapObjTimestamps(data, this.timeMapTargets);
return {
client_id: data.client_id,
name: data.name,
api_key: apiKey,
status: data.status,
flag: data.flag,
- last_use: data.last_use,
- last_modified: data.last_modified,
- created_on: data.created_on
+ last_use: CommonUtils.getTimestampUTC(new Date(data.last_use)),
+ last_modified: CommonUtils.getTimestampUTC(new Date(data.last_modified)),
+ created_on: CommonUtils.getTimestampUTC(new Date(data.created_on))
};
}
+ toClientsResponseDTO(entity: Clients, extended: true): ClientsExtendedResponseDTO;
+ toClientsResponseDTO(entity: Clients, extended: false): ClientsResponseDTO;
+
+ toClientsResponseDTO(entity: Clients, extended: boolean) {
+ const response = {
+ client_id: entity.client_id,
+ name: entity.name,
+ api_key_hash: entity.api_key_hash,
+ status: entity.status,
+ flag: entity.flag,
+ last_use: CommonUtils.getTimestampUTC(new Date(entity.last_use)),
+ last_modified: CommonUtils.getTimestampUTC(new Date(entity.last_modified)),
+ created_on: CommonUtils.getTimestampUTC(new Date(entity.created_on))
+ };
+ if(!extended) {
+ delete (response as any)['api_key_hash'];
+ }
+ return response;
+ }
+
private generateApiKeyObj(): { keyRaw: string, keyHash: string } {
const keyLength = 42;
const charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
diff --git a/backend/src/models/tickets.model.ts b/backend/src/models/tickets.model.ts
index 2b011ee..117c48f 100644
--- a/backend/src/models/tickets.model.ts
+++ b/backend/src/models/tickets.model.ts
@@ -1,4 +1,4 @@
-import { TicketsCreateDTO, TicketsResponseDTO, TicketsUpdateDTO } from "../dtos/tickets.dto";
+import { TicketsCreateDTO, TicketsCreateResponseDTO, TicketsResponseDTO, TicketsUpdateDTO } from "../dtos/tickets.dto";
import { Tickets, TicketsId } from "../repositories/interfaces/tickets.entity.interface";
import * as CommonUtils from "../utils/common.utils";
import { TicketStatus } from "../utils/enums/ticket-status.enum";
@@ -9,7 +9,7 @@ class TicketsModel {
async generateTicketEntity(dto: TicketsCreateDTO, files: Express.Multer.File[] | null): Promise {
const timestamp = CommonUtils.getTimestampUTC();
const newId = CommonUtils.generateUUID();
- let paths: string[] | null = null;
+ let paths: string[] | undefined = undefined;
if(files) {
const filesService = new FilesService(files, 'tickets');
filesService.transformFiles(newId);
@@ -24,7 +24,7 @@ class TicketsModel {
option: dto.option,
title: dto.title,
message: dto.message,
- resource_paths: paths ?? dto.resource_paths,
+ resource_paths: paths,
flag: null,
info_browser: dto.info_browser,
info_os: dto.info_os,
@@ -42,6 +42,15 @@ class TicketsModel {
};
}
+ toTicketsCreateResponseDTO(entity: Tickets): TicketsCreateResponseDTO {
+ return {
+ status: entity.status,
+ option: entity.option,
+ flag: entity.flag,
+ created_on: CommonUtils.getTimestampUTC(new Date(entity.created_on))
+ }
+ }
+
async handleTicketBeforeDelete(dto: TicketsResponseDTO) {
if(dto.resource_paths && dto.resource_paths.length > 0) {
const filesService = new FilesService([], 'tickets');
@@ -55,9 +64,18 @@ class TicketsModel {
{ timeRange: 30, apply: (status: TicketStatus) => status === TicketStatus.CLOSED },
{ timeRange: 0, apply: (status: TicketStatus) => status === TicketStatus.CANCEL }
];
- const factorMilSecToDays = 1 / (1000 * 3600 * 24);
- const days = Math.floor((CommonUtils.now().getTime() - new Date(dto.created_on).getTime()) * factorMilSecToDays);
- const isPermitted = deleteRules.find(rule => days >= rule.timeRange)?.apply(dto.status) ?? false
+ const factorMilSecToDays = 1 / (1000 * 3600 * 24);
+ const convertedTS = (ts: string) => {
+ const offSetInMs = new Date(ts).getTimezoneOffset();
+ const conversion = 60 * 1000;
+ // Javascript turns - (-) => +
+ return offSetInMs < 0
+ ? new Date(ts).getTime() - ((-1) * offSetInMs * conversion)
+ : new Date(ts).getTime() - (offSetInMs * conversion);
+ }
+
+ const passedDays = Math.floor((CommonUtils.now().getTime() - convertedTS(dto.created_on)) * factorMilSecToDays);
+ const isPermitted = deleteRules.find(rule => passedDays >= rule.timeRange)?.apply(dto.status);
if(!isPermitted) {
throw new PermissionException('support-delete-prohibited');
}
diff --git a/backend/src/repositories/clients.repository.ts b/backend/src/repositories/clients.repository.ts
index 6e85de4..2c7b319 100644
--- a/backend/src/repositories/clients.repository.ts
+++ b/backend/src/repositories/clients.repository.ts
@@ -22,14 +22,14 @@ class ClientsRepository implements ICreateRepository, IUpdateFlagReposi
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, value);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_ClientsRepository_findById";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -41,14 +41,14 @@ class ClientsRepository implements ICreateRepository, IUpdateFlagReposi
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, values);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_ClientsRepository_findByActiveKey";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -60,14 +60,14 @@ class ClientsRepository implements ICreateRepository, IUpdateFlagReposi
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, value);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_ClientsRepository_findStatusByName";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -83,14 +83,14 @@ class ClientsRepository implements ICreateRepository, IUpdateFlagReposi
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, values);
- await db.close(client);
return result.rows[0];
} catch(err: any) {
const message = "DB ERROR ON INSERT QUERY";
const method = "SUPPORT_ClientsRepository_create";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -107,14 +107,14 @@ class ClientsRepository implements ICreateRepository, IUpdateFlagReposi
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, values);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON UPDATE QUERY";
const method = "SUPPORT_ClientsRepository_updateFlag";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -131,14 +131,14 @@ class ClientsRepository implements ICreateRepository, IUpdateFlagReposi
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, values);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON UPDATE QUERY";
const method = "SUPPORT_ClientsRepository_updateStatus";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -155,14 +155,14 @@ class ClientsRepository implements ICreateRepository, IUpdateFlagReposi
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, values);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON UPDATE QUERY";
const method = "SUPPORT_ClientsRepository_updateLastStatus";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
}
diff --git a/backend/src/repositories/demo-limits.repository.ts b/backend/src/repositories/demo-limits.repository.ts
index 77d6f67..a404a97 100644
--- a/backend/src/repositories/demo-limits.repository.ts
+++ b/backend/src/repositories/demo-limits.repository.ts
@@ -23,14 +23,14 @@ class DemoLimitsRepository {
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, value);
- await db.close(client);
return !result.rows[0] || result.rows.length === 0 ? null : result.rows;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_DemoLimitsRepository_count";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -45,14 +45,14 @@ class DemoLimitsRepository {
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, values);
- await db.close(client);
return result.rows[0];
} catch(err: any) {
const message = "DB ERROR ON INSERT QUERY";
const method = "SUPPORT_DemoLimitsRepository_create";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -67,14 +67,14 @@ class DemoLimitsRepository {
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, values);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON UPDATE QUERY";
const method = "SUPPORT_DemoLimitsRepository_update";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
}
diff --git a/backend/src/repositories/feedback-rating.repository.ts b/backend/src/repositories/feedback-rating.repository.ts
index d0251f6..ba924d8 100644
--- a/backend/src/repositories/feedback-rating.repository.ts
+++ b/backend/src/repositories/feedback-rating.repository.ts
@@ -24,14 +24,14 @@ IFindRepository {
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, value);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_FeedbackRatingRepository_findById";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -47,14 +47,14 @@ IFindRepository {
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, value);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_FeedbackRatingRepository_findByClientsName";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -66,14 +66,14 @@ IFindRepository {
try {
client = await db.connect();
const result: QueryResult = await client.query(sql);
- await db.close(client);
return !result.rows[0] || result.rows.length === 0 ? null : result.rows;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_FeedbackRatingRepository_findAll";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
diff --git a/backend/src/repositories/feedback.repository.ts b/backend/src/repositories/feedback.repository.ts
index d218d03..9b84ba2 100644
--- a/backend/src/repositories/feedback.repository.ts
+++ b/backend/src/repositories/feedback.repository.ts
@@ -22,14 +22,14 @@ class FeedbackRepository {
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, value);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_FeedbackRepository_findById";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -40,39 +40,39 @@ class FeedbackRepository {
try {
client = await db.connect();
const result: QueryResult = await client.query(queryData.sql, queryData.values);
- await db.close(client);
return !result.rows[0] || result.rows.length === 0 ? null : result.rows;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_FeedbackRepository_findByFilter";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
async updateReview(id: FeedbackId, dto: FeedbackUpdateReviewDTO): Promise {
const filterColumn = "feedback_id";
- const sql = `UPDATE ${this.table}
- SET reviewed_on = $1::timestamp, last_modified = $2::timestamp
- WHERE ${filterColumn} = $3
- RETURNING *;
- `;
- const values = [dto.reviewed_on, dto.last_modified, id];
- const db = DBConnection.getInstance();
- let client: any;
- try {
- client = await db.connect();
- const result: QueryResult = await client.query(sql, values);
- await db.close(client);
- return result.rows[0] ?? null;
- } catch(err: any) {
- const message = "DB ERROR ON UPDATE QUERY";
- const method = "SUPPORT_FeedbackRepository_updateReview";
- logError(message, method, err);
- await db.close(client);
- throw new DBQueryErrorException(err);
- }
+ const sql = `UPDATE ${this.table}
+ SET reviewed_on = $1::timestamp, last_modified = $2::timestamp
+ WHERE ${filterColumn} = $3
+ RETURNING *;
+ `;
+ const values = [dto.reviewed_on, dto.last_modified, id];
+ const db = DBConnection.getInstance();
+ let client: any;
+ try {
+ client = await db.connect();
+ const result: QueryResult = await client.query(sql, values);
+ return result.rows[0] ?? null;
+ } catch(err: any) {
+ const message = "DB ERROR ON UPDATE QUERY";
+ const method = "SUPPORT_FeedbackRepository_updateReview";
+ logError(message, method, err);
+ throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
+ }
}
/**
diff --git a/backend/src/repositories/meta.repository.ts b/backend/src/repositories/meta.repository.ts
index 1d99bad..58fe673 100644
--- a/backend/src/repositories/meta.repository.ts
+++ b/backend/src/repositories/meta.repository.ts
@@ -27,14 +27,14 @@ class MetaRepository implements IBaseRepository, IFindRepository {
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, value);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_MetaRepository_findById";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -52,14 +52,14 @@ class MetaRepository implements IBaseRepository, IFindRepository {
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, value);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_MetaRepository_findByName";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -71,14 +71,14 @@ class MetaRepository implements IBaseRepository, IFindRepository {
try {
client = await db.connect();
const result: QueryResult = await client.query(sql);
- await db.close(client);
return !result.rows[0] || result.rows.length === 0 ? null : result.rows;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_MetaRepository_findAll";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -92,14 +92,14 @@ class MetaRepository implements IBaseRepository, IFindRepository {
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, value);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_MetaRepository_findMaintenance";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -118,14 +118,14 @@ class MetaRepository implements IBaseRepository, IFindRepository {
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, values);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON UPDATE QUERY";
const method = "SUPPORT_MetaRepository_update";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -142,14 +142,14 @@ class MetaRepository implements IBaseRepository, IFindRepository {
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, values);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON UPDATE QUERY";
const method = "SUPPORT_MetaRepository_updateMaintenance";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -163,14 +163,14 @@ class MetaRepository implements IBaseRepository, IFindRepository {
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, value);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_MetaRepository_demoError";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
}
diff --git a/backend/src/repositories/rate-limits.repository.ts b/backend/src/repositories/rate-limits.repository.ts
index 27fb192..1aa05ed 100644
--- a/backend/src/repositories/rate-limits.repository.ts
+++ b/backend/src/repositories/rate-limits.repository.ts
@@ -29,14 +29,14 @@ class RateLimitsRepository {
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, values);
- await db.close(client);
return !result.rows[0] || result.rows.length === 0 ? null : result.rows;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_RateLimitsRepository_count";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -54,14 +54,14 @@ class RateLimitsRepository {
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, values);
- await db.close(client);
return result.rows[0];
} catch(err: any) {
const message = "DB ERROR ON INSERT QUERY";
const method = "SUPPORT_RateLimitsRepository_create";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -78,14 +78,14 @@ class RateLimitsRepository {
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, values);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON UPDATE QUERY";
const method = "SUPPORT_RateLimitsRepository_update";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
}
diff --git a/backend/src/repositories/tickets.repository.ts b/backend/src/repositories/tickets.repository.ts
index d9a5a9d..522b38f 100644
--- a/backend/src/repositories/tickets.repository.ts
+++ b/backend/src/repositories/tickets.repository.ts
@@ -40,14 +40,14 @@ IDeleteRepository
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, value);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_TicketsRepository_findById";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -59,14 +59,14 @@ IDeleteRepository
try {
client = await db.connect();
const result: QueryResult = await client.query(sql);
- await db.close(client);
return !result.rows[0] || result.rows.length === 0 ? null : result.rows;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_TicketsRepository_findAll";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -84,14 +84,14 @@ IDeleteRepository
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, values);
- await db.close(client);
return !result.rows[0] || result.rows.length === 0 ? null : result.rows;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_TicketsRepository_findByTimeInterval";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -105,14 +105,14 @@ IDeleteRepository
try {
client = await db.connect();
const result: QueryResult = await client.query(queryData.sql, queryData.values);
- await db.close(client);
return !result.rows[0] || result.rows.length === 0 ? null : result.rows;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_TicketsRepository_findByFilter";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -127,14 +127,14 @@ IDeleteRepository
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, values);
- await db.close(client);
return result.rows[0];
} catch(err: any) {
const message = "DB ERROR ON INSERT QUERY";
const method = "SUPPORT_TicketsRepository_create";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -152,14 +152,14 @@ IDeleteRepository
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, values);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON UPDATE QUERY";
const method = "SUPPORT_TicketsRepository_update";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -172,14 +172,14 @@ IDeleteRepository
try {
client = await db.connect();
const result = await client.query(sql, value);
- await db.close(client);
return result.rowCount > 0;
} catch(err: any) {
const message = "DB ERROR ON DELETE QUERY";
const method = "SUPPORT_TicketsRepository_delete";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
}
diff --git a/backend/src/repositories/users.repository.ts b/backend/src/repositories/users.repository.ts
index d579c99..3fd8531 100644
--- a/backend/src/repositories/users.repository.ts
+++ b/backend/src/repositories/users.repository.ts
@@ -28,14 +28,14 @@ IUpdateFlagRepository
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, value);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_UsersRepository_findById";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -48,14 +48,14 @@ IUpdateFlagRepository
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, value);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_UsersRepository_findByEmail";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -67,14 +67,14 @@ IUpdateFlagRepository
try {
client = await db.connect();
const result: QueryResult = await client.query(sql);
- await db.close(client);
return !result.rows[0] || result.rows.length === 0 ? null : result.rows;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_UsersRepository_findAll";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -88,14 +88,14 @@ IUpdateFlagRepository
try {
client = await db.connect();
const result: QueryResult = await client.query(queryData.sql, queryData.values);
- await db.close(client);
return !result.rows[0] || result.rows.length === 0 ? null : result.rows;
} catch(err: any) {
const message = "DB ERROR ON SELECT QUERY";
const method = "SUPPORT_UsersRepository_findByFilter";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -111,14 +111,14 @@ IUpdateFlagRepository
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, values);
- await db.close(client);
return result.rows[0];
} catch(err: any) {
const message = "DB ERROR ON INSERT QUERY";
const method = "SUPPORT_UsersRepository_create";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -135,14 +135,14 @@ IUpdateFlagRepository
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, values);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON UPDATE QUERY";
const method = "SUPPORT_UsersRepository_update";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
@@ -159,14 +159,14 @@ IUpdateFlagRepository
try {
client = await db.connect();
const result: QueryResult = await client.query(sql, values);
- await db.close(client);
return result.rows[0] ?? null;
} catch(err: any) {
const message = "DB ERROR ON UPDATE QUERY";
const method = "SUPPORT_UsersRepository_updateFlag";
logError(message, method, err);
- await db.close(client);
throw new DBQueryErrorException(err);
+ } finally {
+ await db.close(client);
}
}
}
diff --git a/backend/src/routes/tickets.route.ts b/backend/src/routes/tickets.route.ts
index f56083d..af6d164 100644
--- a/backend/src/routes/tickets.route.ts
+++ b/backend/src/routes/tickets.route.ts
@@ -14,9 +14,10 @@ import { authClient } from '../middleware/auth.client.middleware';
import { authUser } from '../middleware/auth.user.middleware';
import { maintain } from '../middleware/maintenance.middleware';
import { observe } from '../middleware/observe.middleware';
-import { parseFiles } from '../middleware/files/parse.files.middleware';
+import { parseFiles } from '../middleware/parser/files.parser.middleware';
import { requirePayload } from '../middleware/require.middleware';
import { validateFiles } from '../middleware/files/validate.files.middleware';
+import { parseFormData } from '../middleware/parser/form-data.parser.middleware';
const router = Router();
@@ -46,7 +47,7 @@ router.post(
// create
router.post(
'/create',
- maintain(), parseFiles(), authClient(), authUser(),
+ maintain(), parseFiles(), parseFormData(), authClient(), authUser(),
requirePayload(), observe(), validateFiles(),
postSchema,
factory(ticketsController.postTicket)
diff --git a/backend/src/services/clients.service.ts b/backend/src/services/clients.service.ts
index b0160ba..37e2c6c 100644
--- a/backend/src/services/clients.service.ts
+++ b/backend/src/services/clients.service.ts
@@ -1,12 +1,10 @@
import {
ClientsCreateDTO,
ClientsCreateResponseDTO,
- ClientsExistResponseDTO,
- ClientsFlagResponseDTO,
+ ClientsExtendedResponseDTO,
ClientsFlagUpdateDTO,
- ClientsLastUseResponseDTO,
ClientsLastUseUpdateDTO,
- ClientsStatusResponseDTO,
+ ClientsResponseDTO,
ClientsStatusUpdateDTO
} from "../dtos/clients.dto";
import clientsModel from '../models/clients.model';
@@ -15,56 +13,51 @@ import clientsRepository from '../repositories/clients.repository';
import { Clients, ClientsId } from "../repositories/interfaces/clients.entity.interface";
class ClientsService {
- private timeMapTargets: string[];
-
- constructor() {
- this.timeMapTargets = ['last_use', 'last_modified', 'created_on'];
- }
/**
* @description Usage for testing purpose.
*/
- async getClientById(id: ClientsId): Promise {
- const result = await clientsRepository.findById(id);
- return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets);
+ async getClientById(id: ClientsId): Promise {
+ const result: Clients | null = await clientsRepository.findById(id);
+ return !result ? null : clientsModel.toClientsResponseDTO(result, true);
}
/**
* @description Usage for apikey authentication in auth.middleware.ts.
*/
- async getClientByActiveKey(key: string): Promise {
+ async getClientByActiveKey(key: string): Promise {
const hash = CommonUtils.mapKeyToHash(key);
- const result = await clientsRepository.findByActiveKey(hash);
- return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets);
+ const result: Clients | null = await clientsRepository.findByActiveKey(hash);
+ return !result ? null : clientsModel.toClientsResponseDTO(result, true);
}
- async getClientStatusByName(name: string): Promise {
- const result = await clientsRepository.findStatusByName(name);
- return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets);
+ async getClientStatusByName(name: string): Promise {
+ const result: Clients | null = await clientsRepository.findStatusByName(name);
+ return !result ? null : clientsModel.toClientsResponseDTO(result, false);
}
async createClient(dto: ClientsCreateDTO): Promise {
const clientsCreateObj = clientsModel.generateClientsCreateObj(dto);
- const result = await clientsRepository.create(clientsCreateObj.client);
+ const result: Clients = await clientsRepository.create(clientsCreateObj.client);
return clientsModel.toClientsCreateResponseDTO(result as Clients, clientsCreateObj.keyRaw);
}
- async updateClientFlag(id: ClientsId, dto: ClientsFlagUpdateDTO): Promise {
+ async updateClientFlag(id: ClientsId, dto: ClientsFlagUpdateDTO): Promise {
dto.last_modified = CommonUtils.getTimestampUTC();
- const result = await clientsRepository.updateFlag(id, dto);
- return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets);
+ const result: Clients | null = await clientsRepository.updateFlag(id, dto);
+ return !result ? null : clientsModel.toClientsResponseDTO(result, false);
}
- async updateClientStatus(id: ClientsId, dto: ClientsStatusUpdateDTO): Promise {
+ async updateClientStatus(id: ClientsId, dto: ClientsStatusUpdateDTO): Promise {
dto.last_modified = CommonUtils.getTimestampUTC();
- const result = await clientsRepository.updateStatus(id, dto);
- return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets);
+ const result: Clients | null = await clientsRepository.updateStatus(id, dto);
+ return !result ? null : clientsModel.toClientsResponseDTO(result, false);
}
- async updateClientLastUse(id: ClientsId): Promise {
+ async updateClientLastUse(id: ClientsId): Promise {
const dto: ClientsLastUseUpdateDTO = { last_use: CommonUtils.getTimestampUTC() };
- const result = await clientsRepository.updateLastUse(id, dto);
- return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets);
+ const result: Clients | null = await clientsRepository.updateLastUse(id, dto);
+ return !result ? null : clientsModel.toClientsResponseDTO(result, false);
}
}
diff --git a/backend/src/services/feedback.service.ts b/backend/src/services/feedback.service.ts
index 87c1f40..b07e42d 100644
--- a/backend/src/services/feedback.service.ts
+++ b/backend/src/services/feedback.service.ts
@@ -1,11 +1,13 @@
import {
FeedbackCreateDTO,
+ FeedbackCreateResponseDTO,
FeedbackFilterDTO,
FeedbackResponseDTO,
FeedbackUpdateReviewDTO
} from "../dtos/feedback.dto";
import { FeedbackRatingCreateDTO, FeedbackRatingResponseDTO, FeedbackRatingUpdateDTO } from "../dtos/feedback-rating.dto";
import * as RepoUtils from "../utils/repository.utils";
+import * as CommonUtils from "../utils/common.utils";
import feedbackRatingModel from "../models/feedback-rating.model";
import feedbackModel from "../models/feedback.model";
import feedbackRepository from "../repositories/feedback.repository";
@@ -15,7 +17,7 @@ import { DBConstraintErrorException } from "../utils/exceptions/db.exception";
class FeedbackService {
async getFeedbackById(id: FeedbackId): Promise {
- const result = await feedbackRepository.findById(id);
+ const result: Feedback | null = await feedbackRepository.findById(id);
return !result ? null : feedbackModel.toFeedbackResponseDTO(result);
}
@@ -25,15 +27,17 @@ class FeedbackService {
}
/**
- * @description Create is used to create new or overwrite existing Feedback with new data inside database transaction => FeedbackRating created/updated in same process.
+ * @description Create is used to create new or overwrite existing Feedback with new data inside
+ * database transaction => FeedbackRating created/updated in same process.
*/
- async createFeedback(dto: FeedbackCreateDTO): Promise {
+ async createFeedback(dto: FeedbackCreateDTO): Promise {
const message = "DB ERROR ON FEEDBACK/FEEDBACK-RATING TRANSACTION";
const method = "SUPPORT_FeedbackService_createFeedback";
return RepoUtils.asTransaction(message, method, async(client) => {
const entity: Partial = feedbackModel.generateFeedbackEntity(dto);
const result: FeedbackResponseDTO | null = await feedbackRepository.upsertInTa(client, entity);
+
let dtoUpdateFR: FeedbackRatingUpdateDTO;
if(!result) {
return null;
@@ -50,7 +54,8 @@ class FeedbackService {
}
dtoUpdateFR = feedbackRatingModel.mapFeedbackRatingUpdateDTO(dtoUpdateFR);
const update: FeedbackRatingResponseDTO | null =
- await feedbackRatingService.updateFeedbackRatingInTa(client, result.client_id, dtoUpdateFR);
+ await feedbackRatingService.updateFeedbackRatingInTa(client, dto.client_id, dtoUpdateFR);
+
if(!update) {
const dtoCreateFR: FeedbackRatingCreateDTO = {
client_id: dto.client_id,
@@ -59,11 +64,12 @@ class FeedbackService {
};
await feedbackRatingService.createFeedbackRatingInTa(client, dtoCreateFR);
}
- // Delete blocked info and use rating from dto if no other ratings for this client exist.
- delete result['blocked'];
+
return {
- ...result,
- rating_average_new: update?.rating_average ?? dto.rating
+ rating: result.rating,
+ rating_old: result.rating_old,
+ rating_average_new: update?.rating_average ?? dto.rating,
+ created_on: CommonUtils.getTimestampUTC(new Date(result.created_on))
}
})
}
diff --git a/backend/src/services/rate-limits.service.ts b/backend/src/services/rate-limits.service.ts
index 54ef9d5..0146b9c 100644
--- a/backend/src/services/rate-limits.service.ts
+++ b/backend/src/services/rate-limits.service.ts
@@ -17,21 +17,23 @@ class RateLimitsService {
}
async getRateLimitCount(dto: RateLimitsCountDTO): Promise {
- const result = await rateLimitsRepository.count(dto);
+ const result: RateLimits[] | null = await rateLimitsRepository.count(dto);
return rateLimitsModel.mapCounts(result);
+ // TODO(yqni13): null-value handling missing!
}
async createRateLimit(dto: RateLimitsCreateDTO): Promise {
const entity: Partial = rateLimitsModel.mapNewEntity(dto);
- const result = await rateLimitsRepository.create(entity);
+ const result: RateLimits = await rateLimitsRepository.create(entity);
return CommonUtils.mapObjTimestamps(result, this.timeMapTargets);
+ // TODO(yqni13): mapping from entity to dto missing!
}
async updateRateLimit(dto: RateLimitsUpdateDTO): Promise {
const timestamp = new Date();
dto['day'] = CommonUtils.getDateUTC(timestamp);
dto['last_modified'] = CommonUtils.getTimestampUTC(timestamp);
- const result = await rateLimitsRepository.update(dto);
+ const result: RateLimits | null = await rateLimitsRepository.update(dto);
return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets);
}
}
diff --git a/backend/src/services/tickets.service.ts b/backend/src/services/tickets.service.ts
index 2a2eacf..4674446 100644
--- a/backend/src/services/tickets.service.ts
+++ b/backend/src/services/tickets.service.ts
@@ -4,10 +4,11 @@ import {
TicketsFilterDTO,
TicketsCreateDTO,
TicketsUpdateDTO,
- TicketsIntervalDTO
+ TicketsIntervalDTO,
+ TicketsCreateResponseDTO
} from "../dtos/tickets.dto";
import ticketsModel from "../models/tickets.model";
-import { TicketsId } from "../repositories/interfaces/tickets.entity.interface";
+import { Tickets, TicketsId } from "../repositories/interfaces/tickets.entity.interface";
import ticketsRepository from "../repositories/tickets.repository";
import * as CommonUtils from "../utils/common.utils";
@@ -38,10 +39,10 @@ class TicketsService {
return !result ? null : CommonUtils.mapArrayTimestamps(result, this.timeMapTargets);
}
- async createTicket(dto: TicketsCreateDTO, files: Express.Multer.File[] | null): Promise {
+ async createTicket(dto: TicketsCreateDTO, files: Express.Multer.File[] | null): Promise {
const ticket = await ticketsModel.generateTicketEntity(dto, files);
- const result = await ticketsRepository.create(ticket);
- return CommonUtils.mapObjTimestamps(result, this.timeMapTargets);
+ const result: Tickets = await ticketsRepository.create(ticket);
+ return ticketsModel.toTicketsCreateResponseDTO(result);
}
async updateTicket(id: TicketsId, dto: TicketsUpdateDTO): Promise {
diff --git a/backend/src/utils/common.utils.ts b/backend/src/utils/common.utils.ts
index 49aefc1..c381983 100644
--- a/backend/src/utils/common.utils.ts
+++ b/backend/src/utils/common.utils.ts
@@ -1,6 +1,3 @@
-import { InvalidSourceException } from './exceptions/common.exception';
-import { MailSource } from './enums/mail-source.enum';
-import { secrets } from './secrets.utils';
import { v4 as uuid_v4 } from 'uuid';
import crypto from 'crypto';
import { Logger } from '../logger/config.logger';
@@ -9,7 +6,6 @@ const logger = Logger.getLogger();
/**
* @description Testable function for current Date object.
- * @returns new Date()
*/
export const now = (): Date => new Date();
@@ -81,7 +77,6 @@ export function getNextRankEnumValue>(enumObj: T,
}
/**
- *
* @returns {string} Returns substring or empty string if endChar is not found in text.
*/
export function getPreCharString(text: string, endChar: string): string {
@@ -89,7 +84,6 @@ export function getPreCharString(text: string, endChar: string): string {
}
/**
- *
* @returns {string} Returns substring or empty string if startChar is not found in text.
*/
export function getPostCharString(text: string, startChar: string): string {
diff --git a/backend/src/utils/enums/ticket-option.enum.ts b/backend/src/utils/enums/ticket-option.enum.ts
index 5d76b4d..7495482 100644
--- a/backend/src/utils/enums/ticket-option.enum.ts
+++ b/backend/src/utils/enums/ticket-option.enum.ts
@@ -1,7 +1,6 @@
-export type TicketOption = 'bug' | 'support' | 'feedback';
+export type TicketOption = 'bug' | 'support';
export const TicketOption = {
BUG: 'bug' as TicketOption,
- SUPPORT: 'support' as TicketOption,
- FEEDBACK: 'feedback' as TicketOption
+ SUPPORT: 'support' as TicketOption
}
\ No newline at end of file
diff --git a/backend/tests/integration-tests/repositories/clients.integration.test.ts b/backend/tests/integration-tests/repositories/clients.integration.test.ts
index e3831bc..5c5eaf1 100644
--- a/backend/tests/integration-tests/repositories/clients.integration.test.ts
+++ b/backend/tests/integration-tests/repositories/clients.integration.test.ts
@@ -1,12 +1,11 @@
import { NextFunction, Request, Response } from "express";
import {
ClientsCreateResponseDTO,
- ClientsStatusResponseDTO,
ClientsCreateDTO,
ClientsStatusUpdateDTO,
- ClientsFlagResponseDTO,
ClientsFlagUpdateDTO,
- ClientsExistResponseDTO
+ ClientsExtendedResponseDTO,
+ ClientsResponseDTO,
} from "../../../src/dtos/clients.dto";
import * as CommonUtils from '../../../src/utils/common.utils';
import * as MockUtils from "../../common.test-utils";
@@ -31,6 +30,7 @@ jest.mock('../../../src/middleware/maintenance.middleware', () => ({
}));
import app from '../../../src/app';
+import { DBTestData } from "../../db-data.setup";
jest.setTimeout(60000);
@@ -40,10 +40,14 @@ const testTimestamp = '2025-01-01T14:00:02.000Z';
describe('Integration-tests (repository), priority: entity Clients', () => {
let dbTestSetup: DBTestSetup;
+ let dbTestData: DBTestData;
+ let dbData_Clients: any[];
let apiUrl: string;
const testVar_apiKey = { keyRaw: secrets.TEST_APIKEY_RAW, keyHash: secrets.TEST_APIKEY_HASH };
beforeAll(async () => {
dbTestSetup = new DBTestSetup();
+ dbTestData = DBTestData.getInstance();
+ dbData_Clients = dbTestData.getClientsInsertSql().values;
await dbTestSetup.init();
MockUtils.disableConsoleMessages();
await runMigrations('clients.integration.test.ts');
@@ -62,7 +66,7 @@ describe('Integration-tests (repository), priority: entity Clients', () => {
test('Repository process fn findById(), result: "SUCCESS"', async () => {
const testParam_id = mockValidClientId;
- const testResult: ClientsExistResponseDTO | null = {
+ const testResult: ClientsExtendedResponseDTO | null = {
client_id: testParam_id,
name: 'TESTCLIENT',
api_key_hash: secrets.TEST_APIKEY_HASH,
@@ -81,10 +85,11 @@ describe('Integration-tests (repository), priority: entity Clients', () => {
test('Repository process fn findStatusByName(), result: "SUCCESS"', async () => {
const testParam_name = 'TESTCLIENT';
- const testResult: ClientsStatusResponseDTO = {
+ const testResult: ClientsResponseDTO = {
client_id: mockValidClientId,
name: testParam_name,
- status: ApiKeyStatus.ACTIVE,
+ status: dbData_Clients[3],
+ flag: dbData_Clients[4],
last_use: testTimestamp,
last_modified: testTimestamp,
created_on: testTimestamp
@@ -146,8 +151,10 @@ describe('Integration-tests (repository), priority: entity Clients', () => {
jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp);
- const testResult: ClientsFlagResponseDTO | null = {
+ const testResult: ClientsResponseDTO | null = {
client_id: testParam_id,
+ name: dbData_Clients[1],
+ status: dbData_Clients[3],
flag: Flag.WARNING,
last_use: testTimestamp,
last_modified: testTimestamp,
@@ -168,7 +175,7 @@ describe('Integration-tests (repository), priority: entity Clients', () => {
jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp);
- const testResult: ClientsFlagResponseDTO | null = null;
+ const testResult: ClientsResponseDTO | null = null;
await dbTestSetup.addTestData();
const testResponse = await clientsService.updateClientFlag(testParam_id, testParam_dto);
@@ -184,10 +191,11 @@ describe('Integration-tests (repository), priority: entity Clients', () => {
jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp);
- const testResult: ClientsStatusResponseDTO = {
+ const testResult: ClientsResponseDTO = {
client_id: testParam_id,
- name: 'TESTCLIENT',
+ name: dbData_Clients[1],
status: ApiKeyStatus.DISABLED,
+ flag: dbData_Clients[4],
last_use: testTimestamp,
last_modified: testTimestamp,
created_on: testTimestamp
diff --git a/backend/tests/integration-tests/repositories/feedback.integration.test.ts b/backend/tests/integration-tests/repositories/feedback.integration.test.ts
index b12b074..b292274 100644
--- a/backend/tests/integration-tests/repositories/feedback.integration.test.ts
+++ b/backend/tests/integration-tests/repositories/feedback.integration.test.ts
@@ -8,7 +8,7 @@ import { default as mockId } from "../../mock-data/id.mock-data.json";
import { NextFunction, Request, Response } from "express";
import { CommonExceptionMessage } from "../../../src/utils/enums/common-exception-messages.enum";
import { ErrorStatusCodes } from "../../../src/utils/errorStatusCodes.utils";
-import { FeedbackCreateDTO, FeedbackFilterDTO, FeedbackRequestCreateDTO, FeedbackResponseDTO } from '../../../src/dtos/feedback.dto';
+import { FeedbackCreateDTO, FeedbackCreateResponseDTO, FeedbackFilterDTO, FeedbackRequestCreateDTO, FeedbackResponseDTO } from '../../../src/dtos/feedback.dto';
import { DBTestData } from '../../db-data.setup';
import router from '../../../src/routes/feedback.route';
import feedbackRatingService from '../../../src/services/feedback-rating.service';
@@ -126,14 +126,9 @@ describe('Integration-tests (repository), priority: entity Feedback', () => {
jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp);
- const testResult: FeedbackResponseDTO = {
- feedback_id: mockId.feedback.new[0] as FeedbackId,
- client_id: testValidClientId,
- user_id: mockId.users.valid[1] as UsersId,
+ const testResult: FeedbackCreateResponseDTO = {
rating: testParam_dto.rating,
rating_average_new: mockResult_average_rating,
- term_accepted: testParam_dto.term_accepted,
- last_modified: testTimestamp,
created_on: testTimestamp
};
@@ -161,14 +156,9 @@ describe('Integration-tests (repository), priority: entity Feedback', () => {
jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp);
- const testResult: FeedbackResponseDTO = {
- feedback_id: mockId.feedback.new[0] as FeedbackId,
- client_id: mockId.clients.valid[1] as ClientsId,
- user_id: testValidUserId,
+ const testResult: FeedbackCreateResponseDTO = {
rating: testParam_dto.rating,
rating_average_new: testParam_dto.rating,
- term_accepted: testParam_dto.term_accepted,
- last_modified: testTimestamp,
created_on: testTimestamp
};
@@ -205,15 +195,11 @@ describe('Integration-tests (repository), priority: entity Feedback', () => {
jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp_update);
- const testResult: FeedbackResponseDTO = {
- feedback_id: testValidFeedbackId,
- client_id: testValidClientId,
- user_id: testValidUserId,
+ const testResult: FeedbackCreateResponseDTO = {
rating: testParam_dto.rating,
+ rating_old: dbData_Feedback[2],
rating_average_new: mockResult_average_rating,
- term_accepted: testParam_dto.term_accepted,
- last_modified: testTimestamp_update,
- created_on: testTimestamp,
+ created_on: testTimestamp_update,
};
await dbTestSetup.addTestData();
diff --git a/backend/tests/integration-tests/repositories/tickets.integration.test.ts b/backend/tests/integration-tests/repositories/tickets.integration.test.ts
index 2a1119d..16f0ec2 100644
--- a/backend/tests/integration-tests/repositories/tickets.integration.test.ts
+++ b/backend/tests/integration-tests/repositories/tickets.integration.test.ts
@@ -1,5 +1,6 @@
import { NextFunction, Request, Response } from "express";
import {
+ TicketsCreateResponseDTO,
TicketsFilterDTO,
TicketsRequestCreateDTO,
TicketsResponseDTO,
@@ -24,6 +25,7 @@ import { DeviceOption } from "../../../src/utils/enums/device-option.enum";
import { UsersId } from "../../../src/repositories/interfaces/users.entity.interface";
import { ClientsId } from "../../../src/repositories/interfaces/clients.entity.interface";
import { TicketsId } from "../../../src/repositories/interfaces/tickets.entity.interface";
+import ticketsService from "../../../src/services/tickets.service";
const testValidTicketId = mockId.tickets.valid[0] as TicketsId;
const testValidClientId = mockId.clients.valid[0] as ClientsId;
@@ -45,6 +47,9 @@ jest.mock('../../../src/middleware/auth.user.middleware', () => ({
next();
})
}));
+jest.mock('../../../src/middleware/parser/form-data.parser.middleware.ts', () => ({
+ parseFormData: jest.fn(() => (req: Request, res: Response, next: NextFunction) => next())
+}))
jest.mock('../../../src/middleware/maintenance.middleware', () => ({
maintain: jest.fn(() => (req: Request, res: Response, next: NextFunction) => next())
}));
@@ -395,16 +400,10 @@ describe('Integration-tests (repository), priority: entity Tickets', () => {
jest.spyOn(CommonUtils, "generateUUID").mockReturnValue(testNewParam_ticket_id);
jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp);
- const testResult: TicketsResponseDTO = {
- ticket_id: testNewParam_ticket_id,
- client_id: testValidClientId,
- user_id: testValidUserId,
+ const testResult: TicketsCreateResponseDTO = {
status: TicketStatus.ISSUED,
option: TicketOption.SUPPORT,
- title: testParam_dto.title,
- message: testParam_dto.message,
flag: null,
- last_modified: testTimestamp,
created_on: testTimestamp
};
@@ -414,7 +413,7 @@ describe('Integration-tests (repository), priority: entity Tickets', () => {
.send(testParam_dto);
expect(testResponse.statusCode).toBe(200);
- expect(testResponse.body).toMatchObject(testResult)
+ expect(testResponse.body).toMatchObject(testResult);
})
test('Repository process fn create(), priority: with single file, result: "SUCCESS"', async () => {
@@ -436,17 +435,10 @@ describe('Integration-tests (repository), priority: entity Tickets', () => {
jest.spyOn(FilesService.prototype, 'uploadFiles').mockImplementation();
jest.spyOn(FilesService.prototype, 'getResourcePaths').mockReturnValue(mockPaths);
- const testResult: TicketsResponseDTO = {
- ticket_id: testNewParam_ticket_id,
- client_id: testValidClientId,
- user_id: testValidUserId,
+ const testResult: TicketsCreateResponseDTO = {
status: TicketStatus.ISSUED,
option: TicketOption.SUPPORT,
- title: testParam_dto.title,
- message: testParam_dto.message,
- resource_paths: mockPaths,
flag: null,
- last_modified: testTimestamp,
created_on: testTimestamp
};
@@ -462,8 +454,11 @@ describe('Integration-tests (repository), priority: entity Tickets', () => {
.field('title', testParam_dto.title)
.field('message', testParam_dto.message);
+ const testFindByIdResponse = await ticketsService.getTicketById(testNewParam_ticket_id);
+
expect(testResponse.statusCode).toBe(200);
- expect(testResponse.body).toMatchObject(testResult)
+ expect(testResponse.body).toMatchObject(testResult);
+ expect(testFindByIdResponse?.resource_paths).toEqual(mockPaths);
})
test('Repository process fn create(), priority: with multiple files, result: "SUCCESS"', async () => {
@@ -494,17 +489,10 @@ describe('Integration-tests (repository), priority: entity Tickets', () => {
jest.spyOn(FilesService.prototype, 'uploadFiles').mockImplementation();
jest.spyOn(FilesService.prototype, 'getResourcePaths').mockReturnValue(mockPaths);
- const testResult: TicketsResponseDTO = {
- ticket_id: testNewParam_ticket_id,
- client_id: testValidClientId,
- user_id: testValidUserId,
+ const testResult: TicketsCreateResponseDTO = {
status: TicketStatus.ISSUED,
option: TicketOption.SUPPORT,
- title: testParam_dto.title,
- message: testParam_dto.message,
- resource_paths: mockPaths,
flag: null,
- last_modified: testTimestamp,
created_on: testTimestamp
};
@@ -519,8 +507,11 @@ describe('Integration-tests (repository), priority: entity Tickets', () => {
.field('title', testParam_dto.title)
.field('message', testParam_dto.message);
+ const testFindByIdResponse = await ticketsService.getTicketById(testNewParam_ticket_id);
+
expect(testResponse.statusCode).toBe(200);
- expect(testResponse.body).toMatchObject(testResult)
+ expect(testResponse.body).toMatchObject(testResult);
+ expect(testFindByIdResponse?.resource_paths).toEqual(mockPaths);
})
test('Repository process fn update() without resource_paths, result: "SUCCESS"', async () => {
diff --git a/backend/tests/unit-tests/middleware/files/parse.files.middleware.test.ts b/backend/tests/unit-tests/middleware/parser/files.parser.middleware.test.ts
similarity index 95%
rename from backend/tests/unit-tests/middleware/files/parse.files.middleware.test.ts
rename to backend/tests/unit-tests/middleware/parser/files.parser.middleware.test.ts
index 53ed8ff..d28336f 100644
--- a/backend/tests/unit-tests/middleware/files/parse.files.middleware.test.ts
+++ b/backend/tests/unit-tests/middleware/parser/files.parser.middleware.test.ts
@@ -1,6 +1,6 @@
import multer from 'multer';
import * as CommonUtils from '../../../../src/utils/common.utils';
-import { parseFiles } from '../../../../src/middleware/files/parse.files.middleware';
+import { parseFiles } from '../../../../src/middleware/parser/files.parser.middleware';
jest.mock('multer');
// Tests including multer parsing mocked files => tickets.integration.test.ts
diff --git a/backend/tests/unit-tests/middleware/parser/form-data.parser.middleware.test.ts b/backend/tests/unit-tests/middleware/parser/form-data.parser.middleware.test.ts
new file mode 100644
index 0000000..5a96adb
--- /dev/null
+++ b/backend/tests/unit-tests/middleware/parser/form-data.parser.middleware.test.ts
@@ -0,0 +1,44 @@
+import * as CommonUtils from '../../../../src/utils/common.utils';
+import { parseFormData } from '../../../../src/middleware/parser/form-data.parser.middleware';
+
+describe('Unit-tests (middleware), priority: fn parseFormData()', () => {
+
+ const mockBody = {user_email: 'test@demo.com', option: 'support'};
+ let req: any;
+ const res: any = {};
+ const next = jest.fn();
+ beforeEach(() => {
+ req = { body: { data: JSON.stringify(mockBody)}};
+ jest.clearAllMocks();
+ })
+ afterEach(() => {
+ jest.restoreAllMocks();
+ })
+
+ describe('Testing valid fn calls', () => {
+
+ test('Mock form-data parsing into req.body, call next()', () => {
+ const middleware = parseFormData();
+ middleware(req, res, next);
+
+ expect(next).toHaveBeenCalledWith();
+ expect(req.body).toMatchObject(mockBody);
+ })
+ })
+
+ describe('Testing invalid fn calls', () => {
+
+ test('Mock file-parsing of data into req.files, call next(err)', () => {
+ jest.spyOn(CommonUtils, 'logError').mockImplementation();
+ const errorMsg = 'parsing failed';
+ JSON.parse = jest.fn().mockImplementationOnce(() => {throw new Error(errorMsg)})
+
+ const middleware = parseFormData();
+ middleware(req, res, next);
+
+ expect(next).toHaveBeenCalledWith(
+ expect.objectContaining({ message: errorMsg })
+ );
+ })
+ })
+})
\ No newline at end of file
diff --git a/backend/tests/unit-tests/models/clients.models.test.ts b/backend/tests/unit-tests/models/clients.models.test.ts
index 0abe852..c11c30e 100644
--- a/backend/tests/unit-tests/models/clients.models.test.ts
+++ b/backend/tests/unit-tests/models/clients.models.test.ts
@@ -1,6 +1,8 @@
import {
ClientsCreateDTO,
ClientsCreateResponseDTO,
+ ClientsExtendedResponseDTO,
+ ClientsResponseDTO,
} from "../../../src/dtos/clients.dto";
import * as CommonUtils from "../../../src/utils/common.utils";
import { default as mockId } from "../../mock-data/id.mock-data.json";
@@ -48,6 +50,51 @@ describe('Unit-tests (model), priority: entity Clients', () => {
})
})
+ describe('Priority: fn toClientsResponseDTO()', () => {
+
+ describe('Testing valid fn calls', () => {
+
+ test('Convert entity to dto, params: Clients, false', () => {
+ const mockParam_entity: Clients = structuredClone(mockData);
+ const mockParam_extended = false;
+
+ jest.spyOn(CommonUtils, 'getTimestampUTC').mockReturnValue(mockTimestamp);
+ const expectResult: ClientsResponseDTO = {
+ client_id: mockParam_entity.client_id,
+ name: mockParam_entity.name,
+ status: mockParam_entity.status,
+ flag: mockParam_entity.flag,
+ last_use: mockTimestamp,
+ last_modified: mockTimestamp,
+ created_on: mockTimestamp
+ };
+ const testFn = clientsModel.toClientsResponseDTO(mockParam_entity, mockParam_extended);
+
+ expect(testFn).toMatchObject(expectResult);
+ })
+
+ test('Convert entity to dto, params: Clients, true', () => {
+ const mockParam_entity: Clients = structuredClone(mockData);
+ const mockParam_extended = true;
+
+ jest.spyOn(CommonUtils, 'getTimestampUTC').mockReturnValue(mockTimestamp);
+ const expectResult: ClientsExtendedResponseDTO = {
+ client_id: mockParam_entity.client_id,
+ name: mockParam_entity.name,
+ api_key_hash: mockParam_entity.api_key_hash,
+ status: mockParam_entity.status,
+ flag: mockParam_entity.flag,
+ last_use: mockTimestamp,
+ last_modified: mockTimestamp,
+ created_on: mockTimestamp
+ };
+ const testFn = clientsModel.toClientsResponseDTO(mockParam_entity, mockParam_extended);
+
+ expect(testFn).toMatchObject(expectResult);
+ })
+ })
+ })
+
describe('Priority: fn generateClientsCreateObj()', () => {
describe('Testing valid fn calls', () => {
@@ -80,5 +127,4 @@ describe('Unit-tests (model), priority: entity Clients', () => {
})
})
})
-})
-
+})
\ No newline at end of file
diff --git a/backend/tests/unit-tests/models/tickets.model.test.ts b/backend/tests/unit-tests/models/tickets.model.test.ts
index 1e5cee7..8ef2c42 100644
--- a/backend/tests/unit-tests/models/tickets.model.test.ts
+++ b/backend/tests/unit-tests/models/tickets.model.test.ts
@@ -1,4 +1,4 @@
-import { TicketsCreateDTO, TicketsResponseDTO, TicketsUpdateDTO } from "../../../src/dtos/tickets.dto";
+import { TicketsCreateDTO, TicketsCreateResponseDTO, TicketsResponseDTO, TicketsUpdateDTO } from "../../../src/dtos/tickets.dto";
import * as CommonUtils from "../../../src/utils/common.utils";
import * as mockId from "../../mock-data/id.mock-data.json";
import { TicketStatus } from "../../../src/utils/enums/ticket-status.enum";
@@ -189,6 +189,38 @@ describe('Unit-tests (model), priority: entity Tickets', () => {
})
})
+ describe('Priority: fn toTicketsCreateResponseDTO()', () => {
+
+ describe('Testing valid fn calls', () => {
+
+ test('Convert entity to dto, params: Clients, false', () => {
+ const mockParam_entity: Tickets = {
+ ticket_id: mockValidTicketId,
+ client_id: mockValidClientId,
+ user_id: mockValidUserId,
+ status: TicketStatus.ISSUED,
+ option: TicketOption.BUG,
+ title: 'test-title',
+ message: 'test-message',
+ flag: null,
+ last_modified: mockTimestamp,
+ created_on: mockTimestamp
+ };
+
+ jest.spyOn(CommonUtils, 'getTimestampUTC').mockReturnValue(mockTimestamp);
+ const expectResult: TicketsCreateResponseDTO = {
+ status: mockParam_entity.status,
+ option: mockParam_entity.option,
+ flag: mockParam_entity.flag,
+ created_on: mockTimestamp
+ };
+ const testFn = ticketsModel.toTicketsCreateResponseDTO(mockParam_entity);
+
+ expect(testFn).toMatchObject(expectResult);
+ })
+ })
+ })
+
describe('Priority: fn handleTicketBeforeDelete()', () => {
describe('Testing valid fn calls', () => {
diff --git a/backend/tests/unit-tests/repositories/clients.repositories.test.ts b/backend/tests/unit-tests/repositories/clients.repositories.test.ts
index 229e8ef..e20c0a9 100644
--- a/backend/tests/unit-tests/repositories/clients.repositories.test.ts
+++ b/backend/tests/unit-tests/repositories/clients.repositories.test.ts
@@ -1,10 +1,8 @@
import {
- ClientsStatusResponseDTO,
- ClientsLastUseResponseDTO,
ClientsStatusUpdateDTO,
ClientsLastUseUpdateDTO,
ClientsFlagUpdateDTO,
- ClientsFlagResponseDTO
+ ClientsResponseDTO,
} from "../../../src/dtos/clients.dto";
import { DBConnection } from "../../../src/configs/db";
import * as CommonUtils from "../../../src/utils/common.utils";
@@ -160,10 +158,11 @@ describe('Unit-tests (repository), priority: entity Clients', () => {
test('Return data for existing entry, params: ', async () => {
const mockParam_name = 'existing_clients_test_name';
- const mockResult: ClientsStatusResponseDTO = {
+ const mockResult: ClientsResponseDTO = {
client_id: mockValidClientId,
name: mockParam_name,
status: ApiKeyStatus.ACTIVE,
+ flag: null,
last_use: mockTimestamp,
last_modified: mockTimestamp,
created_on: mockTimestamp
@@ -278,10 +277,11 @@ describe('Unit-tests (repository), priority: entity Clients', () => {
const mockParam_id = mockValidClientId;
const mockValues = [mockParam_dto.status, mockTimestamp, mockParam_id];
- const mockResult: ClientsStatusResponseDTO = {
+ const mockResult: ClientsResponseDTO = {
client_id: mockParam_id,
name: mockParam_name,
status: mockParam_dto.status,
+ flag: null,
last_use: mockTimestamp,
last_modified: mockTimestamp,
created_on: mockTimestamp
@@ -344,8 +344,10 @@ describe('Unit-tests (repository), priority: entity Clients', () => {
const mockParam_id = mockValidClientId;
const mockValues = [mockParam_dto.flag, mockTimestamp, mockParam_id];
- const mockResult: ClientsFlagResponseDTO | null = {
+ const mockResult: ClientsResponseDTO | null = {
client_id: mockParam_id,
+ name: 'TESTCLIENT',
+ status: ApiKeyStatus.ACTIVE,
flag: Flag.WARNING,
last_use: mockTimestamp,
last_modified: mockTimestamp,
@@ -366,7 +368,7 @@ describe('Unit-tests (repository), priority: entity Clients', () => {
const mockParam_id = mockId.clients.invalid[0] as ClientsId;
const mockValues = [mockParam_dto.flag, mockTimestamp, mockParam_id];
- const mockResult: ClientsFlagResponseDTO | null = null;
+ const mockResult: ClientsResponseDTO | null = null;
const mockClient = MockUtils.mapMockDbClient(mockResult);
const testFn = await clientsRepository.updateFlag(mockParam_id, mockParam_dto);
@@ -384,7 +386,7 @@ describe('Unit-tests (repository), priority: entity Clients', () => {
test('Throw DBQueryErrorException by catch-block', async () => {
const mockParam_id = mockId.clients.invalid[0] as ClientsId;
const mockErrorMsg = "DB ERROR ON UPDATE QUERY";
- const mockResult: ClientsFlagResponseDTO | null = null;
+ const mockResult: ClientsResponseDTO | null = null;
jest.spyOn(CommonUtils, "logError").mockReturnValue();
const _ = MockUtils.mapMockDbClient(mockResult, mockBoolean, mockErrorMsg);
@@ -413,9 +415,11 @@ describe('Unit-tests (repository), priority: entity Clients', () => {
jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(mockTimestamp);
- const mockResult: ClientsLastUseResponseDTO = {
+ const mockResult: ClientsResponseDTO = {
client_id: mockParam_id,
name: mockParam_name,
+ status: ApiKeyStatus.ACTIVE,
+ flag: null,
last_use: mockTimestamp,
last_modified: mockTimestamp,
created_on: mockTimestamp
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 4d6e52d..db087fe 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -1,12 +1,22 @@
-# yqni13 | support
+## ๐ $\textsf{\color{salmon}{List\ of\ updates}}$
-### $\texttt{\color{olive}{LIST\ OF\ UPDATES}}$
+
+### $\textsf{\color{skyblue}2026/03/14}$
+
+$\textsf{[v1.4.4\ =>\ {\textbf{\color{brown}v1.4.9}]}}$ app
+- $\textsf{\color{teal}Addition:}$ Added form-data parser middleware for requests including files.
+- $\textsf{\color{orange}Patch:}$ Updated:
+ + return types, mapping and handling (part 1).
+ + calculation for delete-permission by comparing timestamps (incorporate timezone offset on database read timestamp).
+ + 'tickets' request: more accurate check for containing files.
+ + use Promise-instance fn finally() in repository-layer to reduce code.
+ + documentation headers and display error on symbol (&).
### $\textsf{\color{skyblue}2026/03/09}$
-$\textsf{[v1.4.1\ =>\ {\textbf{\color{brown}v1.4.4}]}}$ app
+$\textsf{[v1.4.1\ =>\ v1.4.4]}$ app
- $\textsf{\color{orange}Patch:}$ Updated:
+ entity ID's are using now nominal types instead basic string|number.
+ some model functions are renamed to keep consistency and improve readability.