@internetderdinge/api 1.224.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +77 -0
- package/CHANGELOG.md +11 -0
- package/README.md +52 -0
- package/package.json +112 -0
- package/src/accounts/accounts.controller.ts +166 -0
- package/src/accounts/accounts.route.ts +107 -0
- package/src/accounts/accounts.schemas.ts +16 -0
- package/src/accounts/accounts.service.ts +85 -0
- package/src/accounts/accounts.validation.ts +118 -0
- package/src/accounts/auth0.service.ts +226 -0
- package/src/config/config.ts +49 -0
- package/src/config/logger.ts +33 -0
- package/src/config/morgan.ts +22 -0
- package/src/config/passport.cjs +30 -0
- package/src/config/roles.ts +13 -0
- package/src/config/tokens.cjs +10 -0
- package/src/devices/devices.controller.ts +276 -0
- package/src/devices/devices.model.ts +126 -0
- package/src/devices/devices.route.ts +198 -0
- package/src/devices/devices.schemas.ts +94 -0
- package/src/devices/devices.service.ts +320 -0
- package/src/devices/devices.validation.ts +221 -0
- package/src/devicesNotifications/devicesNotifications.controller.ts +72 -0
- package/src/devicesNotifications/devicesNotifications.model.ts +67 -0
- package/src/devicesNotifications/devicesNotifications.route.ts +150 -0
- package/src/devicesNotifications/devicesNotifications.schemas.ts +11 -0
- package/src/devicesNotifications/devicesNotifications.service.ts +222 -0
- package/src/devicesNotifications/devicesNotifications.validation.ts +56 -0
- package/src/email/email.service.ts +609 -0
- package/src/files/upload.service.ts +145 -0
- package/src/i18n/i18n.ts +51 -0
- package/src/i18n/saveMissingLocalJsonBackend.ts +92 -0
- package/src/index.ts +7 -0
- package/src/iotdevice/iotdevice.controller.ts +136 -0
- package/src/iotdevice/iotdevice.model.ts +32 -0
- package/src/iotdevice/iotdevice.route.ts +181 -0
- package/src/iotdevice/iotdevice.schemas.ts +79 -0
- package/src/iotdevice/iotdevice.service.ts +732 -0
- package/src/iotdevice/iotdevice.validation.ts +61 -0
- package/src/middlewares/auth.ts +110 -0
- package/src/middlewares/checkJwt.cjs +19 -0
- package/src/middlewares/error.js.legacy +44 -0
- package/src/middlewares/error.ts +41 -0
- package/src/middlewares/mongooseValidations/ensureSameOrganization.ts +15 -0
- package/src/middlewares/rateLimiter.ts +10 -0
- package/src/middlewares/validate.ts +25 -0
- package/src/middlewares/validateAction.ts +41 -0
- package/src/middlewares/validateAdmin.ts +21 -0
- package/src/middlewares/validateAi.ts +24 -0
- package/src/middlewares/validateCurrentAuthUser.ts +23 -0
- package/src/middlewares/validateCurrentUser.ts +35 -0
- package/src/middlewares/validateDevice.ts +191 -0
- package/src/middlewares/validateDeviceUserOrganization.ts +54 -0
- package/src/middlewares/validateOrganization.ts +109 -0
- package/src/middlewares/validateQuerySearchUserAndOrganization.ts +75 -0
- package/src/middlewares/validateTokens.ts +36 -0
- package/src/middlewares/validateUser.ts +75 -0
- package/src/middlewares/validateZod.ts +54 -0
- package/src/models/plugins/index.ts +7 -0
- package/src/models/plugins/paginate.plugin.ts +145 -0
- package/src/models/plugins/paginateNew.plugin.ts +206 -0
- package/src/models/plugins/simplePopulate.ts +12 -0
- package/src/models/plugins/toJSON.plugin.ts +51 -0
- package/src/organizations/organizations.controller.ts +101 -0
- package/src/organizations/organizations.model.ts +62 -0
- package/src/organizations/organizations.route.ts +119 -0
- package/src/organizations/organizations.schemas.ts +8 -0
- package/src/organizations/organizations.service.ts +85 -0
- package/src/organizations/organizations.validation.ts +76 -0
- package/src/pdf/pdf.controller.ts +18 -0
- package/src/pdf/pdf.route.ts +28 -0
- package/src/pdf/pdf.schemas.ts +7 -0
- package/src/pdf/pdf.service.ts +89 -0
- package/src/pdf/pdf.validation.ts +30 -0
- package/src/tokens/tokens.controller.ts +81 -0
- package/src/tokens/tokens.model.ts +24 -0
- package/src/tokens/tokens.route.ts +66 -0
- package/src/tokens/tokens.schemas.ts +15 -0
- package/src/tokens/tokens.service.ts +46 -0
- package/src/tokens/tokens.validation.ts +13 -0
- package/src/types/routeSpec.ts +1 -0
- package/src/users/users.controller.ts +234 -0
- package/src/users/users.model.ts +89 -0
- package/src/users/users.route.ts +171 -0
- package/src/users/users.schemas.ts +79 -0
- package/src/users/users.service.ts +393 -0
- package/src/users/users.validation.ts +166 -0
- package/src/utils/ApiError.ts +18 -0
- package/src/utils/buildRouterAndDocs.ts +85 -0
- package/src/utils/catchAsync.ts +9 -0
- package/src/utils/comparePapers.service.ts +48 -0
- package/src/utils/filterOptions.ts +37 -0
- package/src/utils/medicationName.ts +12 -0
- package/src/utils/pick.ts +16 -0
- package/src/utils/registerOpenApi.ts +32 -0
- package/src/utils/urlUtils.ts +14 -0
- package/src/utils/userName.ts +27 -0
- package/src/utils/zValidations.ts +89 -0
- package/src/validations/auth.validation.cjs +60 -0
- package/src/validations/custom.validation.ts +26 -0
- package/src/validations/index.cjs +2 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import httpStatus from "http-status";
|
|
2
|
+
import ApiError from "../utils/ApiError";
|
|
3
|
+
import devicesService from "../devices/devices.service";
|
|
4
|
+
import { isAdmin } from "./validateAdmin";
|
|
5
|
+
|
|
6
|
+
import type { Request, Response, NextFunction } from "express";
|
|
7
|
+
import type { Device } from "../devices/devices.types";
|
|
8
|
+
import type { User } from "../users/users.types";
|
|
9
|
+
import userService from "../users/users.service";
|
|
10
|
+
|
|
11
|
+
export const validateDeviceUserOrganization = async (
|
|
12
|
+
req: Request,
|
|
13
|
+
res: Response,
|
|
14
|
+
next: NextFunction,
|
|
15
|
+
): Promise<void> => {
|
|
16
|
+
if (isAdmin(res.req.auth)) {
|
|
17
|
+
next();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (req.body.patient) {
|
|
22
|
+
const currentDevice: Device | null = await devicesService.getById(
|
|
23
|
+
req.params.deviceId,
|
|
24
|
+
);
|
|
25
|
+
if (!currentDevice) {
|
|
26
|
+
next(
|
|
27
|
+
new ApiError(
|
|
28
|
+
httpStatus.FORBIDDEN,
|
|
29
|
+
"Device was not found (validateDeviceUserOrganization)",
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const currentUser: User | null =
|
|
36
|
+
await userService.getUsersByOrganizationAndId(
|
|
37
|
+
currentDevice.organization,
|
|
38
|
+
req.body.patient,
|
|
39
|
+
);
|
|
40
|
+
if (!currentUser) {
|
|
41
|
+
next(
|
|
42
|
+
new ApiError(
|
|
43
|
+
httpStatus.FORBIDDEN,
|
|
44
|
+
"User is not part of the organization which has access to the device (validateDeviceUserOrganization)",
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
req.currentUser = currentUser;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
next();
|
|
54
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from "express";
|
|
2
|
+
import type { UserService } from "../users/users.service";
|
|
3
|
+
import userService from "../users/users.service";
|
|
4
|
+
|
|
5
|
+
import httpStatus from "http-status";
|
|
6
|
+
import ApiError from "../utils/ApiError";
|
|
7
|
+
import { isAdmin } from "./validateAdmin";
|
|
8
|
+
|
|
9
|
+
export const validateUser = async (
|
|
10
|
+
req: Request,
|
|
11
|
+
res: Response,
|
|
12
|
+
next: NextFunction,
|
|
13
|
+
): Promise<void> => {
|
|
14
|
+
next();
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const validateOrganization = async (
|
|
18
|
+
req: Request,
|
|
19
|
+
res: Response,
|
|
20
|
+
next: NextFunction,
|
|
21
|
+
): Promise<void> => {
|
|
22
|
+
if (isAdmin(res.req.auth)) {
|
|
23
|
+
next();
|
|
24
|
+
} else {
|
|
25
|
+
const currentUser = await userService.getUserByOwner(
|
|
26
|
+
res.req.auth.sub,
|
|
27
|
+
req.params.organizationId,
|
|
28
|
+
);
|
|
29
|
+
if (!currentUser) {
|
|
30
|
+
next(
|
|
31
|
+
new ApiError(
|
|
32
|
+
httpStatus.FORBIDDEN,
|
|
33
|
+
"User is not part of the organization (validateOrganization)",
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
/* if (currentUser.role && currentUser.role === 'onlyself') {
|
|
39
|
+
next(new ApiError(httpStatus.FORBIDDEN, 'User does not have sufficient permissions in the organization'));
|
|
40
|
+
return;
|
|
41
|
+
} */
|
|
42
|
+
req.currentUser = currentUser;
|
|
43
|
+
next();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const validateQueryOrganization = async (
|
|
48
|
+
req: Request,
|
|
49
|
+
res: Response,
|
|
50
|
+
next: NextFunction,
|
|
51
|
+
): Promise<void> => {
|
|
52
|
+
if (isAdmin(res.req.auth)) {
|
|
53
|
+
next();
|
|
54
|
+
} else {
|
|
55
|
+
const currentUser = await userService.getUserByOwner(
|
|
56
|
+
res.req.auth.sub,
|
|
57
|
+
req.query.organization,
|
|
58
|
+
);
|
|
59
|
+
if (!currentUser) {
|
|
60
|
+
next(
|
|
61
|
+
new ApiError(
|
|
62
|
+
httpStatus.FORBIDDEN,
|
|
63
|
+
"User is not part of the organization (validateQueryOrganization)",
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* if (currentUser.role && currentUser.role === 'onlyself') {
|
|
70
|
+
next(new ApiError(httpStatus.FORBIDDEN, 'User does not have sufficient permissions in the organization'));
|
|
71
|
+
return;
|
|
72
|
+
} */
|
|
73
|
+
req.currentUser = currentUser;
|
|
74
|
+
next();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const validateBodyOrganization = async (
|
|
79
|
+
req: Request,
|
|
80
|
+
res: Response,
|
|
81
|
+
next: NextFunction,
|
|
82
|
+
): Promise<void> => {
|
|
83
|
+
if (isAdmin(res.req.auth)) {
|
|
84
|
+
next();
|
|
85
|
+
} else {
|
|
86
|
+
const currentUser = await userService.getUserByOwner(
|
|
87
|
+
res.req.auth.sub,
|
|
88
|
+
req.body.organization,
|
|
89
|
+
);
|
|
90
|
+
if (!currentUser) {
|
|
91
|
+
next(
|
|
92
|
+
new ApiError(
|
|
93
|
+
httpStatus.FORBIDDEN,
|
|
94
|
+
"User is not part of the organization (validateBodyOrganization)",
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
req.currentUser = currentUser;
|
|
100
|
+
next();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export default {
|
|
105
|
+
validateOrganization,
|
|
106
|
+
validateQueryOrganization,
|
|
107
|
+
validateBodyOrganization,
|
|
108
|
+
validateUser,
|
|
109
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import httpStatus from "http-status";
|
|
2
|
+
import ApiError from "../utils/ApiError";
|
|
3
|
+
import { isAdmin } from "./validateAdmin";
|
|
4
|
+
import type { Request, Response, NextFunction } from "express";
|
|
5
|
+
import userService from "../users/users.service";
|
|
6
|
+
import type { UserService } from "../users/users.service";
|
|
7
|
+
|
|
8
|
+
const validateQuerySearchUserAndOrganization = async (
|
|
9
|
+
req: Request,
|
|
10
|
+
res: Response,
|
|
11
|
+
next: NextFunction,
|
|
12
|
+
): Promise<void> => {
|
|
13
|
+
if (isAdmin(res.req.auth)) {
|
|
14
|
+
next();
|
|
15
|
+
} else {
|
|
16
|
+
console.log("validateQuerySearchUserAndOrganization", req.query);
|
|
17
|
+
if (req.query.organization) {
|
|
18
|
+
const currentUser = await userService.getUserByOwner(
|
|
19
|
+
res.req.auth.sub,
|
|
20
|
+
req.query.organization as string,
|
|
21
|
+
);
|
|
22
|
+
if (!currentUser) {
|
|
23
|
+
next(
|
|
24
|
+
new ApiError(
|
|
25
|
+
httpStatus.FORBIDDEN,
|
|
26
|
+
"User is not part of the organization (validateQuerySearchUserAndOrganization)",
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
/* if (currentUser.role && currentUser.role === 'onlyself') {
|
|
32
|
+
next(new ApiError(httpStatus.FORBIDDEN, 'User does not have sufficient permissions in the organization'));
|
|
33
|
+
return;
|
|
34
|
+
} */
|
|
35
|
+
req.currentUser = currentUser;
|
|
36
|
+
next();
|
|
37
|
+
} else if (req.query.patient) {
|
|
38
|
+
const activeUser = await userService.getById(req.query.patient as string);
|
|
39
|
+
if (!activeUser) {
|
|
40
|
+
next(
|
|
41
|
+
new ApiError(
|
|
42
|
+
httpStatus.FORBIDDEN,
|
|
43
|
+
"User not found (validateQuerySearchUserAndOrganization)",
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const currentUser = await userService.getUserByOwner(
|
|
49
|
+
res.req.auth.sub,
|
|
50
|
+
activeUser.organization,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (!currentUser) {
|
|
54
|
+
next(
|
|
55
|
+
new ApiError(
|
|
56
|
+
httpStatus.FORBIDDEN,
|
|
57
|
+
"User is not part of the organization which has access to the device (validateDevice)",
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
req.currentUser = currentUser;
|
|
63
|
+
next();
|
|
64
|
+
} else {
|
|
65
|
+
next(
|
|
66
|
+
new ApiError(
|
|
67
|
+
httpStatus.FORBIDDEN,
|
|
68
|
+
"No filter defined (validateQuerySearchUserAndOrganization)",
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export { validateQuerySearchUserAndOrganization };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import httpStatus from "http-status";
|
|
2
|
+
import type { Request, Response, NextFunction } from "express";
|
|
3
|
+
import ApiError from "../utils/ApiError";
|
|
4
|
+
import { getTokenById } from "../tokens/tokens.service";
|
|
5
|
+
|
|
6
|
+
export async function validateParamsToken(
|
|
7
|
+
req: Request,
|
|
8
|
+
res: Response,
|
|
9
|
+
next: NextFunction,
|
|
10
|
+
): Promise<void> {
|
|
11
|
+
// assume tokenId comes from req.params
|
|
12
|
+
const tokenId = req.params.tokenId as string | undefined;
|
|
13
|
+
if (!tokenId) {
|
|
14
|
+
return next(new ApiError(httpStatus.BAD_REQUEST, "Token ID is required"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// fetch your token entity
|
|
18
|
+
const token = await getTokenById(tokenId);
|
|
19
|
+
if (!token) {
|
|
20
|
+
return next(new ApiError(httpStatus.NOT_FOUND, "Token not found"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// compare owner vs. authenticated sub
|
|
24
|
+
if (token.owner !== res.req.auth.sub) {
|
|
25
|
+
return next(
|
|
26
|
+
new ApiError(
|
|
27
|
+
httpStatus.FORBIDDEN,
|
|
28
|
+
"You are not allowed to access this token",
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// attach for downstream handlers and continue
|
|
34
|
+
(req as any).token = token;
|
|
35
|
+
next();
|
|
36
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import httpStatus from "http-status";
|
|
2
|
+
import ApiError from "../utils/ApiError";
|
|
3
|
+
import usersService from "../users/users.service";
|
|
4
|
+
import { isAdmin } from "./validateAdmin";
|
|
5
|
+
|
|
6
|
+
import type { Request, Response, NextFunction } from "express";
|
|
7
|
+
import type { User } from "../users/users.types";
|
|
8
|
+
|
|
9
|
+
// extend makeValidateUser to accept an optional key (defaulting to 'patient')
|
|
10
|
+
const makeValidateUser =
|
|
11
|
+
(source: "query" | "body" | "params", key: string = "patient") =>
|
|
12
|
+
async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
13
|
+
const context = `validate${source.charAt(0).toUpperCase() + source.slice(1)}User`;
|
|
14
|
+
if (isAdmin(res.req.auth)) {
|
|
15
|
+
return next();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// pull the configured key instead of hard‐coded 'patient'
|
|
19
|
+
const id = (req[source] as any)[key] as string | undefined;
|
|
20
|
+
if (!id) {
|
|
21
|
+
return next(new ApiError(httpStatus.FORBIDDEN, `No user (${context})`));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const patient: User | null = await usersService.getById(id);
|
|
25
|
+
if (!patient) {
|
|
26
|
+
return next(
|
|
27
|
+
new ApiError(
|
|
28
|
+
httpStatus.FORBIDDEN,
|
|
29
|
+
`User was with id ${id} not found (${context})`,
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const currentUser: User | null = await usersService.getUserByOwner(
|
|
35
|
+
res.req.auth.sub,
|
|
36
|
+
patient.organization,
|
|
37
|
+
);
|
|
38
|
+
if (!currentUser) {
|
|
39
|
+
return next(
|
|
40
|
+
new ApiError(
|
|
41
|
+
httpStatus.FORBIDDEN,
|
|
42
|
+
`Current user is not part of the organization as the requested user (${context})`,
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
(req as any).currentUser = currentUser;
|
|
48
|
+
next();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// query and body still use the default 'patient' key
|
|
52
|
+
export function validateQueryUser(
|
|
53
|
+
req: Request,
|
|
54
|
+
res: Response,
|
|
55
|
+
next: NextFunction,
|
|
56
|
+
): Promise<void> {
|
|
57
|
+
return makeValidateUser("query")(req, res, next);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function validateBodyUser(
|
|
61
|
+
req: Request,
|
|
62
|
+
res: Response,
|
|
63
|
+
next: NextFunction,
|
|
64
|
+
): Promise<void> {
|
|
65
|
+
return makeValidateUser("body")(req, res, next);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// params now pulls from req.params.userId
|
|
69
|
+
export function validateParamsUser(
|
|
70
|
+
req: Request,
|
|
71
|
+
res: Response,
|
|
72
|
+
next: NextFunction,
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
return makeValidateUser("params", "userId")(req, res, next);
|
|
75
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import type { AnyZodObject } from 'zod';
|
|
3
|
+
import ApiError from '../utils/ApiError';
|
|
4
|
+
import httpStatus from 'http-status';
|
|
5
|
+
import z from 'zod';
|
|
6
|
+
|
|
7
|
+
interface Schema {
|
|
8
|
+
body?: AnyZodObject;
|
|
9
|
+
query?: AnyZodObject;
|
|
10
|
+
params?: AnyZodObject;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const validateZod = (schema: Schema) => (req: Request, res: Response, next: NextFunction) => {
|
|
14
|
+
try {
|
|
15
|
+
schema.body ||= z.object({});
|
|
16
|
+
schema.query ||= z.object({});
|
|
17
|
+
schema.params ||= z.object({});
|
|
18
|
+
|
|
19
|
+
// 1) run safeParse on each
|
|
20
|
+
const result = {
|
|
21
|
+
body: schema.body.strict().safeParse(req.body || {}),
|
|
22
|
+
query: schema.query.strict().safeParse(req.query),
|
|
23
|
+
params: schema.params.strict().safeParse(req.params),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// 2) if any failure, short-circuit
|
|
27
|
+
if (!result.body.success || !result.query.success || !result.params.success) {
|
|
28
|
+
if (process.env.NODE_ENV === 'development') {
|
|
29
|
+
return res.status(400).send(result);
|
|
30
|
+
}
|
|
31
|
+
return next(new ApiError(httpStatus.BAD_REQUEST, 'Validation error'));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 3) merge parsed data back in
|
|
35
|
+
req.body = result.body.data;
|
|
36
|
+
Object.assign(req.query as Record<string, any>, result.query.data);
|
|
37
|
+
Object.assign(req.params as Record<string, any>, result.params.data);
|
|
38
|
+
|
|
39
|
+
return next();
|
|
40
|
+
} catch (err: any) {
|
|
41
|
+
console.error('Zod validation error:', err);
|
|
42
|
+
return next(
|
|
43
|
+
new ApiError(
|
|
44
|
+
httpStatus.BAD_REQUEST,
|
|
45
|
+
'Validation error',
|
|
46
|
+
undefined,
|
|
47
|
+
undefined,
|
|
48
|
+
process.env.NODE_ENV === 'development' ? err : undefined,
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default validateZod;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ToJSONPlugin } from './toJSON.plugin';
|
|
2
|
+
import type { PaginatePlugin } from './paginate.plugin';
|
|
3
|
+
// import type { PaginateNewPlugin } from './paginateNew.plugin';
|
|
4
|
+
|
|
5
|
+
export { default as toJSON } from './toJSON.plugin';
|
|
6
|
+
export { default as paginate } from './paginate.plugin';
|
|
7
|
+
// export { default as paginateNew } from './paginateNew.plugin';
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/* eslint-disable no-param-reassign */
|
|
2
|
+
import { Document, Model, Query, Types } from 'mongoose';
|
|
3
|
+
|
|
4
|
+
export interface QueryResult<T> {
|
|
5
|
+
results: T[];
|
|
6
|
+
page: number;
|
|
7
|
+
limit: number;
|
|
8
|
+
totalPages: number;
|
|
9
|
+
totalResults: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PaginateOptions {
|
|
13
|
+
sortBy?: string;
|
|
14
|
+
populate?: string;
|
|
15
|
+
limit?: number;
|
|
16
|
+
page?: number;
|
|
17
|
+
fuzzySearch?: string;
|
|
18
|
+
// Add more options as needed
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type PluginFunction = (query: Query<any, any>) => Query<any, any>;
|
|
22
|
+
|
|
23
|
+
function paginate<T extends Document>(schema: any) {
|
|
24
|
+
schema.statics.paginate = async function (
|
|
25
|
+
filter: Record<string, any>,
|
|
26
|
+
options: PaginateOptions = {},
|
|
27
|
+
plugin?: PluginFunction,
|
|
28
|
+
): Promise<QueryResult<T>> {
|
|
29
|
+
let sort = '';
|
|
30
|
+
if (options.sortBy) {
|
|
31
|
+
const sortingCriteria: string[] = [];
|
|
32
|
+
options.sortBy.split(',').forEach((sortOption) => {
|
|
33
|
+
const [key, order] = sortOption.split(':');
|
|
34
|
+
sortingCriteria.push((order === 'desc' ? '-' : '') + key);
|
|
35
|
+
});
|
|
36
|
+
sort = sortingCriteria.join(' ');
|
|
37
|
+
} else {
|
|
38
|
+
sort = 'createdAt';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const limit = options.limit && parseInt(String(options.limit), 10) > 0 ? parseInt(String(options.limit), 10) : 10000;
|
|
42
|
+
const page = options.page && parseInt(String(options.page), 10) > 0 ? parseInt(String(options.page), 10) : 1;
|
|
43
|
+
const skip = (page - 1) * limit;
|
|
44
|
+
|
|
45
|
+
let results: any[] = [];
|
|
46
|
+
let totalResults = 0;
|
|
47
|
+
let totalPages = 0;
|
|
48
|
+
|
|
49
|
+
if (options.fuzzySearch && options.fuzzySearch.search) {
|
|
50
|
+
// Fuzzy search branch
|
|
51
|
+
|
|
52
|
+
const fuzzyFields = options.fuzzySearch.fields;
|
|
53
|
+
const mustClauses = Object.entries(filter).map(([key, value]) => {
|
|
54
|
+
if (typeof value === 'string' && value.match(/^[a-fA-F0-9]{24}$/)) {
|
|
55
|
+
return { equals: { path: key, value: new Types.ObjectId(value) } };
|
|
56
|
+
}
|
|
57
|
+
return { equals: { path: key, value } };
|
|
58
|
+
});
|
|
59
|
+
const pipeline = [
|
|
60
|
+
{
|
|
61
|
+
$search: {
|
|
62
|
+
index: options.fuzzySearch.index,
|
|
63
|
+
compound: {
|
|
64
|
+
must: mustClauses,
|
|
65
|
+
should: [
|
|
66
|
+
{
|
|
67
|
+
text: {
|
|
68
|
+
query: options.fuzzySearch.search,
|
|
69
|
+
path: fuzzyFields,
|
|
70
|
+
fuzzy: {},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
minimumShouldMatch: 1,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{ $sort: { createdAt: -1 } },
|
|
79
|
+
{ $skip: skip },
|
|
80
|
+
{ $limit: limit },
|
|
81
|
+
{
|
|
82
|
+
$facet: {
|
|
83
|
+
results: [],
|
|
84
|
+
totalCount: [{ $count: 'count' }],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
const aggResult = await this.aggregate(pipeline).exec();
|
|
89
|
+
results = (aggResult[0]?.results || []).map((doc: any) => new this(doc));
|
|
90
|
+
totalResults = aggResult[0]?.totalCount[0]?.count || 0;
|
|
91
|
+
totalPages = Math.ceil(totalResults / limit);
|
|
92
|
+
} else {
|
|
93
|
+
// Regular find branch
|
|
94
|
+
const countPromise = this.countDocuments(filter).exec();
|
|
95
|
+
|
|
96
|
+
let docsPromise: any = this.find(filter).sort(sort).skip(skip).limit(limit);
|
|
97
|
+
if (options.populate) {
|
|
98
|
+
options.populate.split(',').forEach((populateOption) => {
|
|
99
|
+
docsPromise = docsPromise.populate(
|
|
100
|
+
populateOption
|
|
101
|
+
.split('.')
|
|
102
|
+
.reverse()
|
|
103
|
+
.reduce((a, b) => ({ path: b, populate: a })),
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (plugin) docsPromise = plugin(docsPromise);
|
|
108
|
+
docsPromise = docsPromise.exec();
|
|
109
|
+
const values = await Promise.all([countPromise, docsPromise]);
|
|
110
|
+
totalResults = values[0];
|
|
111
|
+
results = values[1];
|
|
112
|
+
totalPages = Math.ceil(totalResults / limit);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Populate and plugin for both branches (if not already applied)
|
|
116
|
+
if (options.fuzzySearch && options.fuzzySearch.search) {
|
|
117
|
+
if (options.populate) {
|
|
118
|
+
results = await this.populate(
|
|
119
|
+
results,
|
|
120
|
+
options.populate.split(',').map((populateOption) =>
|
|
121
|
+
populateOption
|
|
122
|
+
.split('.')
|
|
123
|
+
.reverse()
|
|
124
|
+
.reduce((a, b) => ({ path: b, populate: a })),
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (plugin) {
|
|
129
|
+
// plugin expects a Query, so wrap results in a Query if needed
|
|
130
|
+
// Not possible for array, so skip plugin for fuzzySearch unless you have a custom handler
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const result: QueryResult<T> = {
|
|
135
|
+
results,
|
|
136
|
+
page,
|
|
137
|
+
limit,
|
|
138
|
+
totalPages,
|
|
139
|
+
totalResults,
|
|
140
|
+
};
|
|
141
|
+
return Promise.resolve(result);
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default paginate;
|