tsledge 0.1.3 → 0.1.5
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/core/types.d.ts +3 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/db/mongodb.d.ts +4 -0
- package/dist/db/mongodb.d.ts.map +1 -1
- package/dist/fluent-interface/{fluent-pattern-executor.d.ts → fluent-pattern-handler.d.ts} +4 -4
- package/dist/fluent-interface/fluent-pattern-handler.d.ts.map +1 -0
- package/dist/fluent-interface/index.d.ts +1 -1
- package/dist/fluent-interface/index.d.ts.map +1 -1
- package/dist/fluent-interface/types.d.ts +6 -2
- package/dist/fluent-interface/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1001 -24
- package/dist/middleware/authentication/index.d.ts +4 -0
- package/dist/middleware/authentication/index.d.ts.map +1 -0
- package/dist/middleware/authentication/session.d.ts +74 -0
- package/dist/middleware/authentication/session.d.ts.map +1 -0
- package/dist/middleware/authentication/types.d.ts +12 -0
- package/dist/middleware/authentication/types.d.ts.map +1 -0
- package/dist/middleware/authentication/validation.d.ts +55 -0
- package/dist/middleware/authentication/validation.d.ts.map +1 -0
- package/dist/middleware/index.d.ts +2 -0
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/logger.d.ts.map +1 -1
- package/dist/middleware/types.d.ts +2 -0
- package/dist/middleware/types.d.ts.map +1 -0
- package/dist/models/auth-user.d.ts +14 -0
- package/dist/models/auth-user.d.ts.map +1 -0
- package/dist/models/index.d.ts +3 -0
- package/dist/models/index.d.ts.map +1 -0
- package/dist/models/token-blocklist.d.ts +12 -0
- package/dist/models/token-blocklist.d.ts.map +1 -0
- package/dist/src/index.js +1000 -0
- package/dist/tests/main.js +932 -0
- package/dist/utils/encoding.d.ts +7 -0
- package/dist/utils/encoding.d.ts.map +1 -0
- package/dist/utils/env.d.ts +9 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/validation.d.ts +9 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/package.json +16 -6
- package/dist/app.js +0 -30
- package/dist/app.js.map +0 -1
- package/dist/core/http.js +0 -79
- package/dist/core/http.js.map +0 -1
- package/dist/core/index.js +0 -20
- package/dist/core/index.js.map +0 -1
- package/dist/core/query-builder.js +0 -271
- package/dist/core/query-builder.js.map +0 -1
- package/dist/core/types.js +0 -49
- package/dist/core/types.js.map +0 -1
- package/dist/db/index.js +0 -18
- package/dist/db/index.js.map +0 -1
- package/dist/db/mongodb.js +0 -24
- package/dist/db/mongodb.js.map +0 -1
- package/dist/exitCodes.js +0 -7
- package/dist/exitCodes.js.map +0 -1
- package/dist/fluent-interface/fluent-pattern-executor.d.ts.map +0 -1
- package/dist/fluent-interface/fluent-pattern-executor.js +0 -137
- package/dist/fluent-interface/fluent-pattern-executor.js.map +0 -1
- package/dist/fluent-interface/index.js +0 -19
- package/dist/fluent-interface/index.js.map +0 -1
- package/dist/fluent-interface/types.js +0 -3
- package/dist/fluent-interface/types.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/middleware/file-storage.js +0 -26
- package/dist/middleware/file-storage.js.map +0 -1
- package/dist/middleware/index.js +0 -19
- package/dist/middleware/index.js.map +0 -1
- package/dist/middleware/logger.js +0 -30
- package/dist/middleware/logger.js.map +0 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
- package/dist/utils/date.js +0 -8
- package/dist/utils/date.js.map +0 -1
- package/dist/utils/index.js +0 -20
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/mongo-relation.js +0 -89
- package/dist/utils/mongo-relation.js.map +0 -1
- package/dist/utils/validation.js +0 -17
- package/dist/utils/validation.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,25 +1,1002 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}))
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
// src/db/mongodb.ts
|
|
2
|
+
import mongoose from "mongoose";
|
|
3
|
+
|
|
4
|
+
// src/exitCodes.ts
|
|
5
|
+
var EXIT_CODE_SUCCESS = 0;
|
|
6
|
+
var EXIT_CODE_GENERAL_ERROR = 1;
|
|
7
|
+
var EXIT_CODE_INVALID_CONFIG = 2;
|
|
8
|
+
|
|
9
|
+
// src/utils/date.ts
|
|
10
|
+
function getCurrentDateString() {
|
|
11
|
+
let date = /* @__PURE__ */ new Date();
|
|
12
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/utils/mongo-relation.ts
|
|
16
|
+
function insertCollectionRelations(model, relations, compareFunc, validateFunc = void 0) {
|
|
17
|
+
if (relations == void 0) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
let relationStack = [];
|
|
21
|
+
for (let relation of relations) {
|
|
22
|
+
if (validateFunc) {
|
|
23
|
+
relation = validateFunc(relation);
|
|
24
|
+
}
|
|
25
|
+
let inStack = false;
|
|
26
|
+
for (let duplicateRelationCheck of relationStack) {
|
|
27
|
+
if (compareFunc(relation, duplicateRelationCheck)) {
|
|
28
|
+
inStack = true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!inStack) {
|
|
32
|
+
relationStack.push(relation);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (relationStack.length > 0) {
|
|
36
|
+
model.insertMany(relationStack);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function mergeCollectionRelations(model, relationsToMerge, match, compareFunc, validateFunc) {
|
|
40
|
+
const relationStack = [];
|
|
41
|
+
if (relationsToMerge != null) {
|
|
42
|
+
for (const relation of relationsToMerge) {
|
|
43
|
+
const validatedRelation = validateFunc(relation);
|
|
44
|
+
if (!relationStack.some(
|
|
45
|
+
(duplicateRelationCheck) => compareFunc(validatedRelation, duplicateRelationCheck)
|
|
46
|
+
)) {
|
|
47
|
+
relationStack.push(validatedRelation);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (relationStack.length === 0) {
|
|
52
|
+
await model.deleteMany(match);
|
|
53
|
+
} else {
|
|
54
|
+
const existingRels = await model.find(match).lean();
|
|
55
|
+
const toRemoveRels = [];
|
|
56
|
+
for (const rel of existingRels) {
|
|
57
|
+
if (!relationStack.some((relationFromStack) => compareFunc(rel, relationFromStack))) {
|
|
58
|
+
toRemoveRels.push(rel);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const toCreateRels = [];
|
|
62
|
+
for (const relationFromStack of relationStack) {
|
|
63
|
+
if (!existingRels.some((rel) => compareFunc(rel, relationFromStack))) {
|
|
64
|
+
toCreateRels.push(relationFromStack);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (toCreateRels.length > 0) {
|
|
68
|
+
await model.insertMany(toCreateRels);
|
|
69
|
+
}
|
|
70
|
+
if (toRemoveRels.length > 0) {
|
|
71
|
+
const idsToRemove = toRemoveRels.map((rel) => rel._id);
|
|
72
|
+
await model.deleteMany({ _id: { $in: idsToRemove } });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/utils/validation.ts
|
|
78
|
+
function isNonEmptyObjectOrArray(value) {
|
|
79
|
+
return value != null && value != void 0 && (Array.isArray(value) && value.length > 0 || typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0);
|
|
80
|
+
}
|
|
81
|
+
function validateString(value, fallback = null) {
|
|
82
|
+
if (value !== void 0 && value !== null && value !== "" && value !== "undefined" && value !== "null") {
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
return fallback;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/utils/env.ts
|
|
89
|
+
var JwtSecret = process.env.JWT_SECRET || "secret";
|
|
90
|
+
var JwtRefreshSecret = process.env.JWT_REFRESH_SECRET || "refresh_secret";
|
|
91
|
+
function isDebug(func = void 0) {
|
|
92
|
+
const dbg = process.env.DEBUG;
|
|
93
|
+
if (dbg !== void 0) {
|
|
94
|
+
const v = dbg.toString().trim().toLowerCase();
|
|
95
|
+
let validation = v === "1" || v === "true" || v === "yes" || v === "on";
|
|
96
|
+
if (validation && func != void 0) {
|
|
97
|
+
func();
|
|
98
|
+
}
|
|
99
|
+
return validation;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/utils/encoding.ts
|
|
105
|
+
function encodeToBase64(obj) {
|
|
106
|
+
return btoa(encodeURIComponent(JSON.stringify(obj)));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/db/mongodb.ts
|
|
110
|
+
async function connectMongoDB(uri) {
|
|
111
|
+
if (uri == null || uri == void 0 || uri.length == 0) {
|
|
112
|
+
console.error(`\u{1F6D1} [${getCurrentDateString()}] Error: MongoDB URI is not provided`);
|
|
113
|
+
process.exit(EXIT_CODE_INVALID_CONFIG);
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
await mongoose.connect(uri);
|
|
117
|
+
console.log(`\u2705 [${getCurrentDateString()}] MongoDB connected`);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error(`\u{1F6D1} [${getCurrentDateString()}] Error: MongoDB connection failed`, err);
|
|
120
|
+
process.exit(EXIT_CODE_GENERAL_ERROR);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/middleware/file-storage.ts
|
|
125
|
+
import multer from "multer";
|
|
126
|
+
import path from "node:path";
|
|
127
|
+
import fs from "node:fs";
|
|
128
|
+
var uploadDir = path.resolve(process.cwd(), "src", "files");
|
|
129
|
+
fs.mkdirSync(uploadDir, { recursive: true });
|
|
130
|
+
var sanitizeFilename = (name) => name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
131
|
+
var diskStorage = multer.diskStorage({
|
|
132
|
+
destination: (_req, _file, next) => next(null, uploadDir),
|
|
133
|
+
filename: (_req, file, next) => {
|
|
134
|
+
const ext = path.extname(file.originalname);
|
|
135
|
+
const base = path.basename(file.originalname, ext);
|
|
136
|
+
const safe = sanitizeFilename(base);
|
|
137
|
+
const unique = Date.now() + "-" + Math.round(Math.random() * 1e9);
|
|
138
|
+
next(null, `${safe}-${unique}${ext}`);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
var memoryStorage = multer.memoryStorage();
|
|
142
|
+
var diskFileUpload = multer({ storage: diskStorage });
|
|
143
|
+
var memoryFileUpload = multer({ storage: memoryStorage });
|
|
144
|
+
|
|
145
|
+
// src/middleware/logger.ts
|
|
146
|
+
function requestLogger(req, res, next) {
|
|
147
|
+
res.on("finish", () => {
|
|
148
|
+
let emoji = "";
|
|
149
|
+
if (res.statusCode >= 100 && res.statusCode < 200) emoji = "\u{1F4A1}";
|
|
150
|
+
else if (res.statusCode >= 200 && res.statusCode < 300) emoji = "\u2705";
|
|
151
|
+
else if (res.statusCode >= 300 && res.statusCode < 400) emoji = "\u{1F6A6}";
|
|
152
|
+
else if (res.statusCode == 401) emoji = "\u{1F501}";
|
|
153
|
+
else if (res.statusCode >= 400 && res.statusCode < 500) emoji = "\u26A0\uFE0F";
|
|
154
|
+
else if (res.statusCode >= 500) emoji = "\u{1F525}";
|
|
155
|
+
console.log(
|
|
156
|
+
`${emoji} [${getCurrentDateString()}] ${req.method} ${req.originalUrl} - ${res.statusCode}`
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
next();
|
|
160
|
+
}
|
|
161
|
+
function errorLogger(err, req, res, next) {
|
|
162
|
+
console.error(`\u{1F6D1} [${getCurrentDateString()}] Error in ${req.method} ${req.originalUrl}:`, err);
|
|
163
|
+
res.status(500).json();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/middleware/authentication/session.ts
|
|
167
|
+
import express from "express";
|
|
168
|
+
|
|
169
|
+
// src/models/auth-user.ts
|
|
170
|
+
import mongoose2 from "mongoose";
|
|
171
|
+
var AuthUserSchema = new mongoose2.Schema(
|
|
172
|
+
{
|
|
173
|
+
identifier: { type: String, unique: true, required: true },
|
|
174
|
+
secretHash: { type: String, select: false },
|
|
175
|
+
blockedSince: { type: Date }
|
|
176
|
+
},
|
|
177
|
+
{ collection: "auth_users", timestamps: true }
|
|
178
|
+
);
|
|
179
|
+
var AuthUser = mongoose2.model("AuthUser", AuthUserSchema);
|
|
180
|
+
|
|
181
|
+
// src/models/token-blocklist.ts
|
|
182
|
+
import mongoose3 from "mongoose";
|
|
183
|
+
var TokenBlocklistSchema = new mongoose3.Schema(
|
|
184
|
+
{
|
|
185
|
+
jti: { type: String, required: true }
|
|
186
|
+
},
|
|
187
|
+
{ collection: "token_blocklist", timestamps: true }
|
|
188
|
+
);
|
|
189
|
+
var TokenBlocklist = mongoose3.model("TokenBlocklist", TokenBlocklistSchema);
|
|
190
|
+
|
|
191
|
+
// src/middleware/authentication/session.ts
|
|
192
|
+
import bcrypt from "bcrypt";
|
|
193
|
+
import jwt2 from "jsonwebtoken";
|
|
194
|
+
|
|
195
|
+
// src/middleware/authentication/validation.ts
|
|
196
|
+
import jwt from "jsonwebtoken";
|
|
197
|
+
var FORBIDDEN = 403;
|
|
198
|
+
var UNAUTHORIZED = 401;
|
|
199
|
+
async function jwtRequired(req, res, next) {
|
|
200
|
+
return validateJwt(req, res, next, JwtSecret);
|
|
201
|
+
}
|
|
202
|
+
async function jwtRefreshRequired(req, res, next) {
|
|
203
|
+
return validateJwt(req, res, next, JwtRefreshSecret);
|
|
204
|
+
}
|
|
205
|
+
async function verifyToken(token, jwtSecret) {
|
|
206
|
+
try {
|
|
207
|
+
const payload = jwt.verify(token, jwtSecret, { ignoreExpiration: true });
|
|
208
|
+
const jti = payload?.jti;
|
|
209
|
+
if (!jti) {
|
|
210
|
+
console.log("[WARN] JWT token without jti");
|
|
211
|
+
return { isTokenValid: false, isTokenExpired: false, isUserBlocked: false, payload };
|
|
212
|
+
}
|
|
213
|
+
const existingBlock = await TokenBlocklist.findOne({ jti });
|
|
214
|
+
if (existingBlock) {
|
|
215
|
+
console.log("[WARN] JWT token is blocked");
|
|
216
|
+
return { isTokenValid: false, isTokenExpired: false, isUserBlocked: false, payload };
|
|
217
|
+
}
|
|
218
|
+
const identifier = payload.identifier;
|
|
219
|
+
if (identifier) {
|
|
220
|
+
const user = await AuthUser.findOne({ identifier });
|
|
221
|
+
if (!user) {
|
|
222
|
+
console.log("[WARN] JWT token for non-existing user");
|
|
223
|
+
return { isTokenValid: false, isTokenExpired: false, isUserBlocked: false, payload };
|
|
224
|
+
}
|
|
225
|
+
if (user.blockedSince != void 0) {
|
|
226
|
+
console.log("[WARN] JWT token for blocked user");
|
|
227
|
+
return { isTokenValid: false, isTokenExpired: false, isUserBlocked: true, payload };
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
console.log("[WARN] JWT token without identifier");
|
|
231
|
+
return { isTokenValid: false, isTokenExpired: false, isUserBlocked: false, payload };
|
|
232
|
+
}
|
|
233
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
234
|
+
if (payload.exp && payload.exp < now) {
|
|
235
|
+
if (jwtSecret == JwtSecret) {
|
|
236
|
+
console.log("[WARN] Access token expired");
|
|
237
|
+
return { isTokenValid: false, isTokenExpired: true, isUserBlocked: false, payload };
|
|
238
|
+
} else {
|
|
239
|
+
console.log("[WARN] Refresh token expired");
|
|
240
|
+
return { isTokenValid: false, isTokenExpired: false, isUserBlocked: false, payload };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { isTokenValid: true, isTokenExpired: false, isUserBlocked: false, payload };
|
|
244
|
+
} catch (err) {
|
|
245
|
+
console.log("[WARN] JWT verification error:", err.message);
|
|
246
|
+
return {
|
|
247
|
+
isTokenValid: false,
|
|
248
|
+
isTokenExpired: false,
|
|
249
|
+
payload: null,
|
|
250
|
+
isUserBlocked: false
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function validateJwt(req, res, next, jwtSecret) {
|
|
255
|
+
const authHeader = req.headers["authorization"];
|
|
256
|
+
if (!authHeader) return res.sendStatus(FORBIDDEN);
|
|
257
|
+
const token = authHeader.split(" ")[1];
|
|
258
|
+
if (!token) return res.sendStatus(FORBIDDEN);
|
|
259
|
+
try {
|
|
260
|
+
const result = await verifyToken(token, jwtSecret);
|
|
261
|
+
if (result.isTokenExpired) {
|
|
262
|
+
console.log("[WARN] Expired JWT token");
|
|
263
|
+
return res.sendStatus(UNAUTHORIZED);
|
|
264
|
+
}
|
|
265
|
+
if (!result.isTokenValid) {
|
|
266
|
+
console.log("[WARN] Invalid JWT token");
|
|
267
|
+
return res.sendStatus(FORBIDDEN);
|
|
268
|
+
}
|
|
269
|
+
if (result.isUserBlocked) {
|
|
270
|
+
console.log("[WARN] JWT token for blocked user");
|
|
271
|
+
return res.sendStatus(FORBIDDEN);
|
|
272
|
+
}
|
|
273
|
+
res.locals.user = result.payload;
|
|
274
|
+
res.locals.token = token;
|
|
275
|
+
next();
|
|
276
|
+
} catch (err) {
|
|
277
|
+
console.log("[ERROR] JWT validation error:", err);
|
|
278
|
+
return res.sendStatus(FORBIDDEN);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async function socketToken(_socket, _next) {
|
|
282
|
+
const token = _socket.handshake.auth?.token;
|
|
283
|
+
if (!token) {
|
|
284
|
+
return _next();
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
const verificationResult = await verifyToken(token, JwtSecret);
|
|
288
|
+
const isValidChar = verificationResult.isTokenValid && !verificationResult.isTokenExpired ? "\u{1F7E2}" : "\u{1F534}";
|
|
289
|
+
console.log(`${isValidChar} Socket verification result: ${JSON.stringify(verificationResult)}`);
|
|
290
|
+
if (!verificationResult.isTokenValid) {
|
|
291
|
+
if (verificationResult.isTokenExpired) {
|
|
292
|
+
return _next(new Error("expired_token"));
|
|
293
|
+
}
|
|
294
|
+
return _next(new Error("invalid_token"));
|
|
295
|
+
}
|
|
296
|
+
_socket.user = verificationResult.payload;
|
|
297
|
+
_next();
|
|
298
|
+
} catch (err) {
|
|
299
|
+
return _next(new Error("invalid_token"));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/middleware/authentication/session.ts
|
|
304
|
+
var router = express.Router();
|
|
305
|
+
var FORBIDDEN2 = 403;
|
|
306
|
+
var BAD_REQUEST = 400;
|
|
307
|
+
async function generateCredentials(auth) {
|
|
308
|
+
let jti = void 0;
|
|
309
|
+
let blocked = void 0;
|
|
310
|
+
do {
|
|
311
|
+
jti = crypto.randomUUID();
|
|
312
|
+
blocked = await TokenBlocklist.findOne({ jti });
|
|
313
|
+
} while (blocked != void 0);
|
|
314
|
+
const user = await AuthUser.findOne({ identifier: auth.identifier }).lean();
|
|
315
|
+
if (!user) {
|
|
316
|
+
return void 0;
|
|
317
|
+
}
|
|
318
|
+
let appUser = void 0;
|
|
319
|
+
try {
|
|
320
|
+
appUser = encodeToBase64(user);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
}
|
|
323
|
+
if (!appUser) {
|
|
324
|
+
return void 0;
|
|
325
|
+
}
|
|
326
|
+
let payload = {
|
|
327
|
+
identifier: auth.identifier,
|
|
328
|
+
jti
|
|
329
|
+
};
|
|
330
|
+
const accessToken = jwt2.sign(payload, JwtSecret, { expiresIn: "1h" });
|
|
331
|
+
const refreshToken = jwt2.sign(payload, JwtRefreshSecret, { expiresIn: "7d" });
|
|
332
|
+
return {
|
|
333
|
+
accessToken,
|
|
334
|
+
refreshToken,
|
|
335
|
+
appUser
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
async function authRegister(req, res, next) {
|
|
339
|
+
let { identifier = void 0, secret = void 0 } = req.body || {};
|
|
340
|
+
if (!identifier || !secret) {
|
|
341
|
+
return res.sendStatus(FORBIDDEN2);
|
|
342
|
+
}
|
|
343
|
+
identifier = identifier.toLowerCase();
|
|
344
|
+
let user = await AuthUser.findOne({ identifier });
|
|
345
|
+
if (user) {
|
|
346
|
+
return res.sendStatus(BAD_REQUEST);
|
|
347
|
+
}
|
|
348
|
+
res.locals.authUser = new AuthUser({
|
|
349
|
+
identifier,
|
|
350
|
+
secretHash: await bcrypt.hash(secret, 10)
|
|
351
|
+
});
|
|
352
|
+
next();
|
|
353
|
+
}
|
|
354
|
+
async function authLogin(req, res, next) {
|
|
355
|
+
let { identifier = void 0, secret = void 0 } = req.body || {};
|
|
356
|
+
if (!identifier || !secret) {
|
|
357
|
+
return res.sendStatus(FORBIDDEN2);
|
|
358
|
+
}
|
|
359
|
+
identifier = identifier.toLowerCase();
|
|
360
|
+
let user = await AuthUser.findOne({ identifier }).select("+secretHash");
|
|
361
|
+
if (!user || !user.secretHash) {
|
|
362
|
+
return res.sendStatus(BAD_REQUEST);
|
|
363
|
+
}
|
|
364
|
+
if (user.blockedSince) {
|
|
365
|
+
return res.sendStatus(FORBIDDEN2);
|
|
366
|
+
}
|
|
367
|
+
let isMatch = await bcrypt.compare(secret, user.secretHash);
|
|
368
|
+
if (!isMatch) {
|
|
369
|
+
return res.sendStatus(BAD_REQUEST);
|
|
370
|
+
}
|
|
371
|
+
let credentials = await generateCredentials(user);
|
|
372
|
+
if (!credentials) {
|
|
373
|
+
return res.sendStatus(BAD_REQUEST);
|
|
374
|
+
}
|
|
375
|
+
res.locals.credentials = credentials;
|
|
376
|
+
next();
|
|
377
|
+
}
|
|
378
|
+
async function authLogout(req, res, next) {
|
|
379
|
+
await jwtRefreshRequired(req, res, async () => {
|
|
380
|
+
const refreshToken = res.locals.token;
|
|
381
|
+
if (!refreshToken) {
|
|
382
|
+
return res.sendStatus(BAD_REQUEST);
|
|
383
|
+
}
|
|
384
|
+
const decoded = jwt2.decode(refreshToken);
|
|
385
|
+
const jti = decoded?.jti;
|
|
386
|
+
if (jti) {
|
|
387
|
+
const existingBlock = await TokenBlocklist.findOne({ jti });
|
|
388
|
+
if (!existingBlock) {
|
|
389
|
+
await new TokenBlocklist({ jti }).save();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
let accessToken = validateString(req.body?.access_token);
|
|
393
|
+
if (accessToken) {
|
|
394
|
+
const accessTokenDecoded = jwt2.decode(accessToken);
|
|
395
|
+
let accessTokenJti = accessTokenDecoded?.jti;
|
|
396
|
+
if (accessTokenJti) {
|
|
397
|
+
const existing = await TokenBlocklist.findOne({ jti: accessTokenJti });
|
|
398
|
+
if (!existing) {
|
|
399
|
+
await new TokenBlocklist({ jti: accessTokenJti }).save();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
next();
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
async function authRefresh(req, res, next) {
|
|
407
|
+
await jwtRefreshRequired(req, res, async () => {
|
|
408
|
+
});
|
|
409
|
+
const refreshToken = res.locals.token;
|
|
410
|
+
if (!refreshToken) {
|
|
411
|
+
return res.sendStatus(BAD_REQUEST);
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
const decoded = jwt2.decode(refreshToken);
|
|
415
|
+
const jti = decoded?.jti;
|
|
416
|
+
if (jti) {
|
|
417
|
+
const existingBlock = await TokenBlocklist.findOne({ jti });
|
|
418
|
+
if (!existingBlock) {
|
|
419
|
+
await new TokenBlocklist({ jti }).save();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
let accessToken = validateString(req.body?.access_token);
|
|
423
|
+
if (accessToken) {
|
|
424
|
+
const accessTokenDecoded = jwt2.decode(accessToken);
|
|
425
|
+
let accessTokenJti = accessTokenDecoded?.jti;
|
|
426
|
+
if (accessTokenJti) {
|
|
427
|
+
const existing = await TokenBlocklist.findOne({
|
|
428
|
+
jti: accessTokenJti
|
|
429
|
+
});
|
|
430
|
+
if (!existing) {
|
|
431
|
+
await new TokenBlocklist({ jti: accessTokenJti }).save();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const payload = jwt2.verify(refreshToken, JwtRefreshSecret);
|
|
436
|
+
let credentials = await generateCredentials(payload);
|
|
437
|
+
if (!credentials) {
|
|
438
|
+
return res.sendStatus(BAD_REQUEST);
|
|
439
|
+
}
|
|
440
|
+
res.locals.credentials = credentials;
|
|
441
|
+
next();
|
|
442
|
+
} catch (err) {
|
|
443
|
+
console.log("[WARN] refreshing JWT:", err);
|
|
444
|
+
return res.sendStatus(BAD_REQUEST);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/fluent-interface/fluent-pattern-handler.ts
|
|
449
|
+
import mongoose5 from "mongoose";
|
|
450
|
+
|
|
451
|
+
// src/fluent-interface/types.ts
|
|
452
|
+
var fluentRequestQueryAttributes = {
|
|
453
|
+
id: "",
|
|
454
|
+
ids: "",
|
|
455
|
+
filter: "",
|
|
456
|
+
offset: "",
|
|
457
|
+
limit: "",
|
|
458
|
+
order: "",
|
|
459
|
+
excluded: ""
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
// src/core/query-builder.ts
|
|
463
|
+
import mongoose4 from "mongoose";
|
|
464
|
+
|
|
465
|
+
// src/core/types.ts
|
|
466
|
+
var Codec = class {
|
|
467
|
+
constructor(content, code = 202) {
|
|
468
|
+
this.content = content;
|
|
469
|
+
this.returnCode = code;
|
|
470
|
+
}
|
|
471
|
+
sendToClient(res) {
|
|
472
|
+
if (this.content == null) {
|
|
473
|
+
res.status(404).json({});
|
|
474
|
+
}
|
|
475
|
+
res.status(this.returnCode).json(this.content);
|
|
476
|
+
}
|
|
477
|
+
is1xx() {
|
|
478
|
+
return this.returnCode >= 100 && this.returnCode <= 199;
|
|
479
|
+
}
|
|
480
|
+
is2xx() {
|
|
481
|
+
return this.returnCode >= 200 && this.returnCode <= 299;
|
|
482
|
+
}
|
|
483
|
+
is3xx() {
|
|
484
|
+
return this.returnCode >= 300 && this.returnCode <= 399;
|
|
485
|
+
}
|
|
486
|
+
is4xx() {
|
|
487
|
+
return this.returnCode >= 400 && this.returnCode <= 499;
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
var JoinRelation = class {
|
|
491
|
+
constructor(localField, ref, alias = void 0) {
|
|
492
|
+
this.ref = ref;
|
|
493
|
+
this.localField = localField;
|
|
494
|
+
this.alias = alias ? alias : ref.collection.name;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
var HttpMethod = /* @__PURE__ */ ((HttpMethod2) => {
|
|
498
|
+
HttpMethod2["POST"] = "post";
|
|
499
|
+
HttpMethod2["GET"] = "get";
|
|
500
|
+
HttpMethod2["DELETE"] = "delete";
|
|
501
|
+
HttpMethod2["PUT"] = "put";
|
|
502
|
+
return HttpMethod2;
|
|
503
|
+
})(HttpMethod || {});
|
|
504
|
+
|
|
505
|
+
// src/core/query-builder.ts
|
|
506
|
+
var QueryBuilder = class {
|
|
507
|
+
constructor(config) {
|
|
508
|
+
this._matchConditions = {};
|
|
509
|
+
this._stages = [];
|
|
510
|
+
this._relations = [];
|
|
511
|
+
this._unsetFields = [];
|
|
512
|
+
this._config = config;
|
|
513
|
+
this._applyPathOptions();
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Generates the aggregation pipeline based on the current configuration of the QueryBuilder.
|
|
517
|
+
* @returns
|
|
518
|
+
*/
|
|
519
|
+
getAggregationPipeline() {
|
|
520
|
+
return this._generatePipeline();
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Returns the current configuration of the QueryBuilder, including model, select fields, and any applied options.
|
|
524
|
+
* @returns
|
|
525
|
+
*/
|
|
526
|
+
getConfig() {
|
|
527
|
+
return this._config;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Applies schema-based options such as joins and unset fields.
|
|
531
|
+
*/
|
|
532
|
+
_applyPathOptions() {
|
|
533
|
+
this._generateSchemaJoins(this._config.model.schema);
|
|
534
|
+
this._generateSchemaUnsetList(this._config);
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Adds match conditions to the query builder.
|
|
538
|
+
* @param match - The match conditions to add.
|
|
539
|
+
* @param conjunction - The logical conjunction ('and' or 'or').
|
|
540
|
+
* @param append - Whether to append to existing conditions or replace them.
|
|
541
|
+
*/
|
|
542
|
+
match(match, conjunction = "and", append = true) {
|
|
543
|
+
if (!match || (Array.isArray(match) ? match.length === 0 : Object.keys(match).length === 0))
|
|
544
|
+
return;
|
|
545
|
+
if (!append) {
|
|
546
|
+
this._matchConditions = Array.isArray(match) ? { $and: match } : match;
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const key = `$${conjunction}`;
|
|
550
|
+
if (!this._matchConditions[key]) this._matchConditions[key] = [];
|
|
551
|
+
if (Array.isArray(match)) {
|
|
552
|
+
this._matchConditions[key].push(...match);
|
|
553
|
+
} else {
|
|
554
|
+
this._matchConditions[key].push(match);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Adds aggregation stages to the query builder.
|
|
559
|
+
* @param stages
|
|
560
|
+
* @returns
|
|
561
|
+
*/
|
|
562
|
+
stage(stages) {
|
|
563
|
+
if (!stages) return;
|
|
564
|
+
if (Array.isArray(stages)) {
|
|
565
|
+
this._stages.push(...globalThis.structuredClone(stages));
|
|
566
|
+
} else {
|
|
567
|
+
this._stages.push(globalThis.structuredClone(stages));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Adds join relations to the query builder.
|
|
572
|
+
* @param rels
|
|
573
|
+
* @returns
|
|
574
|
+
*/
|
|
575
|
+
join(rels) {
|
|
576
|
+
if (!rels) return;
|
|
577
|
+
const list = Array.isArray(rels) ? rels : [rels];
|
|
578
|
+
list.forEach((rel) => this._relations.push(rel));
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Calculates the $lookup stage for a given JoinRelation.
|
|
582
|
+
* @param relation
|
|
583
|
+
* @returns
|
|
584
|
+
*/
|
|
585
|
+
_calculateJoin(relation) {
|
|
586
|
+
if (!(relation instanceof JoinRelation)) throw new Error("relation must be JoinRelation");
|
|
587
|
+
return {
|
|
588
|
+
$lookup: {
|
|
589
|
+
from: relation.ref.collection.name,
|
|
590
|
+
localField: relation.localField,
|
|
591
|
+
foreignField: "_id",
|
|
592
|
+
as: relation.alias
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Automatically generates JoinRelation objects from schema refs.
|
|
598
|
+
* @param model The Mongoose model to scan.
|
|
599
|
+
* @param prefix Optional prefix for nested paths (e.g., 'alias.field').
|
|
600
|
+
* @returns Array of JoinRelation objects.
|
|
601
|
+
*/
|
|
602
|
+
_generateSchemaJoins(schema, prefix = "") {
|
|
603
|
+
schema.eachPath((path2, schematype) => {
|
|
604
|
+
const fullPath = prefix ? `${prefix}.${path2}` : path2;
|
|
605
|
+
if (schematype.options?.ref) {
|
|
606
|
+
const refModelName = schematype.options.ref;
|
|
607
|
+
try {
|
|
608
|
+
const refModel = mongoose4.model(refModelName);
|
|
609
|
+
let alias = schematype.options?.alias ?? refModel.collection.name;
|
|
610
|
+
this.join(new JoinRelation(fullPath, refModel, alias));
|
|
611
|
+
} catch (err) {
|
|
612
|
+
console.warn(
|
|
613
|
+
`[QueryBuilder] Could not resolve ref model '${refModelName}' for path '${fullPath}'`
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (schematype instanceof mongoose4.Schema.Types.Array && schematype.caster?.options?.ref) {
|
|
618
|
+
const refModelName = schematype.caster.options.ref;
|
|
619
|
+
try {
|
|
620
|
+
const refModel = mongoose4.model(refModelName);
|
|
621
|
+
let alias = schematype.caster.options?.alias ?? refModel.collection.name;
|
|
622
|
+
this.join(new JoinRelation(fullPath, refModel, alias));
|
|
623
|
+
} catch (err) {
|
|
624
|
+
console.warn(
|
|
625
|
+
`[QueryBuilder] Could not resolve array ref model '${refModelName}' for path '${fullPath}'`
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Generates the list of fields to unset based on schema select options.
|
|
633
|
+
* @param config - The query request configuration.
|
|
634
|
+
*/
|
|
635
|
+
_generateSchemaUnsetList(config) {
|
|
636
|
+
this._unsetFields = [];
|
|
637
|
+
let unset = this._collectSelectFalse(config.model.schema, void 0, config.select);
|
|
638
|
+
for (const relation of this._relations) {
|
|
639
|
+
unset = unset.concat(
|
|
640
|
+
this._collectSelectFalse(relation.ref.schema, relation.alias, config.select)
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
this._unsetFields = Array.from(new Set(unset));
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Collects paths from the schema where select is set to false.
|
|
647
|
+
* @param schema - The Mongoose schema to scan.
|
|
648
|
+
* @param prefix - Optional prefix for nested paths.
|
|
649
|
+
* @param select - Optional array of fields to select.
|
|
650
|
+
* @returns Array of paths to unset.
|
|
651
|
+
*/
|
|
652
|
+
_collectSelectFalse(schema, prefix = void 0, select = void 0) {
|
|
653
|
+
const unset = [];
|
|
654
|
+
schema.eachPath((path2, schematype) => {
|
|
655
|
+
if (select && select.length > 0 && !select.includes(path2) && schematype?.options?.select !== false) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (schematype?.options?.select === false) {
|
|
659
|
+
unset.push(prefix ? `${prefix}.${path2}` : path2);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
return unset;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Generates the aggregation pipeline based on joins, stages, match conditions, and unset fields.
|
|
666
|
+
* @returns The aggregation pipeline array.
|
|
667
|
+
*/
|
|
668
|
+
_generatePipeline() {
|
|
669
|
+
const pipeline = [];
|
|
670
|
+
for (const rel of this._relations) {
|
|
671
|
+
pipeline.push(this._calculateJoin(rel));
|
|
672
|
+
}
|
|
673
|
+
pipeline.push(...this._stages);
|
|
674
|
+
if (Object.keys(this._matchConditions).length) {
|
|
675
|
+
pipeline.push({ $match: this._matchConditions });
|
|
676
|
+
}
|
|
677
|
+
if (this._unsetFields.length) {
|
|
678
|
+
pipeline.push({ $unset: this._unsetFields });
|
|
679
|
+
}
|
|
680
|
+
return pipeline;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Executes the aggregation pipeline and returns the results.
|
|
684
|
+
* @param config - Parameters for the query execution.
|
|
685
|
+
* @returns The collection response wrapped in a Codec.
|
|
686
|
+
*/
|
|
687
|
+
async exec(config) {
|
|
688
|
+
try {
|
|
689
|
+
const pipeline = this._generatePipeline();
|
|
690
|
+
const countPipeline = [...pipeline, { $count: "n" }];
|
|
691
|
+
const queryPipeline = [...pipeline];
|
|
692
|
+
if (config && !config.isOne) {
|
|
693
|
+
if (config.skip) queryPipeline.push({ $skip: config.skip });
|
|
694
|
+
if (config.limit) queryPipeline.push({ $limit: config.limit });
|
|
695
|
+
}
|
|
696
|
+
const [countRes, res] = await Promise.all([
|
|
697
|
+
this._config.model.aggregate(countPipeline).exec(),
|
|
698
|
+
this._config.model.aggregate(queryPipeline).exec()
|
|
699
|
+
]);
|
|
700
|
+
const totalCount = countRes && countRes[0] ? countRes[0].n : 0;
|
|
701
|
+
const documents = config && config.isOne ? await this._processSingleDocument(res) : await this._processMultipleDocuments(res);
|
|
702
|
+
return new Codec({ data: documents, meta: { total: totalCount } }, 200);
|
|
703
|
+
} catch (err) {
|
|
704
|
+
console.error("[ERROR - QueryBuilder]", err);
|
|
705
|
+
return new Codec({ data: [], meta: { total: 0 } }, 500);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Processes a single document from the aggregation result.
|
|
710
|
+
* @param res - The aggregation result array.
|
|
711
|
+
* @returns The processed document or null if none.
|
|
712
|
+
*/
|
|
713
|
+
async _processSingleDocument(res) {
|
|
714
|
+
if (!res || res.length === 0) {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
let doc = this._config.model.hydrate(res[0]);
|
|
718
|
+
if (this._config.eachFunc) {
|
|
719
|
+
doc = this._config.eachFunc(doc);
|
|
720
|
+
} else if (this._config.asyncEachFunc) {
|
|
721
|
+
doc = await this._config.asyncEachFunc(doc);
|
|
722
|
+
}
|
|
723
|
+
return doc;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Processes multiple documents from the aggregation result.
|
|
727
|
+
* @param res - The aggregation result array.
|
|
728
|
+
* @returns The array of processed documents.
|
|
729
|
+
*/
|
|
730
|
+
async _processMultipleDocuments(res) {
|
|
731
|
+
let final = (res || []).map((doc) => this._config.model.hydrate(doc));
|
|
732
|
+
if (this._config.eachFunc) {
|
|
733
|
+
final = final.map(this._config.eachFunc);
|
|
734
|
+
} else if (this._config.asyncEachFunc) {
|
|
735
|
+
const asyncFinal = [];
|
|
736
|
+
for (const doc of final) {
|
|
737
|
+
const newDoc = await this._config.asyncEachFunc(doc);
|
|
738
|
+
asyncFinal.push(newDoc);
|
|
739
|
+
}
|
|
740
|
+
final = asyncFinal;
|
|
741
|
+
}
|
|
742
|
+
return final;
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
// src/core/http.ts
|
|
747
|
+
function formDataParser(obj) {
|
|
748
|
+
return new URLSearchParams(obj).toString();
|
|
749
|
+
}
|
|
750
|
+
async function httpRequest(config) {
|
|
751
|
+
const url = new URL(config.url);
|
|
752
|
+
if (config.urlSearchParams) {
|
|
753
|
+
Object.keys(config.urlSearchParams).forEach((key) => {
|
|
754
|
+
url.searchParams.set(key, String(config.urlSearchParams[key]));
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
let body;
|
|
758
|
+
let defaultContentType = "application/x-www-form-urlencoded";
|
|
759
|
+
if (config.body) {
|
|
760
|
+
if (typeof config.body === "object") {
|
|
761
|
+
body = JSON.stringify(config.body);
|
|
762
|
+
defaultContentType = "application/json";
|
|
763
|
+
} else {
|
|
764
|
+
body = String(config.body);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
const fetchImpl = globalThis.fetch;
|
|
768
|
+
if (!fetchImpl) {
|
|
769
|
+
throw new Error("fetch() is not available. Please use Node.js 18+ or add a fetch polyfill.");
|
|
770
|
+
}
|
|
771
|
+
const defaultHeaders = {};
|
|
772
|
+
if (!config.headers || !Object.keys(config.headers).find((key) => key.toLowerCase() === "content-type")) {
|
|
773
|
+
defaultHeaders["Content-Type"] = defaultContentType;
|
|
774
|
+
}
|
|
775
|
+
const response = await fetchImpl(url.toString(), {
|
|
776
|
+
method: config.method?.toUpperCase() || "GET",
|
|
777
|
+
body,
|
|
778
|
+
headers: {
|
|
779
|
+
...defaultHeaders,
|
|
780
|
+
...config.headers
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
const contentType = response.headers.get("content-type") || "";
|
|
784
|
+
const isJson = contentType.includes("application/json");
|
|
785
|
+
const isBlob = contentType.includes("application/octet-stream");
|
|
786
|
+
if (!response.ok) {
|
|
787
|
+
const text = await response.text().catch(() => "");
|
|
788
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}: ${text}`);
|
|
789
|
+
}
|
|
790
|
+
let result;
|
|
791
|
+
if (isJson) {
|
|
792
|
+
result = await response.json().catch(async (error) => {
|
|
793
|
+
throw new Error(`Failed to parse JSON response: ${error.message}`);
|
|
794
|
+
});
|
|
795
|
+
} else if (isBlob) {
|
|
796
|
+
result = await response.blob().catch(async (error) => {
|
|
797
|
+
throw new Error(`Failed to get blob response: ${error.message}`);
|
|
798
|
+
});
|
|
799
|
+
} else {
|
|
800
|
+
result = await response.text().catch(async (error) => {
|
|
801
|
+
throw new Error(`Failed to get text response: ${error.message}`);
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
return result;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// src/fluent-interface/fluent-pattern-handler.ts
|
|
808
|
+
var FluentPatternHandler = class _FluentPatternHandler {
|
|
809
|
+
/**
|
|
810
|
+
* Constructor for QueryPatternExecutor.
|
|
811
|
+
* @param paths - Array of query pattern paths for filtering.
|
|
812
|
+
*/
|
|
813
|
+
constructor(paths = []) {
|
|
814
|
+
if (_FluentPatternHandler._singleton) {
|
|
815
|
+
throw new Error(
|
|
816
|
+
"FluentPatternHandler is a singleton class. Use FluentPatternHandler.getInstance() to access the instance."
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
this._paths = paths;
|
|
820
|
+
_FluentPatternHandler._singleton = this;
|
|
821
|
+
}
|
|
822
|
+
static getInstance() {
|
|
823
|
+
if (_FluentPatternHandler._singleton == void 0) {
|
|
824
|
+
throw new Error(
|
|
825
|
+
"FluentPatternHandler instance has not been created yet. Please create an instance before calling getInstance()."
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
return _FluentPatternHandler._singleton;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Parses and validates query parameters from the request.
|
|
832
|
+
* @param query - The query object from the request.
|
|
833
|
+
* @returns Parsed query parameters.
|
|
834
|
+
*/
|
|
835
|
+
_parseFluentRequestQuery(query) {
|
|
836
|
+
const { filter, limit = "5", offset = "0", id, excluded: excludedJSON, ids: idsJSON } = query;
|
|
837
|
+
const queryKeys = Object.keys(fluentRequestQueryAttributes);
|
|
838
|
+
let filterFields = {};
|
|
839
|
+
for (const [key, value] of Object.entries(query)) {
|
|
840
|
+
if (queryKeys.includes(key)) continue;
|
|
841
|
+
if (value == void 0) continue;
|
|
842
|
+
try {
|
|
843
|
+
filterFields[key] = typeof value === "string" ? value : JSON.stringify(value);
|
|
844
|
+
} catch (e) {
|
|
845
|
+
filterFields[key] = String(value);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
let excluded;
|
|
849
|
+
if (excludedJSON) {
|
|
850
|
+
try {
|
|
851
|
+
excluded = JSON.parse(excludedJSON);
|
|
852
|
+
if (!Array.isArray(excluded)) throw new Error("Excluded must be an array");
|
|
853
|
+
} catch (error) {
|
|
854
|
+
console.warn("[QueryPatternExecutor] Invalid excluded parameter:", error);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
let ids;
|
|
858
|
+
if (idsJSON) {
|
|
859
|
+
try {
|
|
860
|
+
ids = JSON.parse(idsJSON);
|
|
861
|
+
if (!Array.isArray(ids)) throw new Error("Ids must be an array");
|
|
862
|
+
} catch (error) {
|
|
863
|
+
console.warn("[QueryPatternExecutor] Invalid ids parameter:", error);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return { filter, limit, offset, id, excluded, ids, filterFields };
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Applies filters to the query builder based on parsed parameters and options.
|
|
870
|
+
* @param queryBuilder - The QueryBuilder instance.
|
|
871
|
+
* @param params - Parsed query parameters.
|
|
872
|
+
* @param config - Query request configuration.
|
|
873
|
+
*/
|
|
874
|
+
_applyParameters(queryBuilder, params) {
|
|
875
|
+
const { id, ids, filter, excluded } = params;
|
|
876
|
+
if (id) {
|
|
877
|
+
queryBuilder.match({ _id: new mongoose5.Types.ObjectId(id) });
|
|
878
|
+
} else if (ids && ids.length > 0) {
|
|
879
|
+
const objectIds = ids.map((id2) => new mongoose5.Types.ObjectId(id2));
|
|
880
|
+
queryBuilder.match({ _id: { $in: objectIds } });
|
|
881
|
+
} else {
|
|
882
|
+
const modelFilterFields = this._getFilterFieldsForModel(queryBuilder.getConfig().model);
|
|
883
|
+
if (filter && modelFilterFields.length > 0) {
|
|
884
|
+
const ors = modelFilterFields.map((field) => ({
|
|
885
|
+
[field]: { $regex: filter, $options: "i" }
|
|
886
|
+
}));
|
|
887
|
+
queryBuilder.match({ $or: ors });
|
|
888
|
+
}
|
|
889
|
+
if (params.filterFields && Object.keys(params.filterFields).length > 0) {
|
|
890
|
+
for (const [field, value] of Object.entries(params.filterFields)) {
|
|
891
|
+
if (!modelFilterFields.includes(field)) {
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
const match = {};
|
|
895
|
+
match[field] = { $regex: value, $options: "i" };
|
|
896
|
+
queryBuilder.match(match);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if (excluded && excluded.length > 0) {
|
|
900
|
+
const objectIds = excluded.map((id2) => new mongoose5.Types.ObjectId(id2));
|
|
901
|
+
queryBuilder.match({ _id: { $nin: objectIds } });
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Retrieves filter fields for the given model from the paths configuration.
|
|
907
|
+
* @param model - The Mongoose model.
|
|
908
|
+
* @returns Array of filter fields.
|
|
909
|
+
*/
|
|
910
|
+
_getFilterFieldsForModel(model) {
|
|
911
|
+
for (const path2 of this._paths) {
|
|
912
|
+
if (path2.model.collection.name === model.collection.name) {
|
|
913
|
+
if (path2.filters) {
|
|
914
|
+
return path2.filters;
|
|
915
|
+
}
|
|
916
|
+
return [];
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return [];
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Builds execution parameters for the query builder.
|
|
923
|
+
* @param params - Parsed query parameters.
|
|
924
|
+
* @returns Execution parameters.
|
|
925
|
+
*/
|
|
926
|
+
_buildExecutionConfig(params) {
|
|
927
|
+
const { id, limit, offset } = params;
|
|
928
|
+
return {
|
|
929
|
+
isOne: Boolean(id),
|
|
930
|
+
limit: limit === "full" ? void 0 : parseInt(limit || "5", 10),
|
|
931
|
+
skip: parseInt(offset || "0", 10)
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Executes the query builder with the applied filters and execution parameters.
|
|
936
|
+
* @param req
|
|
937
|
+
* @param queryBuilder
|
|
938
|
+
* @returns
|
|
939
|
+
*/
|
|
940
|
+
async exec(req, queryBuilder) {
|
|
941
|
+
try {
|
|
942
|
+
const queryParams = this._parseFluentRequestQuery(req.query);
|
|
943
|
+
this._applyParameters(queryBuilder, queryParams);
|
|
944
|
+
const execConfig = this._buildExecutionConfig(queryParams);
|
|
945
|
+
return await queryBuilder.exec(execConfig);
|
|
946
|
+
} catch (err) {
|
|
947
|
+
console.error("[ERROR - QueryPatternExecutor]", err);
|
|
948
|
+
return new Codec({ data: [], meta: { total: 0 } }, 500);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
// src/app.ts
|
|
954
|
+
import express2 from "express";
|
|
955
|
+
import cors from "cors";
|
|
956
|
+
function createApp() {
|
|
957
|
+
const app = express2();
|
|
958
|
+
app.use(cors());
|
|
959
|
+
app.use(express2.json());
|
|
960
|
+
app.use(express2.urlencoded({ extended: true }));
|
|
961
|
+
app.use(requestLogger);
|
|
962
|
+
app.use(errorLogger);
|
|
963
|
+
return app;
|
|
964
|
+
}
|
|
965
|
+
export {
|
|
966
|
+
AuthUser,
|
|
967
|
+
Codec,
|
|
968
|
+
EXIT_CODE_GENERAL_ERROR,
|
|
969
|
+
EXIT_CODE_INVALID_CONFIG,
|
|
970
|
+
EXIT_CODE_SUCCESS,
|
|
971
|
+
FluentPatternHandler,
|
|
972
|
+
HttpMethod,
|
|
973
|
+
JoinRelation,
|
|
974
|
+
JwtRefreshSecret,
|
|
975
|
+
JwtSecret,
|
|
976
|
+
QueryBuilder,
|
|
977
|
+
TokenBlocklist,
|
|
978
|
+
authLogin,
|
|
979
|
+
authLogout,
|
|
980
|
+
authRefresh,
|
|
981
|
+
authRegister,
|
|
982
|
+
connectMongoDB,
|
|
983
|
+
createApp,
|
|
984
|
+
diskFileUpload,
|
|
985
|
+
encodeToBase64,
|
|
986
|
+
errorLogger,
|
|
987
|
+
fluentRequestQueryAttributes,
|
|
988
|
+
formDataParser,
|
|
989
|
+
getCurrentDateString,
|
|
990
|
+
httpRequest,
|
|
991
|
+
insertCollectionRelations,
|
|
992
|
+
isDebug,
|
|
993
|
+
isNonEmptyObjectOrArray,
|
|
994
|
+
jwtRefreshRequired,
|
|
995
|
+
jwtRequired,
|
|
996
|
+
memoryFileUpload,
|
|
997
|
+
mergeCollectionRelations,
|
|
998
|
+
requestLogger,
|
|
999
|
+
socketToken,
|
|
1000
|
+
validateString,
|
|
1001
|
+
verifyToken
|
|
15
1002
|
};
|
|
16
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
__exportStar(require("./db/index"), exports);
|
|
18
|
-
__exportStar(require("./middleware/index"), exports);
|
|
19
|
-
__exportStar(require("./fluent-interface/index"), exports);
|
|
20
|
-
__exportStar(require("./core/index"), exports);
|
|
21
|
-
__exportStar(require("./utils/index"), exports);
|
|
22
|
-
__exportStar(require("./app"), exports);
|
|
23
|
-
__exportStar(require("./types"), exports);
|
|
24
|
-
__exportStar(require("./exitCodes"), exports);
|
|
25
|
-
//# sourceMappingURL=index.js.map
|