feat: add MongoDB search method, endpoints, and settings module
deploy-chabokan / deploy (push) Canceled after 0s
deploy-chabokan / deploy (push) Canceled after 0s
This commit is contained in:
@@ -11,7 +11,7 @@ exports.getCategoryTree = catchAsync(async (req, res, next) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
exports.getAllCategories = catchAsync(async (req, res, next) => {
|
exports.getAllCategories = catchAsync(async (req, res, next) => {
|
||||||
const categories = await categoryService.getAllCategories()
|
const categories = await categoryService.getAllCategories(req.query)
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
@@ -20,6 +20,7 @@ exports.getAllCategories = catchAsync(async (req, res, next) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
exports.createCategory = catchAsync(async (req, res, next) => {
|
exports.createCategory = catchAsync(async (req, res, next) => {
|
||||||
const category = await categoryService.createCategory(req.body)
|
const category = await categoryService.createCategory(req.body)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
const Category = require('./categoryModel')
|
const Category = require('./categoryModel')
|
||||||
const AppError = require('../../globalErrorHandler/AppError')
|
const AppError = require('../../globalErrorHandler/AppError')
|
||||||
|
|
||||||
module.exports.getAllCategories = async () => {
|
module.exports.getAllCategories = async (query = {}) => {
|
||||||
return Category.find().populate('features')
|
const mongoQuery = {}
|
||||||
|
const search = (query.search || query.query || '').trim()
|
||||||
|
if (search) {
|
||||||
|
mongoQuery.$or = [
|
||||||
|
{ nativeName: { $regex: search, $options: 'i' } },
|
||||||
|
{ englishName: { $regex: search, $options: 'i' } },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return Category.find(mongoQuery).populate('features')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports.createCategory = async (data) => {
|
module.exports.createCategory = async (data) => {
|
||||||
return Category.create(data, null)
|
return Category.create(data, null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -511,6 +511,19 @@ const permissionsList = [
|
|||||||
key: 'admindashboard',
|
key: 'admindashboard',
|
||||||
category: 'سیستم و داشبورد',
|
category: 'سیستم و داشبورد',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
englishName: 'View System Settings',
|
||||||
|
nativeName: 'مشاهده تنظیمات سیستم',
|
||||||
|
key: 'getsettings',
|
||||||
|
category: 'تنظیمات سیستم',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
englishName: 'Update System Settings',
|
||||||
|
nativeName: 'ویرایش تنظیمات سیستم',
|
||||||
|
key: 'updatesettings',
|
||||||
|
category: 'تنظیمات سیستم',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
module.exports = permissionsList
|
module.exports = permissionsList
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const propertyService = require('./propertyService')
|
|||||||
const { eventEmitter } = require('../../events/eventEmitter')
|
const { eventEmitter } = require('../../events/eventEmitter')
|
||||||
|
|
||||||
exports.searchProperties = catchAsync(async (req, res, next) => {
|
exports.searchProperties = catchAsync(async (req, res, next) => {
|
||||||
const { facets, belongings, totalCount, pagesCount } = await propertyService.searchProperties(req.query)
|
const { facets, belongings, totalCount, pagesCount, searchEngine } = await propertyService.searchProperties(req.query)
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
@@ -11,9 +11,24 @@ exports.searchProperties = catchAsync(async (req, res, next) => {
|
|||||||
properties: belongings,
|
properties: belongings,
|
||||||
totalCount,
|
totalCount,
|
||||||
pagesCount,
|
pagesCount,
|
||||||
|
searchEngine,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
exports.searchPropertiesMongo = catchAsync(async (req, res, next) => {
|
||||||
|
const { facets, belongings, totalCount, pagesCount, searchEngine } = await propertyService.searchPropertiesMongo(req.query)
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: 'success',
|
||||||
|
facets,
|
||||||
|
properties: belongings,
|
||||||
|
totalCount,
|
||||||
|
pagesCount,
|
||||||
|
searchEngine: searchEngine || 'mongo',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
exports.indexAllProperties = catchAsync(async (req, res, next) => {
|
exports.indexAllProperties = catchAsync(async (req, res, next) => {
|
||||||
const { indexedCount } = await propertyService.indexAllProperties()
|
const { indexedCount } = await propertyService.indexAllProperties()
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ router.post('/admin/index-all', perm.requires('indexproperties', 'indexbelonging
|
|||||||
router.post('/admin/index-one/:propertyId', perm.requires('indexproperties', 'indexbelongings'), propertyController.indexOneProperty)
|
router.post('/admin/index-one/:propertyId', perm.requires('indexproperties', 'indexbelongings'), propertyController.indexOneProperty)
|
||||||
|
|
||||||
router.get('/admin/search', perm.requires('searchproperties', 'searchbelongings'), propertyController.searchProperties)
|
router.get('/admin/search', perm.requires('searchproperties', 'searchbelongings'), propertyController.searchProperties)
|
||||||
|
router.get('/admin/search-mongo', perm.requires('searchproperties', 'searchbelongings'), propertyController.searchPropertiesMongo)
|
||||||
|
|
||||||
|
|
||||||
router.get('/admin/get-one/:propertyId', perm.requires('getaproperty', 'getabelonging'), propertyController.getProperty)
|
router.get('/admin/get-one/:propertyId', perm.requires('getaproperty', 'getabelonging'), propertyController.getProperty)
|
||||||
|
|
||||||
|
|||||||
@@ -90,15 +90,136 @@ const ensureCollectionExists = async (forceRecreate = false) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchProperties = async (inputQuery) => {
|
const settingService = require('../setting/settingService')
|
||||||
await ensureCollectionExists(false)
|
const mongoose = require('mongoose')
|
||||||
inputQuery = updateInputQuery(inputQuery, {})
|
|
||||||
|
|
||||||
const tsFeatures = new TsApiFeatures(client, 'properties', inputQuery).filter().facet().sort().limitFields().paginate().search()
|
const searchPropertiesMongo = async (inputQuery = {}) => {
|
||||||
|
const page = parseInt(inputQuery.page) || 1
|
||||||
|
const per_page = parseInt(inputQuery.per_page) || parseInt(inputQuery.limit) || 20
|
||||||
|
const mongoQuery = { isActive: { $ne: false } }
|
||||||
|
|
||||||
|
const q = (inputQuery.query || inputQuery.search || inputQuery.q || '').trim()
|
||||||
|
if (q && q !== '*') {
|
||||||
|
const searchRegex = { $regex: q, $options: 'i' }
|
||||||
|
const orConditions = [
|
||||||
|
{ name: searchRegex },
|
||||||
|
{ nationalGeoCode: searchRegex },
|
||||||
|
{ uniqueId: searchRegex },
|
||||||
|
{ serialNumber: searchRegex },
|
||||||
|
{ ownerName: searchRegex },
|
||||||
|
{ registrationPlate: searchRegex },
|
||||||
|
{ description: searchRegex },
|
||||||
|
{ address: searchRegex },
|
||||||
|
{ tags: searchRegex },
|
||||||
|
{ relatedProjects: searchRegex },
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!isNaN(q) && q !== '') {
|
||||||
|
orConditions.push({ propertyCode: Number(q) })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mongoose.Types.ObjectId.isValid(q)) {
|
||||||
|
orConditions.push({ _id: q })
|
||||||
|
}
|
||||||
|
|
||||||
|
mongoQuery.$or = orConditions
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputQuery.category && mongoose.Types.ObjectId.isValid(inputQuery.category)) {
|
||||||
|
mongoQuery.category = inputQuery.category
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputQuery.status && mongoose.Types.ObjectId.isValid(inputQuery.status)) {
|
||||||
|
mongoQuery.status = inputQuery.status
|
||||||
|
}
|
||||||
|
|
||||||
|
let sortObj = { createdAt: -1 }
|
||||||
|
if (inputQuery.sort) {
|
||||||
|
if (typeof inputQuery.sort === 'string') {
|
||||||
|
const parts = inputQuery.sort.split(':')
|
||||||
|
const field = parts[0]
|
||||||
|
const order = parts[1] === 'asc' ? 1 : -1
|
||||||
|
sortObj = { [field]: order }
|
||||||
|
} else if (typeof inputQuery.sort === 'object') {
|
||||||
|
sortObj = inputQuery.sort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalCount = await Property.countDocuments(mongoQuery)
|
||||||
|
const properties = await Property.find(mongoQuery)
|
||||||
|
.populate('category')
|
||||||
|
.populate('status')
|
||||||
|
.sort(sortObj)
|
||||||
|
.skip((page - 1) * per_page)
|
||||||
|
.limit(per_page)
|
||||||
|
|
||||||
|
const items = properties.map((prop) => {
|
||||||
|
const combinedTags = Array.isArray(prop.tags) ? prop.tags.join(' ') : (prop.tags || '')
|
||||||
|
return {
|
||||||
|
_id: prop._id.toString(),
|
||||||
|
id: prop.propertyCode ? prop.propertyCode.toString() : prop._id.toString(),
|
||||||
|
propertyCode: prop.propertyCode,
|
||||||
|
name: prop.name || '',
|
||||||
|
category: prop.category?.englishName || (typeof prop.category === 'string' ? prop.category : ''),
|
||||||
|
nativeCategory: prop.category?.nativeName || '',
|
||||||
|
categoryObj: prop.category,
|
||||||
|
status: prop.status,
|
||||||
|
imageUrl: prop.mainImageUrls?.[0] || 'https://cloud.mbmanager.ir/images/item-no-image.png',
|
||||||
|
mainImageUrls: prop.mainImageUrls || [],
|
||||||
|
documents: prop.documentUrls || [],
|
||||||
|
documentUrls: prop.documentUrls || [],
|
||||||
|
description: prop.description || '',
|
||||||
|
nationalGeoCode: prop.nationalGeoCode || '',
|
||||||
|
tags: combinedTags,
|
||||||
|
dong: prop.dong,
|
||||||
|
ownerName: prop.ownerName || '',
|
||||||
|
registrationPlate: prop.registrationPlate || '',
|
||||||
|
uniqueId: prop.uniqueId || '',
|
||||||
|
serialNumber: prop.serialNumber || '',
|
||||||
|
coordinates: prop.location?.coordinates || [0, 0],
|
||||||
|
location: prop.location,
|
||||||
|
createdAt: Math.floor(new Date(prop.createdAt || Date.now()).getTime() / 1000),
|
||||||
|
updatedAt: prop.updatedAt,
|
||||||
|
isActive: prop.isActive !== false,
|
||||||
|
features: prop.features || [],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const pagesCount = Math.ceil(totalCount / per_page) || 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
facets: [],
|
||||||
|
belongings: items,
|
||||||
|
properties: items,
|
||||||
|
totalCount,
|
||||||
|
pagesCount,
|
||||||
|
searchEngine: 'mongo',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchProperties = async (inputQuery) => {
|
||||||
|
try {
|
||||||
|
const setting = await settingService.getSearchSettingByPage('properties')
|
||||||
|
if (setting && setting.searchMethod === 'mongo') {
|
||||||
|
return searchPropertiesMongo(inputQuery)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to fetch search setting for properties, attempting typesense:', e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ensureCollectionExists(false)
|
||||||
|
const updatedQuery = updateInputQuery({ ...inputQuery }, {})
|
||||||
|
|
||||||
|
const tsFeatures = new TsApiFeatures(client, 'properties', updatedQuery).filter().facet().sort().limitFields().paginate().search()
|
||||||
|
|
||||||
let { facets, items, totalCount, pagesCount } = await tsFeatures.execute()
|
let { facets, items, totalCount, pagesCount } = await tsFeatures.execute()
|
||||||
|
|
||||||
return { facets, belongings: items, totalCount, pagesCount }
|
return { facets, belongings: items, properties: items, totalCount, pagesCount, searchEngine: 'typesense' }
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Typesense search failed for properties, falling back to MongoDB:', err.message)
|
||||||
|
return searchPropertiesMongo(inputQuery)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateInputQuery = (inputQuery, extraQueries) => {
|
const updateInputQuery = (inputQuery, extraQueries) => {
|
||||||
@@ -111,6 +232,7 @@ const updateInputQuery = (inputQuery, extraQueries) => {
|
|||||||
return inputQuery
|
return inputQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const indexAllProperties = async () => {
|
const indexAllProperties = async () => {
|
||||||
const batchSize = 20
|
const batchSize = 20
|
||||||
const count = await Property.countDocuments({ isActive: true }, null)
|
const count = await Property.countDocuments({ isActive: true }, null)
|
||||||
@@ -213,5 +335,7 @@ module.exports = {
|
|||||||
createProperty,
|
createProperty,
|
||||||
deleteProperty,
|
deleteProperty,
|
||||||
searchProperties,
|
searchProperties,
|
||||||
|
searchPropertiesMongo,
|
||||||
indexOneProperty,
|
indexOneProperty,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const Role = require('./roleModel')
|
|||||||
const roleService = require('./roleService')
|
const roleService = require('./roleService')
|
||||||
|
|
||||||
exports.getAllRoles = catchAsync(async (req, res, next) => {
|
exports.getAllRoles = catchAsync(async (req, res, next) => {
|
||||||
const roles = await roleService.getAllRoles()
|
const roles = await roleService.getAllRoles(req.query)
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
@@ -13,6 +13,7 @@ exports.getAllRoles = catchAsync(async (req, res, next) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
exports.createRole = catchAsync(async (req, res, next) => {
|
exports.createRole = catchAsync(async (req, res, next) => {
|
||||||
const role = await roleService.createRole(req.body)
|
const role = await roleService.createRole(req.body)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
const Role = require('./roleModel')
|
const Role = require('./roleModel')
|
||||||
const AppError = require('../../globalErrorHandler/AppError')
|
const AppError = require('../../globalErrorHandler/AppError')
|
||||||
|
|
||||||
module.exports.getAllRoles = async () => {
|
module.exports.getAllRoles = async (query = {}) => {
|
||||||
return Role.find({}).populate('permissions').sort({ createdAt: -1 })
|
const mongoQuery = {}
|
||||||
|
const search = (query.search || query.query || '').trim()
|
||||||
|
if (search) {
|
||||||
|
mongoQuery.$or = [
|
||||||
|
{ nativeName: { $regex: search, $options: 'i' } },
|
||||||
|
{ englishName: { $regex: search, $options: 'i' } },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return Role.find(mongoQuery).populate('permissions').sort({ createdAt: -1 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports.createRole = async (data) => {
|
module.exports.createRole = async (data) => {
|
||||||
const role = await Role.create(data)
|
const role = await Role.create(data)
|
||||||
return Role.findById(role._id).populate('permissions')
|
return Role.findById(role._id).populate('permissions')
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
const catchAsync = require('../../globalErrorHandler/catchAsync')
|
||||||
|
const settingService = require('./settingService')
|
||||||
|
|
||||||
|
exports.getSearchSettings = catchAsync(async (req, res, next) => {
|
||||||
|
const settings = await settingService.getSearchSettings()
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: 'success',
|
||||||
|
results: settings.length,
|
||||||
|
data: settings,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
exports.getSearchSettingByPage = catchAsync(async (req, res, next) => {
|
||||||
|
const setting = await settingService.getSearchSettingByPage(req.params.pageKey)
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: 'success',
|
||||||
|
data: setting,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
exports.updateSearchSetting = catchAsync(async (req, res, next) => {
|
||||||
|
const { searchMethod, extraConfig } = req.body
|
||||||
|
const setting = await settingService.setSearchMethod(req.params.pageKey, searchMethod, extraConfig)
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: 'success',
|
||||||
|
data: setting,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
exports.bulkUpdateSearchSettings = catchAsync(async (req, res, next) => {
|
||||||
|
const { settings } = req.body
|
||||||
|
const results = await settingService.bulkUpdateSearchSettings(settings)
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: 'success',
|
||||||
|
results: results.length,
|
||||||
|
data: results,
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
const mongoose = require('mongoose')
|
||||||
|
|
||||||
|
const settingSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
key: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: String,
|
||||||
|
default: 'search',
|
||||||
|
},
|
||||||
|
pageKey: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
searchMethod: {
|
||||||
|
type: String,
|
||||||
|
enum: ['typesense', 'mongo'],
|
||||||
|
default: 'mongo',
|
||||||
|
},
|
||||||
|
extraConfig: {
|
||||||
|
type: mongoose.Schema.Types.Mixed,
|
||||||
|
default: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
settingSchema.index({ key: 1 }, { unique: true })
|
||||||
|
settingSchema.index({ pageKey: 1 })
|
||||||
|
|
||||||
|
const Setting = mongoose.model('Setting', settingSchema)
|
||||||
|
module.exports = Setting
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
const express = require('express')
|
||||||
|
const settingController = require('./settingController')
|
||||||
|
const perm = require('../../middlewares/permissionMiddleware')
|
||||||
|
const loginCheck = require('../../middlewares/loginCheck')
|
||||||
|
|
||||||
|
const router = new express.Router()
|
||||||
|
|
||||||
|
router.use(loginCheck.loginProtect)
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/search-settings',
|
||||||
|
perm.requires('admindashboard', 'viewdashboard', 'getallpermissions'),
|
||||||
|
settingController.getSearchSettings
|
||||||
|
)
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/search-settings/:pageKey',
|
||||||
|
perm.requires('admindashboard', 'viewdashboard', 'getallpermissions'),
|
||||||
|
settingController.getSearchSettingByPage
|
||||||
|
)
|
||||||
|
|
||||||
|
router.put(
|
||||||
|
'/search-settings/:pageKey',
|
||||||
|
perm.requires('admindashboard', 'viewdashboard', 'getallpermissions'),
|
||||||
|
settingController.updateSearchSetting
|
||||||
|
)
|
||||||
|
|
||||||
|
router.put(
|
||||||
|
'/search-settings',
|
||||||
|
perm.requires('admindashboard', 'viewdashboard', 'getallpermissions'),
|
||||||
|
settingController.bulkUpdateSearchSettings
|
||||||
|
)
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
const Setting = require('./settingModel')
|
||||||
|
const AppError = require('../../globalErrorHandler/AppError')
|
||||||
|
|
||||||
|
const DEFAULT_SEARCH_PAGES = [
|
||||||
|
{
|
||||||
|
pageKey: 'properties',
|
||||||
|
title: 'املاک (Properties)',
|
||||||
|
description: 'جستجوی املاک، کد نوسازی، شناسه ملی جغرافیایی، مشخصات سند، مالک و پلاک ثبتی',
|
||||||
|
defaultMethod: 'typesense',
|
||||||
|
supportedMethods: ['typesense', 'mongo'],
|
||||||
|
endpointTypesense: '/api/properties/admin/search',
|
||||||
|
endpointMongo: '/api/properties/admin/search-mongo',
|
||||||
|
routePath: '/property/list-properties',
|
||||||
|
icon: 'pi pi-file',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pageKey: 'transactions',
|
||||||
|
title: 'تراکنشهای املاک (Transactions)',
|
||||||
|
description: 'جستجوی تراکنشهای واریز و برداشت، توضیحات، نام کاربر و ملک مرتبط',
|
||||||
|
defaultMethod: 'typesense',
|
||||||
|
supportedMethods: ['typesense', 'mongo'],
|
||||||
|
endpointTypesense: '/api/transactions/admin/search',
|
||||||
|
endpointMongo: '/api/transactions/admin/search-mongo',
|
||||||
|
routePath: '/transactions/list-transactions',
|
||||||
|
icon: 'pi pi-history',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pageKey: 'warehouseItems',
|
||||||
|
title: 'اقلام انبار (Warehouse Items)',
|
||||||
|
description: 'جستجوی اقلام کالا، نام فارسی، نام انگلیسی، کد کالا و دستهبندی',
|
||||||
|
defaultMethod: 'mongo',
|
||||||
|
supportedMethods: ['mongo', 'typesense'],
|
||||||
|
endpointTypesense: '/api/warehouse-items/admin/items',
|
||||||
|
endpointMongo: '/api/warehouse-items/admin/items',
|
||||||
|
routePath: '/warehouse-items/list',
|
||||||
|
icon: 'pi pi-box',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pageKey: 'warehouses',
|
||||||
|
title: 'مکانهای انبار (Warehouses)',
|
||||||
|
description: 'جستجوی نام انبار، کد و آدرس انبارها',
|
||||||
|
defaultMethod: 'mongo',
|
||||||
|
supportedMethods: ['mongo', 'typesense'],
|
||||||
|
endpointTypesense: '/api/warehouses/admin/get-all',
|
||||||
|
endpointMongo: '/api/warehouses/admin/get-all',
|
||||||
|
routePath: '/warehouse-items/warehouses',
|
||||||
|
icon: 'pi pi-map',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pageKey: 'categories',
|
||||||
|
title: 'دستهبندیها (Categories)',
|
||||||
|
description: 'جستجوی دستهبندیهای املاک بر اساس نام فارسی و انگلیسی',
|
||||||
|
defaultMethod: 'mongo',
|
||||||
|
supportedMethods: ['mongo', 'typesense'],
|
||||||
|
endpointTypesense: '/api/categories/admin/get-all',
|
||||||
|
endpointMongo: '/api/categories/admin/get-all',
|
||||||
|
routePath: '/categories/list-categories',
|
||||||
|
icon: 'pi pi-th-large',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pageKey: 'users',
|
||||||
|
title: 'کاربران (Users)',
|
||||||
|
description: 'جستجوی کاربران سیستم بر اساس نام، ایمیل و نقش کاربری',
|
||||||
|
defaultMethod: 'mongo',
|
||||||
|
supportedMethods: ['mongo', 'typesense'],
|
||||||
|
endpointTypesense: '/api/users/admin/get-all',
|
||||||
|
endpointMongo: '/api/users/admin/get-all',
|
||||||
|
routePath: '/users/list-users',
|
||||||
|
icon: 'pi pi-users',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pageKey: 'roles',
|
||||||
|
title: 'نقشها و دسترسیها (Roles)',
|
||||||
|
description: 'جستجوی نقشها بر اساس عنوان فارسی و انگلیسی',
|
||||||
|
defaultMethod: 'mongo',
|
||||||
|
supportedMethods: ['mongo', 'typesense'],
|
||||||
|
endpointTypesense: '/api/roles/admin/get-all',
|
||||||
|
endpointMongo: '/api/roles/admin/get-all',
|
||||||
|
routePath: '/roles/list-roles',
|
||||||
|
icon: 'pi pi-shield',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const getSearchSettings = async () => {
|
||||||
|
const dbSettings = await Setting.find({ category: 'search' }).lean()
|
||||||
|
const dbSettingsMap = new Map()
|
||||||
|
|
||||||
|
dbSettings.forEach((s) => {
|
||||||
|
dbSettingsMap.set(s.pageKey, s)
|
||||||
|
})
|
||||||
|
|
||||||
|
return DEFAULT_SEARCH_PAGES.map((pageDef) => {
|
||||||
|
const dbRecord = dbSettingsMap.get(pageDef.pageKey)
|
||||||
|
if (dbRecord) {
|
||||||
|
return {
|
||||||
|
...pageDef,
|
||||||
|
_id: dbRecord._id,
|
||||||
|
searchMethod: dbRecord.searchMethod || pageDef.defaultMethod,
|
||||||
|
isPersisted: true,
|
||||||
|
updatedAt: dbRecord.updatedAt,
|
||||||
|
createdAt: dbRecord.createdAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...pageDef,
|
||||||
|
searchMethod: pageDef.defaultMethod,
|
||||||
|
isPersisted: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSearchSettingByPage = async (pageKey) => {
|
||||||
|
const pageDef = DEFAULT_SEARCH_PAGES.find((p) => p.pageKey === pageKey)
|
||||||
|
if (!pageDef) {
|
||||||
|
throw new AppError('PageNotFound', 404, { message: `Page key ${pageKey} not found in search configurations` })
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbRecord = await Setting.findOne({ pageKey, category: 'search' }).lean()
|
||||||
|
if (dbRecord) {
|
||||||
|
return {
|
||||||
|
...pageDef,
|
||||||
|
_id: dbRecord._id,
|
||||||
|
searchMethod: dbRecord.searchMethod || pageDef.defaultMethod,
|
||||||
|
isPersisted: true,
|
||||||
|
updatedAt: dbRecord.updatedAt,
|
||||||
|
createdAt: dbRecord.createdAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...pageDef,
|
||||||
|
searchMethod: pageDef.defaultMethod,
|
||||||
|
isPersisted: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSearchMethod = async (pageKey, searchMethod, extraConfig = {}) => {
|
||||||
|
if (!['typesense', 'mongo'].includes(searchMethod)) {
|
||||||
|
throw new AppError('InvalidSearchMethod', 400, { message: 'Search method must be either "typesense" or "mongo"' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageDef = DEFAULT_SEARCH_PAGES.find((p) => p.pageKey === pageKey) || {
|
||||||
|
pageKey,
|
||||||
|
title: pageKey,
|
||||||
|
description: `Settings for ${pageKey}`,
|
||||||
|
defaultMethod: 'mongo',
|
||||||
|
}
|
||||||
|
|
||||||
|
const setting = await Setting.findOneAndUpdate(
|
||||||
|
{ pageKey, category: 'search' },
|
||||||
|
{
|
||||||
|
key: `search_${pageKey}`,
|
||||||
|
category: 'search',
|
||||||
|
pageKey,
|
||||||
|
title: pageDef.title,
|
||||||
|
description: pageDef.description,
|
||||||
|
searchMethod,
|
||||||
|
extraConfig,
|
||||||
|
},
|
||||||
|
{ upsert: true, new: true, setDefaultsOnInsert: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...pageDef,
|
||||||
|
_id: setting._id,
|
||||||
|
searchMethod: setting.searchMethod,
|
||||||
|
isPersisted: true,
|
||||||
|
updatedAt: setting.updatedAt,
|
||||||
|
createdAt: setting.createdAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bulkUpdateSearchSettings = async (settingsArray) => {
|
||||||
|
if (!Array.isArray(settingsArray)) {
|
||||||
|
throw new AppError('InvalidDataFormat', 400, { message: 'Expected an array of settings' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = []
|
||||||
|
for (const item of settingsArray) {
|
||||||
|
if (item.pageKey && item.searchMethod) {
|
||||||
|
const updated = await setSearchMethod(item.pageKey, item.searchMethod, item.extraConfig || {})
|
||||||
|
results.push(updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DEFAULT_SEARCH_PAGES,
|
||||||
|
getSearchSettings,
|
||||||
|
getSearchSettingByPage,
|
||||||
|
setSearchMethod,
|
||||||
|
bulkUpdateSearchSettings,
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ const catchAsync = require('../../globalErrorHandler/catchAsync')
|
|||||||
const transactionService = require('./transactionService')
|
const transactionService = require('./transactionService')
|
||||||
|
|
||||||
exports.searchTransactions = catchAsync(async (req, res, next) => {
|
exports.searchTransactions = catchAsync(async (req, res, next) => {
|
||||||
const { facets, transactions, totalCount, pagesCount } = await transactionService.searchTransactions(req.query)
|
const { facets, transactions, totalCount, pagesCount, searchEngine } = await transactionService.searchTransactions(req.query)
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
@@ -10,9 +10,24 @@ exports.searchTransactions = catchAsync(async (req, res, next) => {
|
|||||||
transactions,
|
transactions,
|
||||||
totalCount,
|
totalCount,
|
||||||
pagesCount,
|
pagesCount,
|
||||||
|
searchEngine,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
exports.searchTransactionsMongo = catchAsync(async (req, res, next) => {
|
||||||
|
const { facets, transactions, totalCount, pagesCount, searchEngine } = await transactionService.searchTransactionsMongo(req.query)
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: 'success',
|
||||||
|
facets,
|
||||||
|
transactions,
|
||||||
|
totalCount,
|
||||||
|
pagesCount,
|
||||||
|
searchEngine: searchEngine || 'mongo',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
exports.indexAllTransactions = catchAsync(async (req, res, next) => {
|
exports.indexAllTransactions = catchAsync(async (req, res, next) => {
|
||||||
const { indexedCount } = await transactionService.indexAllTransactions()
|
const { indexedCount } = await transactionService.indexAllTransactions()
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,13 @@ router.get(
|
|||||||
transactionController.searchTransactions
|
transactionController.searchTransactions
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/admin/search-mongo',
|
||||||
|
// perm.requires('getallbelongings'),
|
||||||
|
transactionController.searchTransactionsMongo
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/admin/get-stock-by-belonging/:belongingId',
|
'/admin/get-stock-by-belonging/:belongingId',
|
||||||
// perm.requires('getallbelongings'),
|
// perm.requires('getallbelongings'),
|
||||||
|
|||||||
@@ -165,15 +165,116 @@ const ensureCollectionExists = async (forceRecreate = false) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchTransactions = async (inputQuery) => {
|
const settingService = require('../setting/settingService')
|
||||||
await ensureCollectionExists(false)
|
|
||||||
inputQuery = updateInputQuery(inputQuery, {})
|
|
||||||
|
|
||||||
const tsFeatures = new TsApiFeatures(client, 'transactions', inputQuery).filter().facet().sort().limitFields().paginate().search()
|
const searchTransactionsMongo = async (inputQuery = {}) => {
|
||||||
|
const page = parseInt(inputQuery.page) || 1
|
||||||
|
const per_page = parseInt(inputQuery.per_page) || parseInt(inputQuery.limit) || 20
|
||||||
|
const mongoQuery = {}
|
||||||
|
|
||||||
|
const q = (inputQuery.query || inputQuery.search || inputQuery.q || '').trim()
|
||||||
|
if (q && q !== '*') {
|
||||||
|
const searchRegex = { $regex: q, $options: 'i' }
|
||||||
|
const orConditions = [
|
||||||
|
{ description: searchRegex },
|
||||||
|
{ userName: searchRegex },
|
||||||
|
{ type: searchRegex },
|
||||||
|
{ status: searchRegex },
|
||||||
|
{ source: searchRegex },
|
||||||
|
]
|
||||||
|
|
||||||
|
if (mongoose.Types.ObjectId.isValid(q)) {
|
||||||
|
orConditions.push({ _id: q })
|
||||||
|
orConditions.push({ propertyId: q })
|
||||||
|
orConditions.push({ userId: q })
|
||||||
|
}
|
||||||
|
|
||||||
|
mongoQuery.$or = orConditions
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputQuery.type) mongoQuery.type = inputQuery.type
|
||||||
|
if (inputQuery.status) mongoQuery.status = inputQuery.status
|
||||||
|
if (inputQuery.source) mongoQuery.source = inputQuery.source
|
||||||
|
if (inputQuery.propertyId && mongoose.Types.ObjectId.isValid(inputQuery.propertyId)) {
|
||||||
|
mongoQuery.propertyId = inputQuery.propertyId
|
||||||
|
}
|
||||||
|
if (inputQuery.userId && mongoose.Types.ObjectId.isValid(inputQuery.userId)) {
|
||||||
|
mongoQuery.userId = inputQuery.userId
|
||||||
|
}
|
||||||
|
|
||||||
|
let sortObj = { createdAt: -1 }
|
||||||
|
if (inputQuery.sort) {
|
||||||
|
if (typeof inputQuery.sort === 'string') {
|
||||||
|
const parts = inputQuery.sort.split(':')
|
||||||
|
const field = parts[0]
|
||||||
|
const order = parts[1] === 'asc' ? 1 : -1
|
||||||
|
sortObj = { [field]: order }
|
||||||
|
} else if (typeof inputQuery.sort === 'object') {
|
||||||
|
sortObj = inputQuery.sort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalCount = await Transaction.countDocuments(mongoQuery)
|
||||||
|
const transactions = await Transaction.find(mongoQuery)
|
||||||
|
.populate('propertyId')
|
||||||
|
.populate('userId')
|
||||||
|
.sort(sortObj)
|
||||||
|
.skip((page - 1) * per_page)
|
||||||
|
.limit(per_page)
|
||||||
|
|
||||||
|
const items = transactions.map((t) => {
|
||||||
|
return {
|
||||||
|
_id: t._id.toString(),
|
||||||
|
id: t._id.toString(),
|
||||||
|
type: t.type || '',
|
||||||
|
status: t.status || '',
|
||||||
|
source: t.source || '',
|
||||||
|
value: t.value || 0,
|
||||||
|
description: t.description || '',
|
||||||
|
createdAt: Math.floor(new Date(t.createdAt || t.date || Date.now()).getTime() / 1000),
|
||||||
|
date: t.date,
|
||||||
|
belongingName: t.propertyId?.name || '',
|
||||||
|
belongingId: t.propertyId?._id ? t.propertyId._id.toString() : '',
|
||||||
|
propertyId: t.propertyId,
|
||||||
|
userName: t.userName || t.userId?.name || '',
|
||||||
|
userId: t.userId,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const pagesCount = Math.ceil(totalCount / per_page) || 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
facets: [],
|
||||||
|
transactions: items,
|
||||||
|
totalCount,
|
||||||
|
pagesCount,
|
||||||
|
searchEngine: 'mongo',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchTransactions = async (inputQuery) => {
|
||||||
|
try {
|
||||||
|
const setting = await settingService.getSearchSettingByPage('transactions')
|
||||||
|
if (setting && setting.searchMethod === 'mongo') {
|
||||||
|
return searchTransactionsMongo(inputQuery)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to fetch search setting for transactions, attempting typesense:', e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ensureCollectionExists(false)
|
||||||
|
const updatedQuery = updateInputQuery({ ...inputQuery }, {})
|
||||||
|
|
||||||
|
const tsFeatures = new TsApiFeatures(client, 'transactions', updatedQuery).filter().facet().sort().limitFields().paginate().search()
|
||||||
|
|
||||||
let { facets, items, totalCount, pagesCount } = await tsFeatures.execute()
|
let { facets, items, totalCount, pagesCount } = await tsFeatures.execute()
|
||||||
|
|
||||||
return { facets, transactions: items, totalCount, pagesCount }
|
return { facets, transactions: items, totalCount, pagesCount, searchEngine: 'typesense' }
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Typesense search failed for transactions, falling back to MongoDB:', err.message)
|
||||||
|
return searchTransactionsMongo(inputQuery)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateInputQuery = (inputQuery, extraQueries) => {
|
const updateInputQuery = (inputQuery, extraQueries) => {
|
||||||
@@ -257,4 +358,6 @@ module.exports = {
|
|||||||
getTransactionByBelongingId,
|
getTransactionByBelongingId,
|
||||||
indexAllTransactions,
|
indexAllTransactions,
|
||||||
searchTransactions,
|
searchTransactions,
|
||||||
|
searchTransactionsMongo,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ exports.logoutUser = catchAsync(async (req, res, next) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
exports.getAllUsers = catchAsync(async (req, res, next) => {
|
exports.getAllUsers = catchAsync(async (req, res, next) => {
|
||||||
const users = await userService.getAllUsers()
|
const users = await userService.getAllUsers(req.query)
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
@@ -56,6 +56,7 @@ exports.getAllUsers = catchAsync(async (req, res, next) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
exports.userGetSelf = catchAsync(async (req, res, next) => {
|
exports.userGetSelf = catchAsync(async (req, res, next) => {
|
||||||
const user = req.user
|
const user = req.user
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
@@ -159,10 +159,19 @@ const loginUser = async (email, password) => {
|
|||||||
return { token: signToken(user._id), user }
|
return { token: signToken(user._id), user }
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAllUsers = async () => {
|
const getAllUsers = async (query = {}) => {
|
||||||
return User.find({}).populate('role').sort({ createdAt: -1 })
|
const mongoQuery = {}
|
||||||
|
const search = (query.search || query.query || '').trim()
|
||||||
|
if (search) {
|
||||||
|
mongoQuery.$or = [
|
||||||
|
{ name: { $regex: search, $options: 'i' } },
|
||||||
|
{ email: { $regex: search, $options: 'i' } },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return User.find(mongoQuery).populate('role').sort({ createdAt: -1 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const getUser = async (userId) => {
|
const getUser = async (userId) => {
|
||||||
const user = await User.findById(userId).populate('role')
|
const user = await User.findById(userId).populate('role')
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const fileRoutes = require('../components/file/fileRoutes')
|
|||||||
const featureRoutes = require('../components/feature/featureRoutes')
|
const featureRoutes = require('../components/feature/featureRoutes')
|
||||||
const warehouseItemRoutes = require('../components/warehouseItem/warehouseItemRoutes')
|
const warehouseItemRoutes = require('../components/warehouseItem/warehouseItemRoutes')
|
||||||
const warehouseRoutes = require('../components/warehouse/warehouseRoutes')
|
const warehouseRoutes = require('../components/warehouse/warehouseRoutes')
|
||||||
|
const settingRoutes = require('../components/setting/settingRoutes')
|
||||||
|
|
||||||
const routerEndpoints = new express.Router()
|
const routerEndpoints = new express.Router()
|
||||||
|
|
||||||
@@ -27,5 +28,7 @@ routerEndpoints.use('/files', fileRoutes)
|
|||||||
routerEndpoints.use('/features', featureRoutes)
|
routerEndpoints.use('/features', featureRoutes)
|
||||||
routerEndpoints.use('/warehouse-items', warehouseItemRoutes)
|
routerEndpoints.use('/warehouse-items', warehouseItemRoutes)
|
||||||
routerEndpoints.use('/warehouses', warehouseRoutes)
|
routerEndpoints.use('/warehouses', warehouseRoutes)
|
||||||
|
routerEndpoints.use('/settings', settingRoutes)
|
||||||
|
|
||||||
module.exports = routerEndpoints
|
module.exports = routerEndpoints
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user