tsledge-core 0.1.0
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/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/app.d.ts +14 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/bin/repl.js +1181 -0
- package/dist/bin/repl.js.map +7 -0
- package/dist/core/http.d.ts +15 -0
- package/dist/core/http.d.ts.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/query-builder.d.ts +96 -0
- package/dist/core/query-builder.d.ts.map +1 -0
- package/dist/core/types.d.ts +58 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/mongodb.d.ts +6 -0
- package/dist/db/mongodb.d.ts.map +1 -0
- package/dist/exitCodes.d.ts +4 -0
- package/dist/exitCodes.d.ts.map +1 -0
- package/dist/fluent-interface/fluent-pattern-handler.d.ts +60 -0
- package/dist/fluent-interface/fluent-pattern-handler.d.ts.map +1 -0
- package/dist/fluent-interface/index.d.ts +3 -0
- package/dist/fluent-interface/index.d.ts.map +1 -0
- package/dist/fluent-interface/types.d.ts +26 -0
- package/dist/fluent-interface/types.d.ts.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- 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 +63 -0
- package/dist/middleware/authentication/session.d.ts.map +1 -0
- package/dist/middleware/authentication/types.d.ts +34 -0
- package/dist/middleware/authentication/types.d.ts.map +1 -0
- package/dist/middleware/authentication/validation.d.ts +64 -0
- package/dist/middleware/authentication/validation.d.ts.map +1 -0
- package/dist/middleware/file-storage.d.ts +4 -0
- package/dist/middleware/file-storage.d.ts.map +1 -0
- package/dist/middleware/index.d.ts +5 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/logger.d.ts +4 -0
- package/dist/middleware/logger.d.ts.map +1 -0
- package/dist/middleware/types.d.ts +2 -0
- package/dist/middleware/types.d.ts.map +1 -0
- package/dist/models/auth-token-blocklist.d.ts +13 -0
- package/dist/models/auth-token-blocklist.d.ts.map +1 -0
- package/dist/models/auth-user.d.ts +15 -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/src/index.js +1159 -0
- package/dist/src/index.js.map +7 -0
- package/dist/tests/main.js +1111 -0
- package/dist/tests/main.js.map +7 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/date.d.ts +2 -0
- package/dist/utils/date.d.ts.map +1 -0
- package/dist/utils/encoding.d.ts +13 -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 +6 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/mongo-relation.d.ts +21 -0
- package/dist/utils/mongo-relation.d.ts.map +1 -0
- package/dist/utils/validation.d.ts +59 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,1111 @@
|
|
|
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
|
+
import bcrypt from "bcrypt";
|
|
94
|
+
import jwt2 from "jsonwebtoken";
|
|
95
|
+
|
|
96
|
+
// src/middleware/authentication/validation.ts
|
|
97
|
+
import jwt from "jsonwebtoken";
|
|
98
|
+
|
|
99
|
+
// src/models/auth-user.ts
|
|
100
|
+
import mongoose2 from "mongoose";
|
|
101
|
+
var AuthUserSchema = new mongoose2.Schema(
|
|
102
|
+
{
|
|
103
|
+
identifier: { type: String, unique: true, required: true },
|
|
104
|
+
secretHash: { type: String, select: false },
|
|
105
|
+
blockedSince: { type: Date }
|
|
106
|
+
},
|
|
107
|
+
{ collection: "auth_users", timestamps: true }
|
|
108
|
+
);
|
|
109
|
+
var AuthUserModel = mongoose2.model("AuthUser", AuthUserSchema);
|
|
110
|
+
|
|
111
|
+
// src/models/auth-token-blocklist.ts
|
|
112
|
+
import mongoose3 from "mongoose";
|
|
113
|
+
var AuthTokenBlocklistSchema = new mongoose3.Schema(
|
|
114
|
+
{
|
|
115
|
+
jti: { type: String, required: true }
|
|
116
|
+
},
|
|
117
|
+
{ collection: "token_blocklist", timestamps: true }
|
|
118
|
+
);
|
|
119
|
+
var AuthTokenBlocklistModel = mongoose3.model(
|
|
120
|
+
"AuthTokenBlocklist",
|
|
121
|
+
AuthTokenBlocklistSchema
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// src/middleware/authentication/validation.ts
|
|
125
|
+
var FORBIDDEN = 403;
|
|
126
|
+
var UNAUTHORIZED = 401;
|
|
127
|
+
async function jwtRequired(req, res, next) {
|
|
128
|
+
await validateJwt(req, res, next, JwtSecret);
|
|
129
|
+
}
|
|
130
|
+
async function jwtRefreshRequired(req, res, next) {
|
|
131
|
+
await validateJwt(req, res, next, JwtRefreshSecret);
|
|
132
|
+
}
|
|
133
|
+
async function verifyToken(token, jwtSecret) {
|
|
134
|
+
try {
|
|
135
|
+
const payload = jwt.verify(token, jwtSecret, { ignoreExpiration: true });
|
|
136
|
+
const jti = payload?.jti;
|
|
137
|
+
if (!jti) {
|
|
138
|
+
console.log("[WARN] JWT token without jti");
|
|
139
|
+
return { isTokenValid: false, isTokenExpired: false, isUserBlocked: false, payload };
|
|
140
|
+
}
|
|
141
|
+
const existingBlock = await AuthTokenBlocklistModel.findOne({ jti });
|
|
142
|
+
if (existingBlock) {
|
|
143
|
+
console.log("[WARN] JWT token is blocked");
|
|
144
|
+
return { isTokenValid: false, isTokenExpired: false, isUserBlocked: false, payload };
|
|
145
|
+
}
|
|
146
|
+
const identifier = payload.identifier;
|
|
147
|
+
if (identifier) {
|
|
148
|
+
const user = await AuthUserModel.findOne({ identifier });
|
|
149
|
+
if (!user) {
|
|
150
|
+
console.log("[WARN] JWT token for non-existing user");
|
|
151
|
+
return { isTokenValid: false, isTokenExpired: false, isUserBlocked: false, payload };
|
|
152
|
+
}
|
|
153
|
+
if (user.blockedSince != void 0) {
|
|
154
|
+
console.log("[WARN] JWT token for blocked user");
|
|
155
|
+
return { isTokenValid: false, isTokenExpired: false, isUserBlocked: true, payload };
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
console.log("[WARN] JWT token without identifier");
|
|
159
|
+
return { isTokenValid: false, isTokenExpired: false, isUserBlocked: false, payload };
|
|
160
|
+
}
|
|
161
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
162
|
+
if (payload.exp && payload.exp < now) {
|
|
163
|
+
if (jwtSecret == JwtSecret) {
|
|
164
|
+
console.log("[WARN] Access token expired");
|
|
165
|
+
return { isTokenValid: false, isTokenExpired: true, isUserBlocked: false, payload };
|
|
166
|
+
} else {
|
|
167
|
+
console.log("[WARN] Refresh token expired");
|
|
168
|
+
return { isTokenValid: false, isTokenExpired: false, isUserBlocked: false, payload };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { isTokenValid: true, isTokenExpired: false, isUserBlocked: false, payload };
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.log("[WARN] JWT verification error:", err.message);
|
|
174
|
+
return {
|
|
175
|
+
isTokenValid: false,
|
|
176
|
+
isTokenExpired: false,
|
|
177
|
+
payload: null,
|
|
178
|
+
isUserBlocked: false
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function validateJwt(req, res, next, jwtSecret) {
|
|
183
|
+
const authHeader = req.headers["authorization"];
|
|
184
|
+
if (!authHeader) return res.sendStatus(FORBIDDEN);
|
|
185
|
+
const token = authHeader.split(" ")[1];
|
|
186
|
+
if (!token) return res.sendStatus(FORBIDDEN);
|
|
187
|
+
try {
|
|
188
|
+
const result = await verifyToken(token, jwtSecret);
|
|
189
|
+
if (result.isTokenExpired) {
|
|
190
|
+
console.log("[WARN] Expired JWT token");
|
|
191
|
+
return res.sendStatus(UNAUTHORIZED);
|
|
192
|
+
}
|
|
193
|
+
if (!result.isTokenValid) {
|
|
194
|
+
console.log("[WARN] Invalid JWT token");
|
|
195
|
+
return res.sendStatus(FORBIDDEN);
|
|
196
|
+
}
|
|
197
|
+
if (result.isUserBlocked) {
|
|
198
|
+
console.log("[WARN] JWT token for blocked user");
|
|
199
|
+
return res.sendStatus(FORBIDDEN);
|
|
200
|
+
}
|
|
201
|
+
res.locals.authUserPayload = result.payload;
|
|
202
|
+
res.locals.token = token;
|
|
203
|
+
next();
|
|
204
|
+
} catch (err) {
|
|
205
|
+
console.log("[ERROR] JWT validation error:", err);
|
|
206
|
+
return res.sendStatus(FORBIDDEN);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/middleware/authentication/session.ts
|
|
211
|
+
var router = express.Router();
|
|
212
|
+
var FORBIDDEN2 = 403;
|
|
213
|
+
var BAD_REQUEST = 400;
|
|
214
|
+
async function generateCredentials(auth) {
|
|
215
|
+
let jti = void 0;
|
|
216
|
+
let blocked = void 0;
|
|
217
|
+
do {
|
|
218
|
+
jti = crypto.randomUUID();
|
|
219
|
+
blocked = await AuthTokenBlocklistModel.findOne({ jti });
|
|
220
|
+
} while (blocked != void 0);
|
|
221
|
+
const user = await AuthUserModel.findOne({ identifier: auth.identifier }).lean();
|
|
222
|
+
if (!user) {
|
|
223
|
+
return void 0;
|
|
224
|
+
}
|
|
225
|
+
let appUser = void 0;
|
|
226
|
+
try {
|
|
227
|
+
appUser = encodeToBase64(user);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
}
|
|
230
|
+
if (!appUser) {
|
|
231
|
+
return void 0;
|
|
232
|
+
}
|
|
233
|
+
let payload = {
|
|
234
|
+
identifier: auth.identifier,
|
|
235
|
+
jti
|
|
236
|
+
};
|
|
237
|
+
const accessToken = jwt2.sign(payload, JwtSecret, { expiresIn: "1h" });
|
|
238
|
+
const refreshToken = jwt2.sign(payload, JwtRefreshSecret, { expiresIn: "7d" });
|
|
239
|
+
return {
|
|
240
|
+
accessToken,
|
|
241
|
+
refreshToken,
|
|
242
|
+
appUser
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
async function authRegister(req, res, next) {
|
|
246
|
+
let { identifier = void 0, secret = void 0 } = req.body || {};
|
|
247
|
+
if (!identifier || !secret) {
|
|
248
|
+
res.sendStatus(FORBIDDEN2);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
identifier = identifier.toLowerCase();
|
|
252
|
+
let user = await AuthUserModel.findOne({ identifier });
|
|
253
|
+
if (user) {
|
|
254
|
+
res.sendStatus(BAD_REQUEST);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
res.locals.authUser = new AuthUserModel({
|
|
258
|
+
identifier,
|
|
259
|
+
secretHash: await bcrypt.hash(secret, 10)
|
|
260
|
+
});
|
|
261
|
+
next();
|
|
262
|
+
}
|
|
263
|
+
async function authLogin(req, res, next) {
|
|
264
|
+
let { identifier = void 0, secret = void 0 } = req.body || {};
|
|
265
|
+
if (!identifier || !secret) {
|
|
266
|
+
res.sendStatus(FORBIDDEN2);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
identifier = identifier.toLowerCase();
|
|
270
|
+
let user = await AuthUserModel.findOne({ identifier }).select("+secretHash");
|
|
271
|
+
if (!user || !user.secretHash) {
|
|
272
|
+
res.sendStatus(BAD_REQUEST);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (user.blockedSince) {
|
|
276
|
+
res.sendStatus(FORBIDDEN2);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
let isMatch = await bcrypt.compare(secret, user.secretHash);
|
|
280
|
+
if (!isMatch) {
|
|
281
|
+
res.sendStatus(BAD_REQUEST);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
let credentials = await generateCredentials(user);
|
|
285
|
+
if (!credentials) {
|
|
286
|
+
res.sendStatus(BAD_REQUEST);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
res.locals.credentials = credentials;
|
|
290
|
+
res.locals.authUser = user;
|
|
291
|
+
next();
|
|
292
|
+
}
|
|
293
|
+
async function authLogout(req, res, next) {
|
|
294
|
+
await jwtRefreshRequired(req, res, async () => {
|
|
295
|
+
let authUserPayload = res.locals.authUserPayload;
|
|
296
|
+
let user = await AuthUserModel.findOne({ identifier: authUserPayload.identifier }).select(
|
|
297
|
+
"+secretHash"
|
|
298
|
+
);
|
|
299
|
+
if (!user || !user.secretHash) {
|
|
300
|
+
res.sendStatus(BAD_REQUEST);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const refreshToken = res.locals.token;
|
|
304
|
+
if (!refreshToken) {
|
|
305
|
+
res.sendStatus(BAD_REQUEST);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const decoded = jwt2.decode(refreshToken);
|
|
309
|
+
const jti = decoded?.jti;
|
|
310
|
+
if (jti) {
|
|
311
|
+
const existingBlock = await AuthTokenBlocklistModel.findOne({ jti });
|
|
312
|
+
if (!existingBlock) {
|
|
313
|
+
await new AuthTokenBlocklistModel({ jti }).save();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
let accessToken = validateString(req.body?.access_token);
|
|
317
|
+
if (accessToken) {
|
|
318
|
+
const accessTokenDecoded = jwt2.decode(accessToken);
|
|
319
|
+
let accessTokenJti = accessTokenDecoded?.jti;
|
|
320
|
+
if (accessTokenJti) {
|
|
321
|
+
const existing = await AuthTokenBlocklistModel.findOne({ jti: accessTokenJti });
|
|
322
|
+
if (!existing) {
|
|
323
|
+
await new AuthTokenBlocklistModel({ jti: accessTokenJti }).save();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
res.locals.authUser = user;
|
|
328
|
+
next();
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
async function authRefresh(req, res, next) {
|
|
332
|
+
await jwtRefreshRequired(req, res, async () => {
|
|
333
|
+
let authUserPayload = res.locals.authUserPayload;
|
|
334
|
+
let user = await AuthUserModel.findOne({ identifier: authUserPayload.identifier }).select(
|
|
335
|
+
"+secretHash"
|
|
336
|
+
);
|
|
337
|
+
if (!user || !user.secretHash) {
|
|
338
|
+
res.sendStatus(BAD_REQUEST);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const refreshToken = res.locals.token;
|
|
342
|
+
if (!refreshToken) {
|
|
343
|
+
res.sendStatus(BAD_REQUEST);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
const decoded = jwt2.decode(refreshToken);
|
|
348
|
+
const jti = decoded?.jti;
|
|
349
|
+
if (jti) {
|
|
350
|
+
const existingBlock = await AuthTokenBlocklistModel.findOne({ jti });
|
|
351
|
+
if (!existingBlock) {
|
|
352
|
+
await new AuthTokenBlocklistModel({ jti }).save();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
let accessToken = validateString(req.body?.access_token);
|
|
356
|
+
if (accessToken) {
|
|
357
|
+
const accessTokenDecoded = jwt2.decode(accessToken);
|
|
358
|
+
let accessTokenJti = accessTokenDecoded?.jti;
|
|
359
|
+
if (accessTokenJti) {
|
|
360
|
+
const existing = await AuthTokenBlocklistModel.findOne({
|
|
361
|
+
jti: accessTokenJti
|
|
362
|
+
});
|
|
363
|
+
if (!existing) {
|
|
364
|
+
await new AuthTokenBlocklistModel({ jti: accessTokenJti }).save();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const payload = jwt2.verify(refreshToken, JwtRefreshSecret);
|
|
369
|
+
let credentials = await generateCredentials(payload);
|
|
370
|
+
if (!credentials) {
|
|
371
|
+
res.sendStatus(BAD_REQUEST);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
res.locals.authUser = user;
|
|
375
|
+
res.locals.credentials = credentials;
|
|
376
|
+
next();
|
|
377
|
+
} catch (err) {
|
|
378
|
+
console.log("[WARN] refreshing JWT:", err);
|
|
379
|
+
res.sendStatus(BAD_REQUEST);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/fluent-interface/fluent-pattern-handler.ts
|
|
386
|
+
import mongoose5 from "mongoose";
|
|
387
|
+
|
|
388
|
+
// src/fluent-interface/types.ts
|
|
389
|
+
var fluentRequestQueryAttributes = {
|
|
390
|
+
id: "",
|
|
391
|
+
ids: "",
|
|
392
|
+
filter: "",
|
|
393
|
+
offset: "",
|
|
394
|
+
limit: "",
|
|
395
|
+
order: "",
|
|
396
|
+
excluded: ""
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// src/core/query-builder.ts
|
|
400
|
+
import mongoose4 from "mongoose";
|
|
401
|
+
|
|
402
|
+
// src/core/types.ts
|
|
403
|
+
var Codec = class {
|
|
404
|
+
constructor(content, code = 202) {
|
|
405
|
+
this.content = content;
|
|
406
|
+
this.returnCode = code;
|
|
407
|
+
}
|
|
408
|
+
sendToClient(res) {
|
|
409
|
+
if (this.content == null) {
|
|
410
|
+
res.status(404).json({});
|
|
411
|
+
}
|
|
412
|
+
res.status(this.returnCode).json(this.content);
|
|
413
|
+
}
|
|
414
|
+
is1xx() {
|
|
415
|
+
return this.returnCode >= 100 && this.returnCode <= 199;
|
|
416
|
+
}
|
|
417
|
+
is2xx() {
|
|
418
|
+
return this.returnCode >= 200 && this.returnCode <= 299;
|
|
419
|
+
}
|
|
420
|
+
is3xx() {
|
|
421
|
+
return this.returnCode >= 300 && this.returnCode <= 399;
|
|
422
|
+
}
|
|
423
|
+
is4xx() {
|
|
424
|
+
return this.returnCode >= 400 && this.returnCode <= 499;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
var JoinRelation = class {
|
|
428
|
+
constructor(localField, ref, alias = void 0) {
|
|
429
|
+
this.ref = ref;
|
|
430
|
+
this.localField = localField;
|
|
431
|
+
this.alias = alias ? alias : ref.collection.name;
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
// src/core/query-builder.ts
|
|
436
|
+
var QueryBuilder = class {
|
|
437
|
+
constructor(config) {
|
|
438
|
+
this._matchConditions = {};
|
|
439
|
+
this._stages = [];
|
|
440
|
+
this._relations = [];
|
|
441
|
+
this._unsetFields = [];
|
|
442
|
+
this._config = config;
|
|
443
|
+
this._applyPathOptions();
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Generates the aggregation pipeline based on the current configuration of the QueryBuilder.
|
|
447
|
+
* @returns
|
|
448
|
+
*/
|
|
449
|
+
getAggregationPipeline() {
|
|
450
|
+
return this._generatePipeline();
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Returns the current configuration of the QueryBuilder, including model, select fields, and any applied options.
|
|
454
|
+
* @returns
|
|
455
|
+
*/
|
|
456
|
+
getConfig() {
|
|
457
|
+
return this._config;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Applies schema-based options such as joins and unset fields.
|
|
461
|
+
*/
|
|
462
|
+
_applyPathOptions() {
|
|
463
|
+
this._generateSchemaJoins(this._config.model.schema);
|
|
464
|
+
this._generateSchemaUnsetList(this._config);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Adds match conditions to the query builder.
|
|
468
|
+
* @param match - The match conditions to add.
|
|
469
|
+
* @param conjunction - The logical conjunction ('and' or 'or').
|
|
470
|
+
* @param append - Whether to append to existing conditions or replace them.
|
|
471
|
+
*/
|
|
472
|
+
match(match, conjunction = "and", append = true) {
|
|
473
|
+
if (!match || (Array.isArray(match) ? match.length === 0 : Object.keys(match).length === 0))
|
|
474
|
+
return;
|
|
475
|
+
if (!append) {
|
|
476
|
+
this._matchConditions = Array.isArray(match) ? { $and: match } : match;
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const key = `$${conjunction}`;
|
|
480
|
+
if (!this._matchConditions[key]) this._matchConditions[key] = [];
|
|
481
|
+
if (Array.isArray(match)) {
|
|
482
|
+
this._matchConditions[key].push(...match);
|
|
483
|
+
} else {
|
|
484
|
+
this._matchConditions[key].push(match);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Adds aggregation stages to the query builder.
|
|
489
|
+
* @param stages
|
|
490
|
+
* @returns
|
|
491
|
+
*/
|
|
492
|
+
stage(stages) {
|
|
493
|
+
if (!stages) return;
|
|
494
|
+
if (Array.isArray(stages)) {
|
|
495
|
+
this._stages.push(...globalThis.structuredClone(stages));
|
|
496
|
+
} else {
|
|
497
|
+
this._stages.push(globalThis.structuredClone(stages));
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Adds join relations to the query builder.
|
|
502
|
+
* @param rels
|
|
503
|
+
* @returns
|
|
504
|
+
*/
|
|
505
|
+
join(rels) {
|
|
506
|
+
if (!rels) return;
|
|
507
|
+
const list = Array.isArray(rels) ? rels : [rels];
|
|
508
|
+
list.forEach((rel) => this._relations.push(rel));
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Calculates the $lookup stage for a given JoinRelation.
|
|
512
|
+
* @param relation
|
|
513
|
+
* @returns
|
|
514
|
+
*/
|
|
515
|
+
_calculateJoin(relation) {
|
|
516
|
+
if (!(relation instanceof JoinRelation)) throw new Error("relation must be JoinRelation");
|
|
517
|
+
return {
|
|
518
|
+
$lookup: {
|
|
519
|
+
from: relation.ref.collection.name,
|
|
520
|
+
localField: relation.localField,
|
|
521
|
+
foreignField: "_id",
|
|
522
|
+
as: relation.alias
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Automatically generates JoinRelation objects from schema refs.
|
|
528
|
+
* @param model The Mongoose model to scan.
|
|
529
|
+
* @param prefix Optional prefix for nested paths (e.g., 'alias.field').
|
|
530
|
+
* @returns Array of JoinRelation objects.
|
|
531
|
+
*/
|
|
532
|
+
_generateSchemaJoins(schema, prefix = "") {
|
|
533
|
+
schema.eachPath((path2, schematype) => {
|
|
534
|
+
const fullPath = prefix ? `${prefix}.${path2}` : path2;
|
|
535
|
+
if (schematype.options?.ref) {
|
|
536
|
+
const refModelName = schematype.options.ref;
|
|
537
|
+
try {
|
|
538
|
+
const refModel = mongoose4.model(refModelName);
|
|
539
|
+
let alias = schematype.options?.alias ?? refModel.collection.name;
|
|
540
|
+
this.join(new JoinRelation(fullPath, refModel, alias));
|
|
541
|
+
} catch (err) {
|
|
542
|
+
console.warn(
|
|
543
|
+
`[QueryBuilder] Could not resolve ref model '${refModelName}' for path '${fullPath}'`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (schematype instanceof mongoose4.Schema.Types.Array && schematype.caster?.options?.ref) {
|
|
548
|
+
const refModelName = schematype.caster.options.ref;
|
|
549
|
+
try {
|
|
550
|
+
const refModel = mongoose4.model(refModelName);
|
|
551
|
+
let alias = schematype.caster.options?.alias ?? refModel.collection.name;
|
|
552
|
+
this.join(new JoinRelation(fullPath, refModel, alias));
|
|
553
|
+
} catch (err) {
|
|
554
|
+
console.warn(
|
|
555
|
+
`[QueryBuilder] Could not resolve array ref model '${refModelName}' for path '${fullPath}'`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Generates the list of fields to unset based on schema select options.
|
|
563
|
+
* @param config - The query request configuration.
|
|
564
|
+
*/
|
|
565
|
+
_generateSchemaUnsetList(config) {
|
|
566
|
+
this._unsetFields = [];
|
|
567
|
+
let unset = this._collectSelectFalse(config.model.schema, void 0, config.select);
|
|
568
|
+
for (const relation of this._relations) {
|
|
569
|
+
unset = unset.concat(
|
|
570
|
+
this._collectSelectFalse(relation.ref.schema, relation.alias, config.select)
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
this._unsetFields = Array.from(new Set(unset));
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Collects paths from the schema where select is set to false.
|
|
577
|
+
* @param schema - The Mongoose schema to scan.
|
|
578
|
+
* @param prefix - Optional prefix for nested paths.
|
|
579
|
+
* @param select - Optional array of fields to select.
|
|
580
|
+
* @returns Array of paths to unset.
|
|
581
|
+
*/
|
|
582
|
+
_collectSelectFalse(schema, prefix = void 0, select = void 0) {
|
|
583
|
+
const unset = [];
|
|
584
|
+
schema.eachPath((path2, schematype) => {
|
|
585
|
+
if (select && select.length > 0 && !select.includes(path2) && schematype?.options?.select !== false) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (schematype?.options?.select === false) {
|
|
589
|
+
unset.push(prefix ? `${prefix}.${path2}` : path2);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
return unset;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Generates the aggregation pipeline based on joins, stages, match conditions, and unset fields.
|
|
596
|
+
* @returns The aggregation pipeline array.
|
|
597
|
+
*/
|
|
598
|
+
_generatePipeline() {
|
|
599
|
+
const pipeline = [];
|
|
600
|
+
for (const rel of this._relations) {
|
|
601
|
+
pipeline.push(this._calculateJoin(rel));
|
|
602
|
+
}
|
|
603
|
+
pipeline.push(...this._stages);
|
|
604
|
+
if (Object.keys(this._matchConditions).length) {
|
|
605
|
+
pipeline.push({ $match: this._matchConditions });
|
|
606
|
+
}
|
|
607
|
+
if (this._unsetFields.length) {
|
|
608
|
+
pipeline.push({ $unset: this._unsetFields });
|
|
609
|
+
}
|
|
610
|
+
return pipeline;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Executes the aggregation pipeline and returns the results.
|
|
614
|
+
* @param config - Parameters for the query execution.
|
|
615
|
+
* @returns The collection response wrapped in a Codec.
|
|
616
|
+
*/
|
|
617
|
+
async exec(config) {
|
|
618
|
+
try {
|
|
619
|
+
const pipeline = this._generatePipeline();
|
|
620
|
+
const countPipeline = [...pipeline, { $count: "n" }];
|
|
621
|
+
const queryPipeline = [...pipeline];
|
|
622
|
+
if (config && !config.isOne) {
|
|
623
|
+
if (config.skip) queryPipeline.push({ $skip: config.skip });
|
|
624
|
+
if (config.limit) queryPipeline.push({ $limit: config.limit });
|
|
625
|
+
}
|
|
626
|
+
const [countRes, res] = await Promise.all([
|
|
627
|
+
this._config.model.aggregate(countPipeline).exec(),
|
|
628
|
+
this._config.model.aggregate(queryPipeline).exec()
|
|
629
|
+
]);
|
|
630
|
+
const totalCount = countRes && countRes[0] ? countRes[0].n : 0;
|
|
631
|
+
const documents = config && config.isOne ? await this._processSingleDocument(res) : await this._processMultipleDocuments(res);
|
|
632
|
+
return new Codec({ data: documents, meta: { total: totalCount } }, 200);
|
|
633
|
+
} catch (err) {
|
|
634
|
+
console.error("[ERROR - QueryBuilder]", err);
|
|
635
|
+
return new Codec({ data: [], meta: { total: 0 } }, 500);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Processes a single document from the aggregation result.
|
|
640
|
+
* @param res - The aggregation result array.
|
|
641
|
+
* @returns The processed document or null if none.
|
|
642
|
+
*/
|
|
643
|
+
async _processSingleDocument(res) {
|
|
644
|
+
if (!res || res.length === 0) {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
let doc = this._config.model.hydrate(res[0]);
|
|
648
|
+
if (this._config.eachFunc) {
|
|
649
|
+
doc = this._config.eachFunc(doc);
|
|
650
|
+
} else if (this._config.asyncEachFunc) {
|
|
651
|
+
doc = await this._config.asyncEachFunc(doc);
|
|
652
|
+
}
|
|
653
|
+
return doc;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Processes multiple documents from the aggregation result.
|
|
657
|
+
* @param res - The aggregation result array.
|
|
658
|
+
* @returns The array of processed documents.
|
|
659
|
+
*/
|
|
660
|
+
async _processMultipleDocuments(res) {
|
|
661
|
+
let final = (res || []).map((doc) => this._config.model.hydrate(doc));
|
|
662
|
+
if (this._config.eachFunc) {
|
|
663
|
+
final = final.map(this._config.eachFunc);
|
|
664
|
+
} else if (this._config.asyncEachFunc) {
|
|
665
|
+
const asyncFinal = [];
|
|
666
|
+
for (const doc of final) {
|
|
667
|
+
const newDoc = await this._config.asyncEachFunc(doc);
|
|
668
|
+
asyncFinal.push(newDoc);
|
|
669
|
+
}
|
|
670
|
+
final = asyncFinal;
|
|
671
|
+
}
|
|
672
|
+
return final;
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
// src/fluent-interface/fluent-pattern-handler.ts
|
|
677
|
+
var FluentPatternHandler = class _FluentPatternHandler {
|
|
678
|
+
/**
|
|
679
|
+
* Constructor for FluentPatternHandler.
|
|
680
|
+
* @param execMiddleware - Optional array of middleware functions to be executed before the main query execution in the exec method.
|
|
681
|
+
*/
|
|
682
|
+
constructor(execMiddleware = []) {
|
|
683
|
+
/**
|
|
684
|
+
* Array of middleware functions to be executed before the main query execution in the exec method. Each function receives the execution parameters and can modify them as needed.
|
|
685
|
+
*/
|
|
686
|
+
this._execMiddlewareFunctions = [];
|
|
687
|
+
if (_FluentPatternHandler._singleton) {
|
|
688
|
+
throw new Error(
|
|
689
|
+
"FluentPatternHandler is a singleton class. Use FluentPatternHandler.getInstance() to access the instance."
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
this._execMiddlewareFunctions = execMiddleware;
|
|
693
|
+
_FluentPatternHandler._singleton = this;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Initializes the singleton instance of FluentPatternHandler with the provided options.
|
|
697
|
+
* @param execMiddleware - Optional array of middleware functions to be executed before the main query execution in the exec method.
|
|
698
|
+
* @returns Singleton instance of FluentPatternHandler.
|
|
699
|
+
*/
|
|
700
|
+
static init(execMiddleware = []) {
|
|
701
|
+
if (_FluentPatternHandler._singleton != void 0) {
|
|
702
|
+
throw new Error("FluentPatternHandler is already initialized");
|
|
703
|
+
}
|
|
704
|
+
_FluentPatternHandler._singleton = new _FluentPatternHandler(execMiddleware);
|
|
705
|
+
return _FluentPatternHandler._singleton;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Returns the singleton instance of FluentPatternHandler.
|
|
709
|
+
* @returns Singleton instance of FluentPatternHandler.
|
|
710
|
+
*/
|
|
711
|
+
static getInstance() {
|
|
712
|
+
if (_FluentPatternHandler._singleton == void 0) {
|
|
713
|
+
throw new Error(
|
|
714
|
+
"FluentPatternHandler instance has not been created yet. Please create an instance before calling getInstance()."
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
return _FluentPatternHandler._singleton;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Parses and validates query parameters from the request.
|
|
721
|
+
* @param query - The query object from the request.
|
|
722
|
+
* @returns Parsed query parameters.
|
|
723
|
+
*/
|
|
724
|
+
_parseFluentRequestQuery(query) {
|
|
725
|
+
const { filter, limit = "5", offset = "0", id, excluded: excludedJSON, ids: idsJSON } = query;
|
|
726
|
+
const queryKeys = Object.keys(fluentRequestQueryAttributes);
|
|
727
|
+
let filterFields = {};
|
|
728
|
+
for (const [key, value] of Object.entries(query)) {
|
|
729
|
+
if (queryKeys.includes(key)) continue;
|
|
730
|
+
if (value == void 0) continue;
|
|
731
|
+
try {
|
|
732
|
+
filterFields[key] = typeof value === "string" ? value : JSON.stringify(value);
|
|
733
|
+
} catch (e) {
|
|
734
|
+
filterFields[key] = String(value);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
let excluded;
|
|
738
|
+
if (excludedJSON) {
|
|
739
|
+
try {
|
|
740
|
+
excluded = JSON.parse(excludedJSON);
|
|
741
|
+
if (!Array.isArray(excluded)) throw new Error("Excluded must be an array");
|
|
742
|
+
} catch (error) {
|
|
743
|
+
console.warn("[FluentPatternHandler] Invalid excluded parameter:", error);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
let ids;
|
|
747
|
+
if (idsJSON) {
|
|
748
|
+
try {
|
|
749
|
+
ids = JSON.parse(idsJSON);
|
|
750
|
+
if (!Array.isArray(ids)) throw new Error("Ids must be an array");
|
|
751
|
+
} catch (error) {
|
|
752
|
+
console.warn("[FluentPatternHandler] Invalid ids parameter:", error);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return { filter, limit, offset, id, excluded, ids, filterFields };
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Applies filters to the query builder based on parsed parameters and options.
|
|
759
|
+
* @param queryBuilder - The QueryBuilder instance.
|
|
760
|
+
* @param params - Parsed query parameters.
|
|
761
|
+
* @param config - Query request configuration.
|
|
762
|
+
*/
|
|
763
|
+
_applyParameters(queryBuilder, params) {
|
|
764
|
+
const { id, ids, filter, excluded } = params;
|
|
765
|
+
if (id) {
|
|
766
|
+
queryBuilder.match({ _id: new mongoose5.Types.ObjectId(id) });
|
|
767
|
+
} else if (ids && ids.length > 0) {
|
|
768
|
+
const objectIds = ids.map((id2) => new mongoose5.Types.ObjectId(id2));
|
|
769
|
+
queryBuilder.match({ _id: { $in: objectIds } });
|
|
770
|
+
} else {
|
|
771
|
+
const modelFilterFields = this._getFilterFieldsForModel(queryBuilder.getConfig().model);
|
|
772
|
+
if (filter && modelFilterFields.length > 0) {
|
|
773
|
+
const ors = modelFilterFields.map((field) => ({
|
|
774
|
+
[field]: { $regex: filter, $options: "i" }
|
|
775
|
+
}));
|
|
776
|
+
queryBuilder.match({ $or: ors });
|
|
777
|
+
}
|
|
778
|
+
if (params.filterFields && Object.keys(params.filterFields).length > 0) {
|
|
779
|
+
for (const [field, value] of Object.entries(params.filterFields)) {
|
|
780
|
+
if (!modelFilterFields.includes(field)) {
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
const match = {};
|
|
784
|
+
match[field] = { $regex: value, $options: "i" };
|
|
785
|
+
queryBuilder.match(match);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (excluded && excluded.length > 0) {
|
|
789
|
+
const objectIds = excluded.map((id2) => new mongoose5.Types.ObjectId(id2));
|
|
790
|
+
queryBuilder.match({ _id: { $nin: objectIds } });
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Generates filter fields for the given Mongoose model based on schema options.
|
|
796
|
+
* @param model - The Mongoose model.
|
|
797
|
+
* @returns Array of filter fields.
|
|
798
|
+
*/
|
|
799
|
+
_getFilterFieldsForModel(model) {
|
|
800
|
+
let filterFields = [];
|
|
801
|
+
let schema = model.schema;
|
|
802
|
+
schema.eachPath((path2, type) => {
|
|
803
|
+
if (type?.options?.filter == true) {
|
|
804
|
+
filterFields.push(path2);
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
return filterFields;
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Builds execution parameters for the query builder.
|
|
811
|
+
* @param params - Parsed query parameters.
|
|
812
|
+
* @returns Execution parameters.
|
|
813
|
+
*/
|
|
814
|
+
_buildExecutionConfig(params) {
|
|
815
|
+
const { id, limit, offset } = params;
|
|
816
|
+
return {
|
|
817
|
+
isOne: Boolean(id),
|
|
818
|
+
limit: limit === "full" ? void 0 : parseInt(limit || "5", 10),
|
|
819
|
+
skip: parseInt(offset || "0", 10)
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Executes the query builder with applied filters and returns the result.
|
|
824
|
+
* @param params Execution parameters including the query builder and request query.
|
|
825
|
+
* @returns
|
|
826
|
+
*/
|
|
827
|
+
async exec(params) {
|
|
828
|
+
try {
|
|
829
|
+
if (this._execMiddlewareFunctions && this._execMiddlewareFunctions.length > 0) {
|
|
830
|
+
for (const func of this._execMiddlewareFunctions) {
|
|
831
|
+
await func(params);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
const queryParams = this._parseFluentRequestQuery(params.req.query);
|
|
835
|
+
this._applyParameters(params.queryBuilder, queryParams);
|
|
836
|
+
const execConfig = this._buildExecutionConfig(queryParams);
|
|
837
|
+
return await params.queryBuilder.exec(execConfig);
|
|
838
|
+
} catch (err) {
|
|
839
|
+
console.error("[ERROR - FluentPatternHandler]", err);
|
|
840
|
+
return new Codec({ data: [], meta: { total: 0 } }, 500);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
// src/app.ts
|
|
846
|
+
import express2 from "express";
|
|
847
|
+
import cors from "cors";
|
|
848
|
+
function createApp() {
|
|
849
|
+
const app = express2();
|
|
850
|
+
app.use(cors());
|
|
851
|
+
app.use(express2.json());
|
|
852
|
+
app.use(express2.urlencoded({ extended: true }));
|
|
853
|
+
app.use(requestLogger);
|
|
854
|
+
app.use(errorLogger);
|
|
855
|
+
return app;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// tests/routes.ts
|
|
859
|
+
import { Router as Router3 } from "express";
|
|
860
|
+
|
|
861
|
+
// tests/fluent-api.ts
|
|
862
|
+
import express3 from "express";
|
|
863
|
+
import mongoose6 from "mongoose";
|
|
864
|
+
var router2 = express3.Router();
|
|
865
|
+
router2.get(
|
|
866
|
+
"/:collection",
|
|
867
|
+
jwtRequired,
|
|
868
|
+
async (req, res) => {
|
|
869
|
+
let collectionName = req.params.collection;
|
|
870
|
+
try {
|
|
871
|
+
if (!collectionName) {
|
|
872
|
+
console.log(`[ERROR] Collection name is required in the route parameter`);
|
|
873
|
+
res.status(400).json({});
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
let model;
|
|
877
|
+
for (const modelName in mongoose6.models) {
|
|
878
|
+
let searchModel = mongoose6.models[modelName];
|
|
879
|
+
if (!searchModel) {
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
if (searchModel.collection.name === collectionName) {
|
|
883
|
+
model = searchModel;
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (!model) {
|
|
888
|
+
console.log(`[ERROR] No model found for collection: ${collectionName}`);
|
|
889
|
+
res.status(400).json({});
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
let queryBuilder = new QueryBuilder({
|
|
893
|
+
model
|
|
894
|
+
});
|
|
895
|
+
let codec = await FluentPatternHandler.getInstance().exec({
|
|
896
|
+
req,
|
|
897
|
+
res,
|
|
898
|
+
queryBuilder
|
|
899
|
+
});
|
|
900
|
+
codec.sendToClient(res);
|
|
901
|
+
} catch (e) {
|
|
902
|
+
console.log(e);
|
|
903
|
+
res.status(500).json();
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
);
|
|
907
|
+
var fluent_api_default = router2;
|
|
908
|
+
|
|
909
|
+
// tests/auth.ts
|
|
910
|
+
import express4 from "express";
|
|
911
|
+
var router3 = express4.Router();
|
|
912
|
+
router3.post(
|
|
913
|
+
"/register",
|
|
914
|
+
authRegister,
|
|
915
|
+
async (req, res) => {
|
|
916
|
+
res.locals.authUser.save();
|
|
917
|
+
res.status(200).json({});
|
|
918
|
+
}
|
|
919
|
+
);
|
|
920
|
+
router3.post("/login", authLogin, async (req, res) => {
|
|
921
|
+
res.status(200).json(res.locals.credentials);
|
|
922
|
+
});
|
|
923
|
+
router3.post("/logout", authLogout, async (req, res) => {
|
|
924
|
+
res.status(200).json({});
|
|
925
|
+
});
|
|
926
|
+
router3.post("/refresh", authRefresh, async (req, res) => {
|
|
927
|
+
res.status(200).json({});
|
|
928
|
+
});
|
|
929
|
+
router3.get("/secure", jwtRequired, async (req, res) => {
|
|
930
|
+
res.status(200).json({});
|
|
931
|
+
});
|
|
932
|
+
var auth_default = router3;
|
|
933
|
+
|
|
934
|
+
// tests/icecat-api.ts
|
|
935
|
+
import express5 from "express";
|
|
936
|
+
import axios from "axios";
|
|
937
|
+
var ICECAT_USERNAME = "openIcecat-live";
|
|
938
|
+
var router4 = express5.Router();
|
|
939
|
+
router4.get("/product", async (req, res) => {
|
|
940
|
+
let { gtin = void 0 } = req.query;
|
|
941
|
+
if (!gtin) {
|
|
942
|
+
gtin = "0711719709695";
|
|
943
|
+
}
|
|
944
|
+
try {
|
|
945
|
+
const icecatUrl = `https://live.icecat.biz/api?shopname=${ICECAT_USERNAME}>IN=${gtin}&lang=EN&content=`;
|
|
946
|
+
const response = await axios.get(icecatUrl, {
|
|
947
|
+
headers: {
|
|
948
|
+
Accept: "application/json"
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
return res.json(response.data);
|
|
952
|
+
} catch (error) {
|
|
953
|
+
console.error("Icecat Fehler:", error.message);
|
|
954
|
+
return res.status(500).json({
|
|
955
|
+
error: "Fehler beim Abrufen der Produktdaten",
|
|
956
|
+
details: error.message
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
var icecat_api_default = router4;
|
|
961
|
+
|
|
962
|
+
// tests/nmc/routes.ts
|
|
963
|
+
import { Router } from "express";
|
|
964
|
+
|
|
965
|
+
// tests/nmc/nmc-api.ts
|
|
966
|
+
import express6 from "express";
|
|
967
|
+
|
|
968
|
+
// tests/nmc/niklas-module-config.ts
|
|
969
|
+
import { readFileSync } from "fs";
|
|
970
|
+
function castValue(raw) {
|
|
971
|
+
if (raw === "true") return true;
|
|
972
|
+
if (raw === "false") return false;
|
|
973
|
+
if (raw !== "" && !isNaN(Number(raw))) return Number(raw);
|
|
974
|
+
if (raw.length >= 2) {
|
|
975
|
+
const first = raw[0];
|
|
976
|
+
const last = raw[raw.length - 1];
|
|
977
|
+
if (first === '"' && last === '"' || first === "'" && last === "'") {
|
|
978
|
+
return raw.slice(1, -1);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return raw;
|
|
982
|
+
}
|
|
983
|
+
function navigate(root, stack) {
|
|
984
|
+
let node = root;
|
|
985
|
+
for (const { key } of stack) {
|
|
986
|
+
node = node[key];
|
|
987
|
+
}
|
|
988
|
+
return node;
|
|
989
|
+
}
|
|
990
|
+
function parseNmc(content) {
|
|
991
|
+
const result = { global: {} };
|
|
992
|
+
const stack = [];
|
|
993
|
+
const lines = content.split(/\r?\n/);
|
|
994
|
+
for (const line of lines) {
|
|
995
|
+
const trimmed = line.trim();
|
|
996
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
997
|
+
const indent = line.length - line.trimStart().length;
|
|
998
|
+
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
|
|
999
|
+
stack.pop();
|
|
1000
|
+
}
|
|
1001
|
+
const eqIndex = trimmed.indexOf("=");
|
|
1002
|
+
const isSectionHeader = trimmed.endsWith(":") && eqIndex === -1;
|
|
1003
|
+
if (isSectionHeader) {
|
|
1004
|
+
const key = trimmed.slice(0, -1).trim();
|
|
1005
|
+
const parent = navigate(result, stack);
|
|
1006
|
+
parent[key] = {};
|
|
1007
|
+
stack.push({ indent, key });
|
|
1008
|
+
} else if (eqIndex !== -1) {
|
|
1009
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
1010
|
+
const value = castValue(trimmed.slice(eqIndex + 1).trim());
|
|
1011
|
+
if (stack.length === 0) {
|
|
1012
|
+
result["global"][key] = value;
|
|
1013
|
+
} else {
|
|
1014
|
+
navigate(result, stack)[key] = value;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return result;
|
|
1019
|
+
}
|
|
1020
|
+
function parseNmcFile(filePath) {
|
|
1021
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1022
|
+
return parseNmc(content);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// tests/nmc/nmc-api.ts
|
|
1026
|
+
var router5 = express6.Router();
|
|
1027
|
+
router5.get("/", async (req, res) => {
|
|
1028
|
+
try {
|
|
1029
|
+
const config = parseNmcFile("public/example.nmc");
|
|
1030
|
+
res.status(200).json(config);
|
|
1031
|
+
} catch (e) {
|
|
1032
|
+
console.log(e);
|
|
1033
|
+
res.status(500).json();
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
var nmc_api_default = router5;
|
|
1037
|
+
|
|
1038
|
+
// tests/nmc/routes.ts
|
|
1039
|
+
var router6 = Router();
|
|
1040
|
+
router6.use("/", nmc_api_default);
|
|
1041
|
+
var routes_default = router6;
|
|
1042
|
+
|
|
1043
|
+
// tests/http-validation/routes.ts
|
|
1044
|
+
import { Router as Router2 } from "express";
|
|
1045
|
+
|
|
1046
|
+
// tests/http-validation/http-api.ts
|
|
1047
|
+
import express7 from "express";
|
|
1048
|
+
|
|
1049
|
+
// tests/http-validation/http-request-validator.ts
|
|
1050
|
+
function httpRequestValidator(params, mapping) {
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// tests/http-validation/http-api.ts
|
|
1054
|
+
var router7 = express7.Router();
|
|
1055
|
+
router7.get("/", async (req, res) => {
|
|
1056
|
+
try {
|
|
1057
|
+
let {
|
|
1058
|
+
user_title: title
|
|
1059
|
+
} = req.body || {};
|
|
1060
|
+
let params = httpRequestValidator({
|
|
1061
|
+
// TODO Eventuell ist das hier eine Quatsch idee, das bringt mir kaum Vorteile
|
|
1062
|
+
title
|
|
1063
|
+
}, {
|
|
1064
|
+
title: {
|
|
1065
|
+
validation: (value) => {
|
|
1066
|
+
return value;
|
|
1067
|
+
},
|
|
1068
|
+
default: void 0
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
res.status(200).json(params);
|
|
1072
|
+
} catch (e) {
|
|
1073
|
+
console.log(e);
|
|
1074
|
+
res.status(500).json();
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
var http_api_default = router7;
|
|
1078
|
+
|
|
1079
|
+
// tests/http-validation/routes.ts
|
|
1080
|
+
var router8 = Router2();
|
|
1081
|
+
router8.use("/", http_api_default);
|
|
1082
|
+
var routes_default2 = router8;
|
|
1083
|
+
|
|
1084
|
+
// tests/routes.ts
|
|
1085
|
+
var router9 = Router3();
|
|
1086
|
+
router9.use("/fluent", fluent_api_default);
|
|
1087
|
+
router9.use("/auth", auth_default);
|
|
1088
|
+
router9.use("/icecat", icecat_api_default);
|
|
1089
|
+
router9.use("/nmc", routes_default);
|
|
1090
|
+
router9.use("/http", routes_default2);
|
|
1091
|
+
var routes_default3 = router9;
|
|
1092
|
+
|
|
1093
|
+
// tests/main.ts
|
|
1094
|
+
dotenv.config();
|
|
1095
|
+
var PORT = process.env.PORT || 3e3;
|
|
1096
|
+
var URI = process.env.MONGODB_URI;
|
|
1097
|
+
async function setup() {
|
|
1098
|
+
new FluentPatternHandler();
|
|
1099
|
+
connectMongoDB(URI).then(() => {
|
|
1100
|
+
let app = createApp();
|
|
1101
|
+
app.use("/", routes_default3);
|
|
1102
|
+
app.listen(PORT, () => {
|
|
1103
|
+
console.log(`Server running on port ${PORT}`);
|
|
1104
|
+
});
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
setup();
|
|
1108
|
+
export {
|
|
1109
|
+
setup
|
|
1110
|
+
};
|
|
1111
|
+
//# sourceMappingURL=main.js.map
|