tsledge 0.1.4 → 0.1.6

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