Skip to content

Commit a186cda

Browse files
Merge branch 'develop' of https://github.com/Suvidh-kaushik/website-backend into test/unit_tests_for_update_impersonation_requests
2 parents a605c87 + 798794d commit a186cda

File tree

8 files changed

+869
-36
lines changed

8 files changed

+869
-36
lines changed

controllers/impersonationRequests.ts

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import {
22
ERROR_WHILE_CREATING_REQUEST,
3-
FEATURE_NOT_IMPLEMENTED,
4-
REQUEST_CREATED_SUCCESSFULLY
3+
ERROR_WHILE_FETCHING_REQUEST,
4+
REQUEST_FETCHED_SUCCESSFULLY,
5+
REQUEST_CREATED_SUCCESSFULLY,
6+
REQUEST_DOES_NOT_EXIST
57
} from "../constants/requests";
68
import { createImpersonationRequestService } from "../services/impersonationRequests";
9+
import { getImpersonationRequestById, getImpersonationRequests } from "../models/impersonationRequests";
710
import {
811
CreateImpersonationRequest,
912
CreateImpersonationRequestBody,
10-
ImpersonationRequestResponse
13+
ImpersonationRequestResponse,
14+
GetImpersonationControllerRequest,
15+
GetImpersonationRequestByIdRequest
1116
} from "../types/impersonationRequest";
17+
import { getPaginatedLink } from "../utils/helper";
1218
import { NextFunction } from "express";
1319
const logger = require("../utils/logger");
1420

@@ -18,7 +24,7 @@ const logger = require("../utils/logger");
1824
* @param {CreateImpersonationRequest} req - Express request object with user and body data.
1925
* @param {ImpersonationRequestResponse} res - Express response object.
2026
* @param {NextFunction} next - Express next middleware function.
21-
* @returns {Promise<ImpersonationRequestResponse | void>}
27+
* @returns {Promise<ImpersonationRequestResponse | void>} Returns the created request or passes error to next middleware.
2228
*/
2329
export const createImpersonationRequestController = async (
2430
req: CreateImpersonationRequest,
@@ -47,4 +53,90 @@ export const createImpersonationRequestController = async (
4753
logger.error(ERROR_WHILE_CREATING_REQUEST, error);
4854
next(error);
4955
}
50-
};
56+
};
57+
58+
/**
59+
* Controller to fetch an impersonation request by its ID.
60+
*
61+
* @param {GetImpersonationRequestByIdRequest} req - Express request object containing `id` parameter.
62+
* @param {ImpersonationRequestResponse} res - Express response object.
63+
* @returns {Promise<ImpersonationRequestResponse>} Returns the request if found, or 404 if it doesn't exist.
64+
*/
65+
export const getImpersonationRequestByIdController = async (
66+
req: GetImpersonationRequestByIdRequest,
67+
res: ImpersonationRequestResponse
68+
): Promise<ImpersonationRequestResponse> => {
69+
const id = req.params.id;
70+
try {
71+
const request = await getImpersonationRequestById(id);
72+
73+
if (!request) {
74+
return res.status(404).json({
75+
message: REQUEST_DOES_NOT_EXIST,
76+
});
77+
}
78+
79+
return res.status(200).json({
80+
message: REQUEST_FETCHED_SUCCESSFULLY,
81+
data: request,
82+
});
83+
84+
} catch (error) {
85+
logger.error(ERROR_WHILE_FETCHING_REQUEST, error);
86+
return res.boom.badImplementation(ERROR_WHILE_FETCHING_REQUEST);
87+
}
88+
};
89+
90+
/**
91+
* Controller to fetch impersonation requests with optional filtering and pagination.
92+
*
93+
* @param {GetImpersonationControllerRequest} req - Express request object containing query parameters.
94+
* @param {ImpersonationRequestResponse} res - Express response object.
95+
* @returns {Promise<ImpersonationRequestResponse>} Returns paginated impersonation request data or 204 if none found.
96+
*/
97+
export const getImpersonationRequestsController = async (
98+
req: GetImpersonationControllerRequest,
99+
res: ImpersonationRequestResponse
100+
): Promise<ImpersonationRequestResponse> => {
101+
try {
102+
const { query } = req;
103+
104+
const requests = await getImpersonationRequests(query);
105+
if (!requests || requests.allRequests.length === 0) {
106+
return res.status(204).send();
107+
}
108+
109+
const { allRequests, next, prev } = requests;
110+
const count = allRequests.length;
111+
112+
let nextUrl = null;
113+
let prevUrl = null;
114+
if (next) {
115+
nextUrl = getPaginatedLink({
116+
endpoint: "/impersonation/requests",
117+
query,
118+
cursorKey: "next",
119+
docId: next,
120+
});
121+
}
122+
if (prev) {
123+
prevUrl = getPaginatedLink({
124+
endpoint: "/impersonation/requests",
125+
query,
126+
cursorKey: "prev",
127+
docId: prev,
128+
});
129+
}
130+
131+
return res.status(200).json({
132+
message: REQUEST_FETCHED_SUCCESSFULLY,
133+
data: allRequests,
134+
next: nextUrl,
135+
prev: prevUrl,
136+
count,
137+
});
138+
} catch (err) {
139+
logger.error(ERROR_WHILE_FETCHING_REQUEST, err);
140+
return res.boom.badImplementation(ERROR_WHILE_FETCHING_REQUEST);
141+
}
142+
};

