response-standardizer 1.3.5 → 1.3.7
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/dist/index.js +8 -2
- package/package.json +38 -38
- package/src/exceptions.ts +64 -64
- package/src/index.ts +225 -218
- package/src/types.ts +60 -60
- package/src/utils.ts +9 -9
- package/tsconfig.json +14 -14
package/dist/index.js
CHANGED
|
@@ -14,8 +14,14 @@ const initKeycloak = async (config) => {
|
|
|
14
14
|
const realmUrl = `http://${KEYCLOAK_SERVICE}/realms/${KEYCLOAK_REALM}`;
|
|
15
15
|
(0, exports.info)(`Keycloak PublicKey Url: ${realmUrl}`);
|
|
16
16
|
const resp = await axios_1.default.get(realmUrl);
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
if (resp.status === 200) {
|
|
18
|
+
(0, exports.info)(`Keycloak: ${resp.data}`);
|
|
19
|
+
const key = resp.data.public_key;
|
|
20
|
+
KEYCLOAK_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----\n${key.match(/.{1,64}/g)?.join("\n")}\n-----END PUBLIC KEY-----`;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
(0, exports.error)(`There is no any public key for keycloak`);
|
|
24
|
+
}
|
|
19
25
|
};
|
|
20
26
|
exports.initKeycloak = initKeycloak;
|
|
21
27
|
const protect = (allowedRoles) => {
|
package/package.json
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "response-standardizer",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"description": "Express middleware to standardize API responses",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
|
-
"type": "commonjs",
|
|
8
|
-
"scripts": {
|
|
9
|
-
"build": "tsc",
|
|
10
|
-
"prepare": "npm run build"
|
|
11
|
-
},
|
|
12
|
-
"repository": {
|
|
13
|
-
"type": "git",
|
|
14
|
-
"url": "git+https://gitlab.com/dezhnevesht-archive-software/response-standardizer.git"
|
|
15
|
-
},
|
|
16
|
-
"author": "Hamid Atyabi",
|
|
17
|
-
"license": "ISC",
|
|
18
|
-
"bugs": {
|
|
19
|
-
"url": "https://gitlab.com/dezhnevesht-archive-software/response-standardizer/issues"
|
|
20
|
-
},
|
|
21
|
-
"homepage": "https://gitlab.com/dezhnevesht-archive-software/response-standardizer#readme",
|
|
22
|
-
"peerDependencies": {
|
|
23
|
-
"express": "^4 || ^5"
|
|
24
|
-
},
|
|
25
|
-
"dependencies": {
|
|
26
|
-
"axios": "^1.13.2",
|
|
27
|
-
"jsonwebtoken": "^9.0.2",
|
|
28
|
-
"uuid": "^9.0.1",
|
|
29
|
-
"zod": "^4.1.12"
|
|
30
|
-
},
|
|
31
|
-
"devDependencies": {
|
|
32
|
-
"@types/express": "^5.0.5",
|
|
33
|
-
"@types/jsonwebtoken": "^9.0.10",
|
|
34
|
-
"@types/uuid": "^10.0.0",
|
|
35
|
-
"ts-node-dev": "^2.0.0",
|
|
36
|
-
"typescript": "^5.9.3"
|
|
37
|
-
}
|
|
38
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "response-standardizer",
|
|
3
|
+
"version": "1.3.7",
|
|
4
|
+
"description": "Express middleware to standardize API responses",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"prepare": "npm run build"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://gitlab.com/dezhnevesht-archive-software/response-standardizer.git"
|
|
15
|
+
},
|
|
16
|
+
"author": "Hamid Atyabi",
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://gitlab.com/dezhnevesht-archive-software/response-standardizer/issues"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://gitlab.com/dezhnevesht-archive-software/response-standardizer#readme",
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"express": "^4 || ^5"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"axios": "^1.13.2",
|
|
27
|
+
"jsonwebtoken": "^9.0.2",
|
|
28
|
+
"uuid": "^9.0.1",
|
|
29
|
+
"zod": "^4.1.12"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/express": "^5.0.5",
|
|
33
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
34
|
+
"@types/uuid": "^10.0.0",
|
|
35
|
+
"ts-node-dev": "^2.0.0",
|
|
36
|
+
"typescript": "^5.9.3"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/exceptions.ts
CHANGED
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
import { Request, Response } from "express";
|
|
2
|
-
import { RestResponse } from ".";
|
|
3
|
-
import { ErrorFields } from "./types";
|
|
4
|
-
export class ServiceException extends Error{
|
|
5
|
-
statusCode: number;
|
|
6
|
-
constructor(statusCode: number, message: string) {
|
|
7
|
-
super(message);
|
|
8
|
-
this.statusCode = statusCode;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
export class UnknownException extends ServiceException {
|
|
12
|
-
constructor(statusCode: number, message: string) {
|
|
13
|
-
super(statusCode, message);
|
|
14
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
export class NotFoundException extends ServiceException {
|
|
18
|
-
constructor(message: string) {
|
|
19
|
-
super(404, message);
|
|
20
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
export class UnauthorizedException extends ServiceException {
|
|
24
|
-
constructor(message: string) {
|
|
25
|
-
super(401, message);
|
|
26
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
export class AccessDeniedException extends ServiceException {
|
|
30
|
-
constructor(message: string) {
|
|
31
|
-
super(403, message);
|
|
32
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
export class InternalServerException extends ServiceException {
|
|
36
|
-
constructor(message: string) {
|
|
37
|
-
super(500, message);
|
|
38
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
export class ValidationException extends ServiceException {
|
|
42
|
-
errors?: ErrorFields
|
|
43
|
-
|
|
44
|
-
constructor(message: string, errors?: ErrorFields) {
|
|
45
|
-
super(400, message);
|
|
46
|
-
this.errors = errors;
|
|
47
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export const handleControllerException = (req: Request, res: Response, err: Error) => {
|
|
52
|
-
if(err instanceof UnauthorizedException){
|
|
53
|
-
return RestResponse.unauthorized(req, res, err.message);
|
|
54
|
-
}else if(err instanceof AccessDeniedException){
|
|
55
|
-
return RestResponse.accessDenied(req, res, err.message);
|
|
56
|
-
}else if(err instanceof ValidationException){
|
|
57
|
-
return RestResponse.validationError(req, res, (err as any)?.errors, (err as any)?.message);
|
|
58
|
-
}else if(err instanceof InternalServerException){
|
|
59
|
-
return RestResponse.exceptionError(req, res, err.message);
|
|
60
|
-
}else if(err instanceof NotFoundException){
|
|
61
|
-
return RestResponse.notFound(req, res, err.message);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return RestResponse.exceptionError(req, res, err.message);
|
|
1
|
+
import { Request, Response } from "express";
|
|
2
|
+
import { RestResponse } from ".";
|
|
3
|
+
import { ErrorFields } from "./types";
|
|
4
|
+
export class ServiceException extends Error{
|
|
5
|
+
statusCode: number;
|
|
6
|
+
constructor(statusCode: number, message: string) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.statusCode = statusCode;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class UnknownException extends ServiceException {
|
|
12
|
+
constructor(statusCode: number, message: string) {
|
|
13
|
+
super(statusCode, message);
|
|
14
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class NotFoundException extends ServiceException {
|
|
18
|
+
constructor(message: string) {
|
|
19
|
+
super(404, message);
|
|
20
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class UnauthorizedException extends ServiceException {
|
|
24
|
+
constructor(message: string) {
|
|
25
|
+
super(401, message);
|
|
26
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export class AccessDeniedException extends ServiceException {
|
|
30
|
+
constructor(message: string) {
|
|
31
|
+
super(403, message);
|
|
32
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export class InternalServerException extends ServiceException {
|
|
36
|
+
constructor(message: string) {
|
|
37
|
+
super(500, message);
|
|
38
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class ValidationException extends ServiceException {
|
|
42
|
+
errors?: ErrorFields
|
|
43
|
+
|
|
44
|
+
constructor(message: string, errors?: ErrorFields) {
|
|
45
|
+
super(400, message);
|
|
46
|
+
this.errors = errors;
|
|
47
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const handleControllerException = (req: Request, res: Response, err: Error) => {
|
|
52
|
+
if(err instanceof UnauthorizedException){
|
|
53
|
+
return RestResponse.unauthorized(req, res, err.message);
|
|
54
|
+
}else if(err instanceof AccessDeniedException){
|
|
55
|
+
return RestResponse.accessDenied(req, res, err.message);
|
|
56
|
+
}else if(err instanceof ValidationException){
|
|
57
|
+
return RestResponse.validationError(req, res, (err as any)?.errors, (err as any)?.message);
|
|
58
|
+
}else if(err instanceof InternalServerException){
|
|
59
|
+
return RestResponse.exceptionError(req, res, err.message);
|
|
60
|
+
}else if(err instanceof NotFoundException){
|
|
61
|
+
return RestResponse.notFound(req, res, err.message);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return RestResponse.exceptionError(req, res, err.message);
|
|
65
65
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,219 +1,226 @@
|
|
|
1
|
-
import { Request, Response, NextFunction } from "express";
|
|
2
|
-
import { ErrorFields, StandardResponse, RestResponseFunctions, RestMiddlewareFunctions, AuthRequest, Pagination } from "./types";
|
|
3
|
-
import { generateRequestId, getTimestamp } from "./utils";
|
|
4
|
-
import axios from "axios";
|
|
5
|
-
import jwt, { TokenExpiredError } from "jsonwebtoken";
|
|
6
|
-
import path from "path";
|
|
7
|
-
import { ZodError } from "zod";
|
|
8
|
-
declare global {
|
|
9
|
-
namespace Express {
|
|
10
|
-
interface Response {
|
|
11
|
-
json: (data: any) => Response;
|
|
12
|
-
send: (data?: any) => Response;
|
|
13
|
-
locals: {
|
|
14
|
-
requestId: string;
|
|
15
|
-
[key: string]: any;
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
let KEYCLOAK_PUBLIC_KEY: any = null;
|
|
21
|
-
export const initKeycloak = async (config: { service?: string; realm?: string }) => {
|
|
22
|
-
const KEYCLOAK_SERVICE = config.service ?? "localhost";
|
|
23
|
-
const KEYCLOAK_REALM = config.realm ?? "master";
|
|
24
|
-
const realmUrl = `http://${KEYCLOAK_SERVICE}/realms/${KEYCLOAK_REALM}`;
|
|
25
|
-
info(`Keycloak PublicKey Url: ${realmUrl}`);
|
|
26
|
-
const resp = await axios.get(realmUrl);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
res
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
res.status(
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
res.status(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
res
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
return
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
res.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
import { ErrorFields, StandardResponse, RestResponseFunctions, RestMiddlewareFunctions, AuthRequest, Pagination } from "./types";
|
|
3
|
+
import { generateRequestId, getTimestamp } from "./utils";
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
import jwt, { TokenExpiredError } from "jsonwebtoken";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { ZodError } from "zod";
|
|
8
|
+
declare global {
|
|
9
|
+
namespace Express {
|
|
10
|
+
interface Response {
|
|
11
|
+
json: (data: any) => Response;
|
|
12
|
+
send: (data?: any) => Response;
|
|
13
|
+
locals: {
|
|
14
|
+
requestId: string;
|
|
15
|
+
[key: string]: any;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
let KEYCLOAK_PUBLIC_KEY: any = null;
|
|
21
|
+
export const initKeycloak = async (config: { service?: string; realm?: string }) => {
|
|
22
|
+
const KEYCLOAK_SERVICE = config.service ?? "localhost";
|
|
23
|
+
const KEYCLOAK_REALM = config.realm ?? "master";
|
|
24
|
+
const realmUrl = `http://${KEYCLOAK_SERVICE}/realms/${KEYCLOAK_REALM}`;
|
|
25
|
+
info(`Keycloak PublicKey Url: ${realmUrl}`);
|
|
26
|
+
const resp = await axios.get(realmUrl);
|
|
27
|
+
if(resp.status === 200){
|
|
28
|
+
info(`Keycloak: ${resp.data}`);
|
|
29
|
+
const key = resp.data.public_key;
|
|
30
|
+
KEYCLOAK_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----\n${key.match(/.{1,64}/g)?.join("\n")}\n-----END PUBLIC KEY-----`;
|
|
31
|
+
}else{
|
|
32
|
+
error(`There is no any public key for keycloak`);
|
|
33
|
+
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const protect = (allowedRoles?: string[]) => {
|
|
39
|
+
return (req: any, res: any, next: any) => {
|
|
40
|
+
const authHeader = req.headers["authorization"];
|
|
41
|
+
if (!authHeader)
|
|
42
|
+
return RestResponse.unauthorized(req, res)
|
|
43
|
+
|
|
44
|
+
const token = authHeader.split(" ")[1];
|
|
45
|
+
if (!token)
|
|
46
|
+
return RestResponse.unauthorized(req, res, "Token malformed")
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const decoded = jwt.verify(token, KEYCLOAK_PUBLIC_KEY, { algorithms: ["RS256"] });
|
|
50
|
+
(req as AuthRequest).user = decoded;
|
|
51
|
+
(req as AuthRequest).token = token;
|
|
52
|
+
if(allowedRoles)
|
|
53
|
+
return role(req, res, next, allowedRoles);
|
|
54
|
+
|
|
55
|
+
next();
|
|
56
|
+
} catch (err) {
|
|
57
|
+
error((err as any)?.message)
|
|
58
|
+
return RestResponse.unauthorized(req, res, "Token is not valid")
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
const role = (req: any, res: any, next: any, allowedRoles: string[]) => {
|
|
64
|
+
const user = req.user;
|
|
65
|
+
if (!user) {
|
|
66
|
+
return RestResponse.unauthorized(req, res);
|
|
67
|
+
}
|
|
68
|
+
const realmRoles: string[] = (user?.realm_access?.roles ?? []).map((r: string) => r.toUpperCase());
|
|
69
|
+
const clientRoles: string[] = Object.values(user?.resource_access ?? {})
|
|
70
|
+
.flatMap((r: any) => r.roles ?? [])
|
|
71
|
+
.map((r: string) => r.toUpperCase());
|
|
72
|
+
const allowedUpper = allowedRoles.map(r => r === "super-admin"?"ADMIN":`DEZH-${r.toUpperCase()}`);
|
|
73
|
+
const allRoles = [...realmRoles, ...clientRoles];
|
|
74
|
+
const hasAccess: boolean = allowedUpper.some((role: string) =>
|
|
75
|
+
allRoles.includes(role)
|
|
76
|
+
);
|
|
77
|
+
if (!hasAccess) {
|
|
78
|
+
return RestResponse.accessDenied(req, res)
|
|
79
|
+
}
|
|
80
|
+
next()
|
|
81
|
+
}
|
|
82
|
+
const success = <T>(req: Request, res: Response, data: T) => {
|
|
83
|
+
res.status(200).json(data);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const paginate = <T>(
|
|
87
|
+
req: Request,
|
|
88
|
+
res: Response,
|
|
89
|
+
data: Pagination<T>
|
|
90
|
+
) => {
|
|
91
|
+
res.status(200).json(data);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const created = <T>(req: Request, res: Response, data: T) => {
|
|
95
|
+
res.status(201).json(data);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const deleted = (req: Request, res: Response) => {
|
|
99
|
+
res.status(204).send();
|
|
100
|
+
};
|
|
101
|
+
const noContent = (req: Request, res: Response) => {
|
|
102
|
+
res.status(204).send();
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const validationError = (
|
|
106
|
+
req: Request,
|
|
107
|
+
res: Response,
|
|
108
|
+
errors?: ErrorFields,
|
|
109
|
+
message = "Validation error"
|
|
110
|
+
) => {
|
|
111
|
+
res.status(400).json({ errors, message });
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const exceptionError = (
|
|
115
|
+
req: Request,
|
|
116
|
+
res: Response,
|
|
117
|
+
errors: any,
|
|
118
|
+
message = "Internal server error"
|
|
119
|
+
) => {
|
|
120
|
+
res.status(500).json({
|
|
121
|
+
message,
|
|
122
|
+
errors
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const unauthorized = (req: Request, res: Response, message = "Unauthorized") => {
|
|
127
|
+
res.status(401).json({ message });
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const accessDenied = (req: Request, res: Response, message = "Access denied") => {
|
|
131
|
+
res.status(403).json({ message });
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const notFound = (req: Request, res: Response, message = "Not found") => {
|
|
135
|
+
res.status(404).json({ message });
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const RestResponse: RestResponseFunctions = {
|
|
139
|
+
success,
|
|
140
|
+
paginate,
|
|
141
|
+
created,
|
|
142
|
+
deleted,
|
|
143
|
+
noContent,
|
|
144
|
+
validationError,
|
|
145
|
+
exceptionError,
|
|
146
|
+
unauthorized,
|
|
147
|
+
accessDenied,
|
|
148
|
+
notFound
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
const responseHandlerMiddleware = (
|
|
153
|
+
req: Request,
|
|
154
|
+
res: Response,
|
|
155
|
+
next: NextFunction
|
|
156
|
+
) => {
|
|
157
|
+
|
|
158
|
+
const oldJson = res.json.bind(res);
|
|
159
|
+
const oldSend = res.send.bind(res);
|
|
160
|
+
|
|
161
|
+
res.json = function (data: any) {
|
|
162
|
+
if(res.statusCode > 204){
|
|
163
|
+
const response: StandardResponse = {
|
|
164
|
+
code: res.statusCode,
|
|
165
|
+
message: data?.message || res.locals.message || (res.statusCode < 300 ? "OK" : "Error"),
|
|
166
|
+
errors: data?.errors ?? null,
|
|
167
|
+
meta: {
|
|
168
|
+
requestId: res.locals.requestId || generateRequestId(),
|
|
169
|
+
timestamp: getTimestamp()
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return oldJson(response);
|
|
174
|
+
}
|
|
175
|
+
return oldJson(data);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
res.send = function (data: any) {
|
|
179
|
+
if (res.statusCode === 204) {
|
|
180
|
+
return oldSend();
|
|
181
|
+
}
|
|
182
|
+
return oldSend(data);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
res.locals.requestId = generateRequestId();
|
|
186
|
+
next();
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
export const RestMiddleware: RestMiddlewareFunctions = {
|
|
191
|
+
responseHandlerMiddleware
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
const colors = {
|
|
196
|
+
RESET: "\x1b[0m",
|
|
197
|
+
RED: "\x1b[31m",
|
|
198
|
+
GREEN: "\x1b[32m",
|
|
199
|
+
YELLOW: "\x1b[33m"
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
203
|
+
export const error = (message: string, meta?: any) => {
|
|
204
|
+
log("ERROR", message, meta)
|
|
205
|
+
}
|
|
206
|
+
export const warn = (message: string, meta?: any) => {
|
|
207
|
+
log("WARN", message, meta)
|
|
208
|
+
}
|
|
209
|
+
export const info = (message: string, meta?: any) => {
|
|
210
|
+
log("INFO", message, meta)
|
|
211
|
+
}
|
|
212
|
+
export const log = (level: "INFO" | "WARN" | "ERROR", message: string, meta?: any) => {
|
|
213
|
+
const timestamp = new Date().toISOString();
|
|
214
|
+
|
|
215
|
+
// انتخاب رنگ فقط در dev
|
|
216
|
+
let color = colors.RESET;
|
|
217
|
+
if (!isProduction) {
|
|
218
|
+
if (level === "INFO") color = colors.GREEN;
|
|
219
|
+
if (level === "WARN") color = colors.YELLOW;
|
|
220
|
+
if (level === "ERROR") color = colors.RED;
|
|
221
|
+
}
|
|
222
|
+
const metaStr = meta ? `. ${JSON.stringify(meta)}` : "";
|
|
223
|
+
|
|
224
|
+
// چاپ لاگ
|
|
225
|
+
console.log(`${color}[${timestamp}][${level}] ${message}${metaStr}${colors.RESET}`);
|
|
219
226
|
};
|
package/src/types.ts
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
import { Request, Response, NextFunction } from "express";
|
|
2
|
-
|
|
3
|
-
export interface StandardResponse<T = any> {
|
|
4
|
-
code: number;
|
|
5
|
-
message: string;
|
|
6
|
-
errors: Record<string, string[]> | null;
|
|
7
|
-
meta: {
|
|
8
|
-
requestId: string;
|
|
9
|
-
timestamp: string;
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
export interface PaginationMeta{
|
|
13
|
-
total: number;
|
|
14
|
-
page: number;
|
|
15
|
-
limit: number;
|
|
16
|
-
totalPages: number;
|
|
17
|
-
}
|
|
18
|
-
export interface Pagination<T>{
|
|
19
|
-
data: T[];
|
|
20
|
-
meta: PaginationMeta
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export type ErrorFields = Record<string, string[]>;
|
|
25
|
-
|
|
26
|
-
export type Middleware = (req: Request, res: Response, next: NextFunction) => void;
|
|
27
|
-
|
|
28
|
-
export interface RestMiddlewareFunctions {
|
|
29
|
-
responseHandlerMiddleware: <T>(req: Request, res: Response, next: NextFunction) => void
|
|
30
|
-
}
|
|
31
|
-
export interface RestResponseFunctions {
|
|
32
|
-
success: <T>(req: Request, res: Response, data: T) => void;
|
|
33
|
-
paginate: <T>(
|
|
34
|
-
req: Request,
|
|
35
|
-
res: Response,
|
|
36
|
-
data: Pagination<T>
|
|
37
|
-
) => void;
|
|
38
|
-
created: <T>(req: Request, res: Response, data: T) => void;
|
|
39
|
-
deleted: (req: Request, res: Response) => void;
|
|
40
|
-
noContent: (req: Request, res: Response) => void;
|
|
41
|
-
validationError: (
|
|
42
|
-
req: Request,
|
|
43
|
-
res: Response,
|
|
44
|
-
errors?: ErrorFields,
|
|
45
|
-
message?: string
|
|
46
|
-
) => void;
|
|
47
|
-
exceptionError: (
|
|
48
|
-
req: Request,
|
|
49
|
-
res: Response,
|
|
50
|
-
errors: any,
|
|
51
|
-
message?: string
|
|
52
|
-
) => void;
|
|
53
|
-
unauthorized: (req: Request, res: Response, message?: string) => void;
|
|
54
|
-
accessDenied: (req: Request, res: Response, message?: string) => void;
|
|
55
|
-
notFound: (req: Request, res: Response, message?: string) => void;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface AuthRequest extends Request {
|
|
59
|
-
user?: any;
|
|
60
|
-
token?: string
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
|
|
3
|
+
export interface StandardResponse<T = any> {
|
|
4
|
+
code: number;
|
|
5
|
+
message: string;
|
|
6
|
+
errors: Record<string, string[]> | null;
|
|
7
|
+
meta: {
|
|
8
|
+
requestId: string;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface PaginationMeta{
|
|
13
|
+
total: number;
|
|
14
|
+
page: number;
|
|
15
|
+
limit: number;
|
|
16
|
+
totalPages: number;
|
|
17
|
+
}
|
|
18
|
+
export interface Pagination<T>{
|
|
19
|
+
data: T[];
|
|
20
|
+
meta: PaginationMeta
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
export type ErrorFields = Record<string, string[]>;
|
|
25
|
+
|
|
26
|
+
export type Middleware = (req: Request, res: Response, next: NextFunction) => void;
|
|
27
|
+
|
|
28
|
+
export interface RestMiddlewareFunctions {
|
|
29
|
+
responseHandlerMiddleware: <T>(req: Request, res: Response, next: NextFunction) => void
|
|
30
|
+
}
|
|
31
|
+
export interface RestResponseFunctions {
|
|
32
|
+
success: <T>(req: Request, res: Response, data: T) => void;
|
|
33
|
+
paginate: <T>(
|
|
34
|
+
req: Request,
|
|
35
|
+
res: Response,
|
|
36
|
+
data: Pagination<T>
|
|
37
|
+
) => void;
|
|
38
|
+
created: <T>(req: Request, res: Response, data: T) => void;
|
|
39
|
+
deleted: (req: Request, res: Response) => void;
|
|
40
|
+
noContent: (req: Request, res: Response) => void;
|
|
41
|
+
validationError: (
|
|
42
|
+
req: Request,
|
|
43
|
+
res: Response,
|
|
44
|
+
errors?: ErrorFields,
|
|
45
|
+
message?: string
|
|
46
|
+
) => void;
|
|
47
|
+
exceptionError: (
|
|
48
|
+
req: Request,
|
|
49
|
+
res: Response,
|
|
50
|
+
errors: any,
|
|
51
|
+
message?: string
|
|
52
|
+
) => void;
|
|
53
|
+
unauthorized: (req: Request, res: Response, message?: string) => void;
|
|
54
|
+
accessDenied: (req: Request, res: Response, message?: string) => void;
|
|
55
|
+
notFound: (req: Request, res: Response, message?: string) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AuthRequest extends Request {
|
|
59
|
+
user?: any;
|
|
60
|
+
token?: string
|
|
61
61
|
}
|
package/src/utils.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { v4 as uuidv4 } from "uuid";
|
|
2
|
-
|
|
3
|
-
export const generateRequestId = () => {
|
|
4
|
-
return uuidv4();
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export const getTimestamp = () => {
|
|
8
|
-
return new Date().toISOString();
|
|
9
|
-
}
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
|
|
3
|
+
export const generateRequestId = () => {
|
|
4
|
+
return uuidv4();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const getTimestamp = () => {
|
|
8
|
+
return new Date().toISOString();
|
|
9
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "CommonJS",
|
|
5
|
-
"moduleResolution": "Node",
|
|
6
|
-
"rootDir": "src",
|
|
7
|
-
"outDir": "dist",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"declaration": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true,
|
|
12
|
-
"skipLibCheck": true
|
|
13
|
-
},
|
|
14
|
-
"include": ["src/**/*"]
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"moduleResolution": "Node",
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"skipLibCheck": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"]
|
|
15
15
|
}
|