@feardread/fear 1.0.1
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/FEAR.js +459 -0
- package/FEARServer.js +280 -0
- package/controllers/agent.js +438 -0
- package/controllers/auth/index.js +345 -0
- package/controllers/auth/token.js +50 -0
- package/controllers/blog.js +105 -0
- package/controllers/brand.js +10 -0
- package/controllers/cart.js +425 -0
- package/controllers/category.js +9 -0
- package/controllers/coupon.js +63 -0
- package/controllers/crud/crud.js +508 -0
- package/controllers/crud/index.js +36 -0
- package/controllers/email.js +34 -0
- package/controllers/enquiry.js +65 -0
- package/controllers/events.js +9 -0
- package/controllers/order.js +125 -0
- package/controllers/payment.js +31 -0
- package/controllers/product.js +147 -0
- package/controllers/review.js +247 -0
- package/controllers/tag.js +10 -0
- package/controllers/task.js +10 -0
- package/controllers/upload.js +41 -0
- package/controllers/user.js +401 -0
- package/index.js +7 -0
- package/libs/agent/index.js +561 -0
- package/libs/agent/modules/ai/ai.js +285 -0
- package/libs/agent/modules/ai/chat.js +518 -0
- package/libs/agent/modules/ai/config.js +688 -0
- package/libs/agent/modules/ai/operations.js +787 -0
- package/libs/agent/modules/analyze/api.js +546 -0
- package/libs/agent/modules/analyze/dorks.js +395 -0
- package/libs/agent/modules/ccard/README.md +454 -0
- package/libs/agent/modules/ccard/audit.js +479 -0
- package/libs/agent/modules/ccard/checker.js +674 -0
- package/libs/agent/modules/ccard/payment-processors.json +16 -0
- package/libs/agent/modules/ccard/validator.js +629 -0
- package/libs/agent/modules/code/analyzer.js +303 -0
- package/libs/agent/modules/code/jquery.js +1093 -0
- package/libs/agent/modules/code/react.js +1536 -0
- package/libs/agent/modules/code/refactor.js +499 -0
- package/libs/agent/modules/crypto/exchange.js +564 -0
- package/libs/agent/modules/net/proxy.js +409 -0
- package/libs/agent/modules/security/cve.js +442 -0
- package/libs/agent/modules/security/monitor.js +360 -0
- package/libs/agent/modules/security/scanner.js +300 -0
- package/libs/agent/modules/security/vulnerability.js +506 -0
- package/libs/agent/modules/security/web.js +465 -0
- package/libs/agent/modules/utils/browser.js +492 -0
- package/libs/agent/modules/utils/colorizer.js +285 -0
- package/libs/agent/modules/utils/manager.js +478 -0
- package/libs/cloud/index.js +228 -0
- package/libs/config/db.js +21 -0
- package/libs/config/validator.js +82 -0
- package/libs/db/index.js +318 -0
- package/libs/emailer/imap.js +126 -0
- package/libs/emailer/info.js +41 -0
- package/libs/emailer/smtp.js +77 -0
- package/libs/handler/async.js +3 -0
- package/libs/handler/error.js +66 -0
- package/libs/handler/index.js +161 -0
- package/libs/logger/index.js +49 -0
- package/libs/logger/morgan.js +24 -0
- package/libs/passport/passport.js +109 -0
- package/libs/search/api.js +384 -0
- package/libs/search/features.js +219 -0
- package/libs/search/service.js +64 -0
- package/libs/swagger/config.js +18 -0
- package/libs/swagger/index.js +35 -0
- package/libs/validator/index.js +254 -0
- package/models/blog.js +31 -0
- package/models/brand.js +12 -0
- package/models/cart.js +14 -0
- package/models/category.js +11 -0
- package/models/coupon.js +9 -0
- package/models/customer.js +0 -0
- package/models/enquiry.js +29 -0
- package/models/events.js +13 -0
- package/models/order.js +94 -0
- package/models/product.js +32 -0
- package/models/review.js +14 -0
- package/models/tag.js +10 -0
- package/models/task.js +11 -0
- package/models/user.js +68 -0
- package/package.json +12 -0
- package/routes/agent.js +615 -0
- package/routes/auth.js +13 -0
- package/routes/blog.js +19 -0
- package/routes/brand.js +15 -0
- package/routes/cart.js +105 -0
- package/routes/category.js +16 -0
- package/routes/coupon.js +15 -0
- package/routes/enquiry.js +14 -0
- package/routes/events.js +16 -0
- package/routes/mail.js +170 -0
- package/routes/order.js +19 -0
- package/routes/product.js +22 -0
- package/routes/review.js +11 -0
- package/routes/task.js +12 -0
- package/routes/user.js +17 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
const logger = require("../logger");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Advanced search, filter, and pagination features for Mongoose models
|
|
5
|
+
* Provides chainable methods for building complex database queries
|
|
6
|
+
*/
|
|
7
|
+
module.exports = class SearchApi {
|
|
8
|
+
/**
|
|
9
|
+
* Initialize SearchFeatures with model and query parameters
|
|
10
|
+
* @param {mongoose.Model} Model - Mongoose model to query
|
|
11
|
+
* @param {Object} queryObj - Query parameters from request
|
|
12
|
+
* @param {Object} [options={}] - Additional configuration options
|
|
13
|
+
*/
|
|
14
|
+
constructor(Model, queryObj, options = {}) {
|
|
15
|
+
if (!Model) {
|
|
16
|
+
throw new Error('Model parameter is required');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
this.Model = Model;
|
|
20
|
+
this.query = Model.find();
|
|
21
|
+
this.queryObj = queryObj || {};
|
|
22
|
+
this.options = {
|
|
23
|
+
defaultSort: { createdAt: -1 }, // Default sorting
|
|
24
|
+
searchFields: [], // Specific fields to search in
|
|
25
|
+
excludeFields: ["keyword", "page", "limit", "sort", "fields"], // Fields to exclude from filtering
|
|
26
|
+
caseSensitive: false, // Case sensitivity for search
|
|
27
|
+
fuzzySearch: true, // Enable fuzzy matching
|
|
28
|
+
...options
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
this.originalQuery = { ...this.queryObj };
|
|
32
|
+
this.isExecuted = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Perform text search across specified fields or all string fields
|
|
37
|
+
* @param {string[]} [searchFields] - Specific fields to search in
|
|
38
|
+
* @returns {SearchApi} Chainable instance
|
|
39
|
+
*/
|
|
40
|
+
search(searchFields = null) {
|
|
41
|
+
try {
|
|
42
|
+
const fieldsToSearch = searchFields || this.options.searchFields;
|
|
43
|
+
const searchTerms = {};
|
|
44
|
+
const keyword = this.queryObj.keyword;
|
|
45
|
+
|
|
46
|
+
if (!keyword) {
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// If specific search fields are provided
|
|
51
|
+
if (fieldsToSearch.length > 0) {
|
|
52
|
+
const searchConditions = fieldsToSearch.map(field => ({
|
|
53
|
+
[field]: {
|
|
54
|
+
$regex: keyword,
|
|
55
|
+
$options: this.options.caseSensitive ? "" : "i"
|
|
56
|
+
}
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
this.query = this.query.find({
|
|
60
|
+
$or: searchConditions
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
// Search in all provided query fields (excluding system fields)
|
|
64
|
+
Object.keys(this.queryObj).forEach(prop => {
|
|
65
|
+
if (this.queryObj.hasOwnProperty(prop) &&
|
|
66
|
+
!this.options.excludeFields.includes(prop)) {
|
|
67
|
+
searchTerms[prop] = {
|
|
68
|
+
$regex: this.queryObj[prop],
|
|
69
|
+
$options: this.options.caseSensitive ? "" : "$i"
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (Object.keys(searchTerms).length > 0) {
|
|
75
|
+
logger.info('Search terms applied:', searchTerms);
|
|
76
|
+
this.query = this.query.find(searchTerms);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return this;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
logger.error('Search operation failed:', error);
|
|
83
|
+
throw new Error(`Search failed: ${error.message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Apply filters for exact matches and range queries
|
|
89
|
+
* @param {string[]} [additionalExcludeFields] - Additional fields to exclude
|
|
90
|
+
* @returns {SearchFeatures} Chainable instance
|
|
91
|
+
*/
|
|
92
|
+
filter(additionalExcludeFields = []) {
|
|
93
|
+
try {
|
|
94
|
+
const queryCopy = { ...this.queryObj };
|
|
95
|
+
const excludeFields = [...this.options.excludeFields, ...additionalExcludeFields];
|
|
96
|
+
|
|
97
|
+
// Remove excluded fields
|
|
98
|
+
excludeFields.forEach(field => delete queryCopy[field]);
|
|
99
|
+
|
|
100
|
+
if (Object.keys(queryCopy).length === 0) {
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Handle range operators (gt, gte, lt, lte, ne, in, nin)
|
|
105
|
+
let queryString = JSON.stringify(queryCopy);
|
|
106
|
+
queryString = queryString.replace(
|
|
107
|
+
/\b(gt|gte|lt|lte|ne|in|nin|regex)\b/g,
|
|
108
|
+
match => `$${match}`
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const filterQuery = JSON.parse(queryString);
|
|
112
|
+
|
|
113
|
+
// Handle array fields (in/nin operations)
|
|
114
|
+
Object.keys(filterQuery).forEach(key => {
|
|
115
|
+
const value = filterQuery[key];
|
|
116
|
+
if (typeof value === 'object' && value !== null) {
|
|
117
|
+
// Handle comma-separated values for $in operator
|
|
118
|
+
Object.keys(value).forEach(operator => {
|
|
119
|
+
if ((operator === '$in' || operator === '$nin') &&
|
|
120
|
+
typeof value[operator] === 'string') {
|
|
121
|
+
value[operator] = value[operator].split(',').map(item => item.trim());
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
logger.info('Filter query applied:', filterQuery);
|
|
128
|
+
this.query = this.query.find(filterQuery);
|
|
129
|
+
|
|
130
|
+
return this;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
logger.error('Filter operation failed:', error);
|
|
133
|
+
throw new Error(`Filter failed: ${error.message}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Apply sorting to the query
|
|
139
|
+
* @param {string|Object} [customSort] - Custom sort criteria
|
|
140
|
+
* @returns {SearchFeatures} Chainable instance
|
|
141
|
+
*/
|
|
142
|
+
sort(customSort = null) {
|
|
143
|
+
try {
|
|
144
|
+
let sortCriteria = customSort || this.queryObj.sort || this.options.defaultSort;
|
|
145
|
+
|
|
146
|
+
// Convert string sort to object
|
|
147
|
+
if (typeof sortCriteria === 'string') {
|
|
148
|
+
const sortObj = {};
|
|
149
|
+
sortCriteria.split(',').forEach(field => {
|
|
150
|
+
const trimmedField = field.trim();
|
|
151
|
+
if (trimmedField.startsWith('-')) {
|
|
152
|
+
sortObj[trimmedField.substring(1)] = -1;
|
|
153
|
+
} else {
|
|
154
|
+
sortObj[trimmedField] = 1;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
sortCriteria = sortObj;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.query = this.query.sort(sortCriteria);
|
|
161
|
+
logger.info('Sort applied:', sortCriteria);
|
|
162
|
+
|
|
163
|
+
return this;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
logger.error('Sort operation failed:', error);
|
|
166
|
+
throw new Error(`Sort failed: ${error.message}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Select specific fields to return
|
|
172
|
+
* @param {string|string[]} [fields] - Fields to select
|
|
173
|
+
* @returns {SearchFeatures} Chainable instance
|
|
174
|
+
*/
|
|
175
|
+
selectFields(fields = null) {
|
|
176
|
+
try {
|
|
177
|
+
const fieldsToSelect = fields || this.queryObj.fields;
|
|
178
|
+
|
|
179
|
+
if (fieldsToSelect) {
|
|
180
|
+
const selectFields = Array.isArray(fieldsToSelect)
|
|
181
|
+
? fieldsToSelect.join(' ')
|
|
182
|
+
: fieldsToSelect.split(',').join(' ');
|
|
183
|
+
|
|
184
|
+
this.query = this.query.select(selectFields);
|
|
185
|
+
logger.info('Fields selected:', selectFields);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return this;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
logger.error('Field selection failed:', error);
|
|
191
|
+
throw new Error(`Field selection failed: ${error.message}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Apply pagination with configurable limits
|
|
197
|
+
* @param {number} [resultPerPage=10] - Number of results per page
|
|
198
|
+
* @param {number} [maxLimit=100] - Maximum allowed limit
|
|
199
|
+
* @returns {SearchFeatures} Chainable instance
|
|
200
|
+
*/
|
|
201
|
+
paginate(resultPerPage = 10, maxLimit = 100) {
|
|
202
|
+
try {
|
|
203
|
+
const page = Math.max(1, Number(this.queryObj.page) || 1);
|
|
204
|
+
const limit = Math.min(
|
|
205
|
+
maxLimit,
|
|
206
|
+
Math.max(1, Number(this.queryObj.limit) || resultPerPage)
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const skip = limit * (page - 1);
|
|
210
|
+
|
|
211
|
+
this.query = this.query.limit(limit).skip(skip);
|
|
212
|
+
|
|
213
|
+
// Store pagination info for later use
|
|
214
|
+
this.paginationInfo = {
|
|
215
|
+
page,
|
|
216
|
+
limit,
|
|
217
|
+
skip
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
logger.info('Pagination applied:', { page, limit, skip });
|
|
221
|
+
|
|
222
|
+
return this;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
logger.error('Pagination failed:', error);
|
|
225
|
+
throw new Error(`Pagination failed: ${error.message}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Populate referenced fields
|
|
231
|
+
* @param {string|Object|Array} [populateOptions] - Population configuration
|
|
232
|
+
* @returns {SearchFeatures} Chainable instance
|
|
233
|
+
*/
|
|
234
|
+
populate(populateOptions = null) {
|
|
235
|
+
try {
|
|
236
|
+
if (populateOptions) {
|
|
237
|
+
this.query = this.query.populate(populateOptions);
|
|
238
|
+
logger.info('Population applied:', populateOptions);
|
|
239
|
+
} else if (this.queryObj.populate) {
|
|
240
|
+
// Handle populate from query string
|
|
241
|
+
const populateFields = this.queryObj.populate.split(',').map(field => field.trim());
|
|
242
|
+
populateFields.forEach(field => {
|
|
243
|
+
this.query = this.query.populate(field);
|
|
244
|
+
});
|
|
245
|
+
logger.info('Population applied from query:', populateFields);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return this;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
logger.error('Population failed:', error);
|
|
251
|
+
throw new Error(`Population failed: ${error.message}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Execute the query and return results with metadata
|
|
257
|
+
* @returns {Promise<Object>} Results with pagination metadata
|
|
258
|
+
*/
|
|
259
|
+
async execute() {
|
|
260
|
+
try {
|
|
261
|
+
if (this.isExecuted) {
|
|
262
|
+
logger.warn('Query has already been executed');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Get total count for pagination metadata
|
|
266
|
+
const totalDocuments = await this.Model.countDocuments(this.query.getQuery());
|
|
267
|
+
|
|
268
|
+
// Execute the main query
|
|
269
|
+
const results = await this.query.exec();
|
|
270
|
+
|
|
271
|
+
// Prepare pagination metadata
|
|
272
|
+
const paginationMeta = this.paginationInfo ? {
|
|
273
|
+
currentPage: this.paginationInfo.page,
|
|
274
|
+
totalPages: Math.ceil(totalDocuments / this.paginationInfo.limit),
|
|
275
|
+
totalDocuments,
|
|
276
|
+
documentsPerPage: this.paginationInfo.limit,
|
|
277
|
+
hasNextPage: this.paginationInfo.page < Math.ceil(totalDocuments / this.paginationInfo.limit),
|
|
278
|
+
hasPrevPage: this.paginationInfo.page > 1
|
|
279
|
+
} : null;
|
|
280
|
+
|
|
281
|
+
this.isExecuted = true;
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
result: results,
|
|
285
|
+
meta: {
|
|
286
|
+
total: totalDocuments,
|
|
287
|
+
count: results.length,
|
|
288
|
+
pagination: paginationMeta,
|
|
289
|
+
query: this.originalQuery
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
} catch (error) {
|
|
293
|
+
logger.error('Query execution failed:', error);
|
|
294
|
+
throw new Error(`Query execution failed: ${error.message}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get the current query object (for debugging)
|
|
300
|
+
* @returns {mongoose.Query} Current Mongoose query
|
|
301
|
+
*/
|
|
302
|
+
getQuery() {
|
|
303
|
+
return this.query;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Get the current query filter (for debugging)
|
|
308
|
+
* @returns {Object} Current query filter
|
|
309
|
+
*/
|
|
310
|
+
getFilter() {
|
|
311
|
+
return this.query.getQuery();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Reset the query to its initial state
|
|
316
|
+
* @returns {SearchFeatures} Chainable instance
|
|
317
|
+
*/
|
|
318
|
+
reset() {
|
|
319
|
+
this.query = this.Model.find();
|
|
320
|
+
this.isExecuted = false;
|
|
321
|
+
this.paginationInfo = null;
|
|
322
|
+
logger.info('Query reset to initial state');
|
|
323
|
+
return this;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Clone the current SearchFeatures instance
|
|
328
|
+
* @returns {SearchFeatures} New SearchFeatures instance
|
|
329
|
+
*/
|
|
330
|
+
clone() {
|
|
331
|
+
const cloned = new SearchFeatures(this.Model, this.queryObj, this.options);
|
|
332
|
+
cloned.query = this.query.clone();
|
|
333
|
+
return cloned;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Apply full-text search using MongoDB text indexes
|
|
338
|
+
* @param {string} searchText - Text to search for
|
|
339
|
+
* @param {Object} [options] - Text search options
|
|
340
|
+
* @returns {SearchFeatures} Chainable instance
|
|
341
|
+
*/
|
|
342
|
+
textSearch(searchText, options = {}) {
|
|
343
|
+
try {
|
|
344
|
+
if (!searchText) {
|
|
345
|
+
return this;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const textSearchOptions = {
|
|
349
|
+
$search: searchText,
|
|
350
|
+
...options
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
this.query = this.query.find({ $text: textSearchOptions });
|
|
354
|
+
logger.info('Text search applied:', textSearchOptions);
|
|
355
|
+
|
|
356
|
+
return this;
|
|
357
|
+
} catch (error) {
|
|
358
|
+
logger.error('Text search failed:', error);
|
|
359
|
+
throw new Error(`Text search failed: ${error.message}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
safeSearch(obj) {
|
|
364
|
+
// Fallback to basic search if SearchFeatures fails
|
|
365
|
+
const { keyword } = this.queryObj || obj;
|
|
366
|
+
const limit = parseInt(this.queryObj.limit) || 10;
|
|
367
|
+
|
|
368
|
+
if (!keyword) {
|
|
369
|
+
return res.status(400).json({
|
|
370
|
+
success: false, result: null, message: "Search keyword is required"
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
this.query = this.query.find({
|
|
375
|
+
$or: [
|
|
376
|
+
{ name: { $regex: keyword, $options: 'i' } },
|
|
377
|
+
{ description: { $regex: keyword, $options: 'i' } }
|
|
378
|
+
]})
|
|
379
|
+
.limit(limit)
|
|
380
|
+
.sort({ createdAt: -1 });
|
|
381
|
+
|
|
382
|
+
return this;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
const logger = require("../logger");
|
|
2
|
+
const SearchApi = require('./api');
|
|
3
|
+
|
|
4
|
+
// Helper function to get/set model
|
|
5
|
+
let currentModel = null;
|
|
6
|
+
|
|
7
|
+
exports.getModel = () => {
|
|
8
|
+
return currentModel;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
exports.setModel = (Model) => {
|
|
12
|
+
currentModel = Model;
|
|
13
|
+
return currentModel;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
exports.basicSearch = async (params, Model = currentModel) => {
|
|
17
|
+
const queryParams = params || {
|
|
18
|
+
keyword: "laptop",
|
|
19
|
+
page: 1,
|
|
20
|
+
limit: 10
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const Search = new SearchApi(Model, queryParams, {
|
|
24
|
+
searchFields: ['name', 'description', 'brand']
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const results = await Search
|
|
28
|
+
.search()
|
|
29
|
+
.sort()
|
|
30
|
+
.paginate()
|
|
31
|
+
.execute();
|
|
32
|
+
|
|
33
|
+
console.log('Basic Search Results:', results);
|
|
34
|
+
return results;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
exports.textSearch = async (searchText, Model = currentModel) => {
|
|
38
|
+
const queryParams = { searchText } || { searchText: "marvel comic book" };
|
|
39
|
+
|
|
40
|
+
const Search = new SearchApi(Model, queryParams);
|
|
41
|
+
|
|
42
|
+
const results = await Search
|
|
43
|
+
.textSearch(queryParams.searchText, {
|
|
44
|
+
$language: 'english',
|
|
45
|
+
$caseSensitive: false
|
|
46
|
+
})
|
|
47
|
+
.sort({ score: { $meta: 'textScore' } })
|
|
48
|
+
.paginate()
|
|
49
|
+
.execute();
|
|
50
|
+
|
|
51
|
+
console.log('Full-text Search Results:', results);
|
|
52
|
+
return results;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
exports.standardSearch = async (Model, query) => {
|
|
56
|
+
const Search = new SearchApi(Model, query, {
|
|
57
|
+
searchFields: ['title', 'description', 'brand'],
|
|
58
|
+
defaultSort: { createdAt: -1 }
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const results = await Search.search()
|
|
62
|
+
.filter()
|
|
63
|
+
.sort()
|
|
64
|
+
.selectFields()
|
|
65
|
+
.populate('category brand')
|
|
66
|
+
.paginate()
|
|
67
|
+
.execute();
|
|
68
|
+
|
|
69
|
+
return results;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
exports.productSearch = async (query, Product) => {
|
|
73
|
+
const {
|
|
74
|
+
keyword,
|
|
75
|
+
category,
|
|
76
|
+
brand,
|
|
77
|
+
minPrice,
|
|
78
|
+
maxPrice,
|
|
79
|
+
rating,
|
|
80
|
+
sortBy = 'popularity',
|
|
81
|
+
page = 1,
|
|
82
|
+
limit = 24
|
|
83
|
+
} = query;
|
|
84
|
+
|
|
85
|
+
// Build query object
|
|
86
|
+
const queryObj = { page, limit };
|
|
87
|
+
if (keyword) queryObj.keyword = keyword;
|
|
88
|
+
if (category) queryObj.category = category;
|
|
89
|
+
if (brand) queryObj.brand = { in: brand }; // Support multiple brands
|
|
90
|
+
if (minPrice || maxPrice) {
|
|
91
|
+
queryObj.price = {};
|
|
92
|
+
if (minPrice) queryObj.price.gte = minPrice;
|
|
93
|
+
if (maxPrice) queryObj.price.lte = maxPrice;
|
|
94
|
+
}
|
|
95
|
+
if (rating) queryObj.rating = { gte: rating };
|
|
96
|
+
|
|
97
|
+
const sortOptions = {
|
|
98
|
+
popularity: { popularity: -1 },
|
|
99
|
+
newest: { createdAt: -1 },
|
|
100
|
+
'price-low': { price: 1 },
|
|
101
|
+
'price-high': { price: -1 },
|
|
102
|
+
rating: { rating: -1 }
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const Search = new SearchApi(Product, queryObj, {
|
|
106
|
+
searchFields: ['name', 'description', 'tags'],
|
|
107
|
+
defaultSort: sortOptions[sortBy] || sortOptions.popularity
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const results = await Search
|
|
111
|
+
.search(['name', 'description', 'brand', 'tags'])
|
|
112
|
+
.filter()
|
|
113
|
+
.sort()
|
|
114
|
+
.selectFields('-__v,-updatedAt')
|
|
115
|
+
.populate('reviews')
|
|
116
|
+
.paginate(limit, 50) // Max 50 items per page
|
|
117
|
+
.execute();
|
|
118
|
+
|
|
119
|
+
return results;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
exports.orderSearch = async (searchParams, Order) => {
|
|
123
|
+
const {
|
|
124
|
+
customerEmail,
|
|
125
|
+
status,
|
|
126
|
+
minAmount,
|
|
127
|
+
maxAmount,
|
|
128
|
+
dateFrom,
|
|
129
|
+
dateTo,
|
|
130
|
+
paymentMethod
|
|
131
|
+
} = searchParams;
|
|
132
|
+
|
|
133
|
+
const queryObj = {};
|
|
134
|
+
if (status) queryObj.status = status;
|
|
135
|
+
if (paymentMethod) queryObj.paymentMethod = paymentMethod;
|
|
136
|
+
|
|
137
|
+
if (minAmount || maxAmount) {
|
|
138
|
+
queryObj.totalAmount = {};
|
|
139
|
+
if (minAmount) queryObj.totalAmount.gte = parseFloat(minAmount);
|
|
140
|
+
if (maxAmount) queryObj.totalAmount.lte = parseFloat(maxAmount);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (dateFrom || dateTo) {
|
|
144
|
+
queryObj.orderDate = {};
|
|
145
|
+
if (dateFrom) queryObj.orderDate.gte = new Date(dateFrom);
|
|
146
|
+
if (dateTo) queryObj.orderDate.lte = new Date(dateTo);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const Search = new SearchApi(Order, queryObj);
|
|
150
|
+
|
|
151
|
+
let query = Search
|
|
152
|
+
.filter()
|
|
153
|
+
.sort({ orderDate: -1 })
|
|
154
|
+
.populate({
|
|
155
|
+
path: 'customer',
|
|
156
|
+
select: 'firstName lastName email'
|
|
157
|
+
})
|
|
158
|
+
.populate({
|
|
159
|
+
path: 'items.product',
|
|
160
|
+
select: 'name price'
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Add customer email search if provided
|
|
164
|
+
if (customerEmail) {
|
|
165
|
+
// This requires a more complex approach since we're searching in populated fields
|
|
166
|
+
query = query.populate({
|
|
167
|
+
path: 'customer',
|
|
168
|
+
match: { email: { $regex: customerEmail, $options: 'i' } },
|
|
169
|
+
select: 'firstName lastName email'
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return await query.paginate().execute();
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
exports.userSearch = async (adminQuery, User) => {
|
|
177
|
+
const {
|
|
178
|
+
keyword,
|
|
179
|
+
role,
|
|
180
|
+
status,
|
|
181
|
+
dateFrom,
|
|
182
|
+
dateTo,
|
|
183
|
+
sortBy = 'createdAt',
|
|
184
|
+
order = 'desc'
|
|
185
|
+
} = adminQuery;
|
|
186
|
+
|
|
187
|
+
const queryObj = {};
|
|
188
|
+
if (keyword) queryObj.keyword = keyword;
|
|
189
|
+
if (role) queryObj.role = role;
|
|
190
|
+
if (status) queryObj.status = status;
|
|
191
|
+
|
|
192
|
+
// Date range filtering
|
|
193
|
+
if (dateFrom || dateTo) {
|
|
194
|
+
queryObj.createdAt = {};
|
|
195
|
+
if (dateFrom) queryObj.createdAt.gte = new Date(dateFrom);
|
|
196
|
+
if (dateTo) queryObj.createdAt.lte = new Date(dateTo);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const sortOrder = order === 'desc' ? -1 : 1;
|
|
200
|
+
const sortOptions = { [sortBy]: sortOrder };
|
|
201
|
+
|
|
202
|
+
const Search = new SearchApi(User, queryObj, {
|
|
203
|
+
searchFields: ['firstName', 'lastName', 'email', 'username'],
|
|
204
|
+
excludeFields: ['password', 'resetToken']
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const results = await Search
|
|
208
|
+
.search()
|
|
209
|
+
.filter()
|
|
210
|
+
.sort(sortOptions)
|
|
211
|
+
.selectFields('-password,-resetToken,-__v')
|
|
212
|
+
.paginate(25)
|
|
213
|
+
.execute();
|
|
214
|
+
|
|
215
|
+
return results;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Also provide module.exports for compatibility
|
|
219
|
+
module.exports = exports;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const SearchApi = require('./api');
|
|
2
|
+
|
|
3
|
+
module.exports = class SearchService {
|
|
4
|
+
constructor(model, config = {}) {
|
|
5
|
+
this.model = model;
|
|
6
|
+
this.config = {
|
|
7
|
+
searchFields: [],
|
|
8
|
+
sortOptions: {},
|
|
9
|
+
filterRules: {},
|
|
10
|
+
...config
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async search(params) {
|
|
15
|
+
const searchFeatures = new SearchApi(this.model, params, {
|
|
16
|
+
searchFields: this.config.searchFields,
|
|
17
|
+
defaultSort: this.config.sortOptions.default || { createdAt: -1 }
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Apply custom filter rules
|
|
21
|
+
if (this.config.filterRules.beforeSearch) {
|
|
22
|
+
await this.config.filterRules.beforeSearch(searchFeatures, params);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let query = searchFeatures.search().filter().sort();
|
|
26
|
+
|
|
27
|
+
// Apply population rules
|
|
28
|
+
if (this.config.populate) {
|
|
29
|
+
query = query.populate(this.config.populate);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const results = await query.paginate().execute();
|
|
33
|
+
|
|
34
|
+
// Apply post-processing
|
|
35
|
+
if (this.config.filterRules.afterSearch) {
|
|
36
|
+
await this.config.filterRules.afterSearch(results, params);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return results;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/*
|
|
44
|
+
// Usage of Dynamic Search Service
|
|
45
|
+
const productSearchService = new DynamicSearchService(Product, {
|
|
46
|
+
searchFields: ['name', 'description', 'tags'],
|
|
47
|
+
populate: 'category reviews',
|
|
48
|
+
filterRules: {
|
|
49
|
+
beforeSearch: async (searchFeatures, params) => {
|
|
50
|
+
// Apply business logic before search
|
|
51
|
+
if (params.featured) {
|
|
52
|
+
searchFeatures.query = searchFeatures.query.find({ featured: true });
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
afterSearch: async (results, params) => {
|
|
56
|
+
// Apply business logic after search
|
|
57
|
+
results.data = results.data.map(item => ({
|
|
58
|
+
...item.toObject(),
|
|
59
|
+
discountedPrice: item.price * 0.9 // Apply 10% discount
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
*/
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const swaggerJSDoc = require("swagger-jsdoc");
|
|
2
|
+
|
|
3
|
+
const swaggerDefinition = {
|
|
4
|
+
openapi: "3.0.0",
|
|
5
|
+
info: {
|
|
6
|
+
title: "FEAR API",
|
|
7
|
+
version: "1.0.0",
|
|
8
|
+
description: "The Main API for all FEAR apps (Ekomix, Ekoins, Admin CRM",
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const options = {
|
|
13
|
+
swaggerDefinition,
|
|
14
|
+
apis: [`../routes/*.js`],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const swaggerSpec = swaggerJSDoc(options);
|
|
18
|
+
module.exports = swaggerSpec;
|