middlewares/validators/impersonationRequests.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import joi from "joi";
22
import { NextFunction } from "express";
3-
import { CreateImpersonationRequest, ImpersonationRequestResponse } from "../../types/impersonationRequest";
3+
import { CreateImpersonationRequest,GetImpersonationControllerRequest,GetImpersonationRequestByIdRequest,ImpersonationRequestResponse } from "../../types/impersonationRequest";
4+
import { REQUEST_STATE } from "../../constants/requests";
45
const logger = require("../../utils/logger");
56

67
/**
@@ -34,4 +35,67 @@ export const createImpersonationRequestValidator = async (
3435
logger.error(`Error while validating request payload : ${errorMessages}`);
3536
return res.boom.badRequest(errorMessages);
3637
}
37-
};
38+
};
39+
40+
/**
41+
* Middleware to validate query parameters for fetching impersonation requests.
42+
*
43+
* @param {GetImpersonationControllerRequest} req - Express request object.
44+
* @param {ImpersonationRequestResponse} res - Express response object.
45+
* @param {NextFunction} next - Express next middleware function.
46+
*/
47+
export const getImpersonationRequestsValidator = async (
48+
req: GetImpersonationControllerRequest,
49+
res: ImpersonationRequestResponse,
50+
next: NextFunction
51+
) => {
52+
const schema = joi.object().keys({
53+
dev: joi.string().optional(), // TODO: Remove this validator once feature is tested and ready to be used
54+
createdBy: joi.string().insensitive().optional(),
55+
createdFor: joi.string().insensitive().optional(),
56+
status: joi
57+
.string()
58+
.valid(REQUEST_STATE.APPROVED, REQUEST_STATE.PENDING, REQUEST_STATE.REJECTED)
59+
.optional(),
60+
next: joi.string().optional(),
61+
prev: joi.string().optional(),
62+
size: joi.number().integer().positive().min(1).max(100).optional(),
63+
});
64+
65+
try {
66+
await schema.validateAsync(req.query, { abortEarly: false });
67+
next();
68+
} catch ( error ) {
69+
const errorMessages = error.details.map((detail: { message: string }) => detail.message);
70+
logger.error(`Error while validating request payload : ${errorMessages}`);
71+
return res.boom.badRequest(errorMessages);
72+
}
73+
};
74+
75+
/**
76+
* Middleware to validate route parameters for fetching an impersonation request by ID.
77+
*
78+
* @param {GetImpersonationRequestByIdRequest} req - Express request object containing route params.
79+
* @param {ImpersonationRequestResponse} res - Express response object.
80+
* @param {NextFunction} next - Express next middleware function.
81+
* @returns {Promise<void>} Resolves and calls `next()` if validation passes, otherwise sends a badRequest response.
82+
*/
83+
export const getImpersonationRequestByIdValidator = async (
84+
req: GetImpersonationRequestByIdRequest,
85+
res: ImpersonationRequestResponse,
86+
next: NextFunction
87+
): Promise<void> => {
88+
const schema = joi.object().keys({
89+
dev: joi.string().optional(),
90+
id: joi.string().max(100).pattern(/^[a-zA-Z0-9-_]+$/).required(),
91+
});
92+
93+
try {
94+
await schema.validateAsync(req.params, { abortEarly: false });
95+
next();
96+
} catch (error) {
97+
const errorMessages = error.details.map((detail: { message: string }) => detail.message);
98+
logger.error(`Error while validating request payload : ${errorMessages}`);
99+
return res.boom.badRequest(errorMessages);
100+
}
101+
};

models/impersonationRequests.ts

Lines changed: 116 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import firestore from "../utils/firestore";
22
import {
33
ERROR_WHILE_CREATING_REQUEST,
4-
IMPERSONATION_NOT_COMPLETED,
5-
REQUEST_ALREADY_PENDING,
6-
REQUEST_STATE
4+
ERROR_WHILE_FETCHING_REQUEST
75
} from "../constants/requests";
86
import { Timestamp } from "firebase-admin/firestore";
9-
import { CreateImpersonationRequestModelDto, ImpersonationRequest } from "../types/impersonationRequest";
7+
import { Query, CollectionReference } from '@google-cloud/firestore';
8+
import { CreateImpersonationRequestModelDto, ImpersonationRequest, PaginatedImpersonationRequests,ImpersonationRequestQuery} from "../types/impersonationRequest";
109
import { Forbidden } from "http-errors";
1110
const logger = require("../utils/logger");
12-
1311
const impersonationRequestModel = firestore.collection("impersonationRequests");
12+
const DEFAULT_PAGE_SIZE = 5;
1413

1514
/**
1615
* Creates a new impersonation request in Firestore.
@@ -55,4 +54,115 @@ export const createImpersonationRequest = async (
5554
logger.error(ERROR_WHILE_CREATING_REQUEST, { error, requestData: body });
5655
throw error;
5756
}
58-
};
57+
};
58+
59+
/**
60+
* Retrieves an impersonation request by its ID.
61+
* @param {string} id - The ID of the impersonation request to retrieve.
62+
* @returns {Promise<ImpersonationRequest|null>} The found impersonation request or null if not found.
63+
* @throws {Error} Logs and rethrows any error encountered during fetch.
64+
*/
65+
export const getImpersonationRequestById = async (
66+
id: string
67+
): Promise<ImpersonationRequest | null> => {
68+
try {
69+
const requestDoc = await impersonationRequestModel.doc(id).get();
70+
if (!requestDoc.exists) {
71+
return null;
72+
}
73+
const data = requestDoc.data() as ImpersonationRequest;
74+
return {
75+
id: requestDoc.id,
76+
...data,
77+
};
78+
} catch (error) {
79+
logger.error(`${ERROR_WHILE_FETCHING_REQUEST} for ID: ${id}`, error);
80+
throw error;
81+
}
82+
};
83+
84+
/**
85+
* Retrieves a paginated list of impersonation requests based on query filters.
86+
* @param {object} query - The query filters.
87+
* @param {string} [query.createdBy] - Filter by the username of the request creator.
88+
* @param {string} [query.createdFor] - Filter by the username of the user the request is created for.
89+
* @param {string} [query.status] - Filter by request status (e.g., "APPROVED", "PENDING", "REJECTED").
90+
* @param {string} [query.prev] - Document ID to use as the ending point for backward pagination.
91+
* @param {string} [query.next] - Document ID to use as the starting point for forward pagination.
92+
* @param {string} [query.size] - Number of results per page.
93+
* @returns {Promise<PaginatedImpersonationRequests|null>} The paginated impersonation requests or null if none found.
94+
* @throws Logs and rethrows any error encountered during fetch.
95+
*/
96+
export const getImpersonationRequests = async (
97+
query
98+
): Promise<PaginatedImpersonationRequests | null> => {
99+
100+
let { createdBy, createdFor, status, prev, next, size = DEFAULT_PAGE_SIZE } = query;
101+
102+
size = size ? Number.parseInt(size) : DEFAULT_PAGE_SIZE;
103+
104+
105+
try {
106+
let requestQuery: Query<ImpersonationRequest> = impersonationRequestModel as CollectionReference<ImpersonationRequest>;
107+
108+
if (createdBy) {
109+
requestQuery = requestQuery.where("createdBy", "==", createdBy);
110+
}
111+
if (status) {
112+
requestQuery = requestQuery.where("status", "==", status);
113+
}
114+
if (createdFor) {
115+
requestQuery = requestQuery.where("createdFor", "==", createdFor);
116+
}
117+
118+
requestQuery = requestQuery.orderBy("createdAt", "desc");
119+
let requestQueryDoc = requestQuery;
120+
121+
if (prev) {
122+
requestQueryDoc = requestQueryDoc.limitToLast(size);
123+
} else {
124+
requestQueryDoc = requestQueryDoc.limit(size);
125+
}
126+
127+
if (next) {
128+
const doc = await impersonationRequestModel.doc(next).get();
129+
requestQueryDoc = requestQueryDoc.startAt(doc);
130+
} else if (prev) {
131+
const doc = await impersonationRequestModel.doc(prev).get();
132+
requestQueryDoc = requestQueryDoc.endAt(doc);
133+
}
134+
135+
const snapshot = await requestQueryDoc.get();
136+
let nextDoc;
137+
let prevDoc;
138+
139+
if (!snapshot.empty) {
140+
const first = snapshot.docs[0];
141+
prevDoc = await requestQuery.endBefore(first).limitToLast(1).get();
142+
const last = snapshot.docs[snapshot.docs.length - 1];
143+
nextDoc = await requestQuery.startAfter(last).limit(1).get();
144+
}
145+
146+
const allRequests = snapshot.empty
147+
? []
148+
: snapshot.docs.map(doc => ({
149+
id: doc.id,
150+
...doc.data()
151+
}));
152+
153+
if (allRequests.length === 0) {
154+
return null;
155+
}
156+
157+
const count = allRequests.length;
158+
return {
159+
allRequests,
160+
prev: prevDoc && !prevDoc.empty ? prevDoc.docs[0].id : null,
161+
next: nextDoc && !nextDoc.empty ? nextDoc.docs[0].id : null,
162+
count,
163+
};
164+
} catch (error) {
165+
logger.error(ERROR_WHILE_FETCHING_REQUEST, error);
166+
throw error;
167+
}
168+
}

routes/impersonation.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import express from "express";
2-
import { createImpersonationRequestValidator } from "../middlewares/validators/impersonationRequests";
2+
import { createImpersonationRequestValidator, getImpersonationRequestByIdValidator, getImpersonationRequestsValidator } from "../middlewares/validators/impersonationRequests";
33
const router = express.Router();
44
const authorizeRoles = require("../middlewares/authorizeRoles");
55
const { SUPERUSER } = require("../constants/roles");
66
import authenticate from "../middlewares/authenticate";
7-
import { createImpersonationRequestController } from "../controllers/impersonationRequests";
7+
import { createImpersonationRequestController, getImpersonationRequestByIdController, getImpersonationRequestsController } from "../controllers/impersonationRequests";
88

99
router.post(
1010
"/requests",
@@ -14,4 +14,18 @@ router.post(
1414
createImpersonationRequestController
1515
);
1616

17+
router.get(
18+
"/requests",
19+
authenticate,
20+
getImpersonationRequestsValidator,
21+
getImpersonationRequestsController
22+
);
23+
24+
router.get(
25+
"/requests/:id",
26+
authenticate,
27+
getImpersonationRequestByIdValidator,
28+
getImpersonationRequestByIdController
29+
);
30+
1731
module.exports = router;

0 commit comments

Comments
 (0)