From ca911b97a06f48497adf3dfff705b502012743be Mon Sep 17 00:00:00 2001 From: Kavehhn174 Date: Tue, 11 Aug 2026 20:35:47 +0330 Subject: [PATCH] =?UTF-8?q?REFACTOR:=20Belonging=20=E2=86=92=20Property=20?= =?UTF-8?q?system=20migration=20+=20utilities=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Property system: - Rename belonging → property throughout (model, controller, routes, service) - Add property sub-models: buildingModel, landModel, belongingModel - Add property lifecycle events: createProperty, deleteProperty - Add propertyStatus component (model, controller, routes, service, seeder) - Add commodity and commodityTransaction model stubs Infrastructure: - Update transactionModel and transactionService - Update events/index.js for new property events - Update seeder (seederDBConnection, seederRunner) - Update typesense client configuration - Update upload utilities (bucket, minio) - Update featureModel and featureSeeder - Update categorySeeder - Update TsApiFeatures utility - Remove docker-compose.yml (moved to root) - Remove legacy belonging component files --- components/belonging/belongingController.js | 81 - components/belonging/belongingRoutes.js | 26 - components/belonging/belongingService.js | 182 --- .../belonging/events/createBelonging.js | 6 - .../belonging/events/deleteBelonging.js | 6 - components/category/categorySeeder.js | 1 + components/commodity/commodityModel.js | 42 + .../commodityTransactionModel.js | 48 + components/feature/featureModel.js | 2 +- components/feature/featureSeeder.js | 1334 ++++++++--------- .../{belonging => property}/belongingModel.js | 4 + components/property/buildingModel.js | 42 + components/property/events/createProperty.js | 6 + components/property/events/deleteProperty.js | 6 + components/property/landModel.js | 30 + components/property/propertyController.js | 81 + components/property/propertyModel.js | 130 ++ components/property/propertyRoutes.js | 26 + components/property/propertyService.js | 202 +++ .../propertyStatusController.js | 48 + .../propertyStatus/propertyStatusModel.js | 28 + .../propertyStatus/propertyStatusRoutes.js | 20 + .../propertyStatus/propertyStatusSeeder.js | 32 + .../propertyStatus/propertyStatusService.js | 42 + components/transaction/transactionModel.js | 12 +- components/transaction/transactionService.js | 88 +- config/.env | 20 +- docker-compose.yml | 56 - events/index.js | 12 +- seeder/seederDBConnection.js | 44 +- seeder/seederRunner.js | 9 + setup/typesense.js | 11 +- utils/TsApiFeatures.js | 3 +- utils/upload/bucket.js | 9 +- utils/upload/minio.js | 38 +- 35 files changed, 1504 insertions(+), 1223 deletions(-) delete mode 100644 components/belonging/belongingController.js delete mode 100644 components/belonging/belongingRoutes.js delete mode 100644 components/belonging/belongingService.js delete mode 100644 components/belonging/events/createBelonging.js delete mode 100644 components/belonging/events/deleteBelonging.js create mode 100644 components/commodity/commodityModel.js create mode 100644 components/commodityTransaction/commodityTransactionModel.js rename components/{belonging => property}/belongingModel.js (95%) create mode 100644 components/property/buildingModel.js create mode 100644 components/property/events/createProperty.js create mode 100644 components/property/events/deleteProperty.js create mode 100644 components/property/landModel.js create mode 100644 components/property/propertyController.js create mode 100644 components/property/propertyModel.js create mode 100644 components/property/propertyRoutes.js create mode 100644 components/property/propertyService.js create mode 100644 components/propertyStatus/propertyStatusController.js create mode 100644 components/propertyStatus/propertyStatusModel.js create mode 100644 components/propertyStatus/propertyStatusRoutes.js create mode 100644 components/propertyStatus/propertyStatusSeeder.js create mode 100644 components/propertyStatus/propertyStatusService.js delete mode 100644 docker-compose.yml diff --git a/components/belonging/belongingController.js b/components/belonging/belongingController.js deleted file mode 100644 index c275028..0000000 --- a/components/belonging/belongingController.js +++ /dev/null @@ -1,81 +0,0 @@ -const catchAsync = require('../../globalErrorHandler/catchAsync') -const belongingService = require('./belongingService') -const { eventEmitter } = require('../../events/eventEmitter') - -exports.searchBelongings = catchAsync(async (req, res, next) => { - const { facets, belongings, totalCount, pagesCount } = await belongingService.searchBelongings(req.query) - - res.status(200).json({ - status: 'success', - facets, - belongings, - totalCount, - pagesCount, - }) -}) - -exports.indexAllBelongings = catchAsync(async (req, res, next) => { - const { indexedCount } = await belongingService.indexAllBelongings() - - res.status(200).json({ - status: 'success', - indexedCount, - }) -}) - -exports.indexOneBelonging = catchAsync(async (req, res, next) => { - await belongingService.indexOneBelonging() - - res.status(200).json({ - status: 'success', - }) -}) - -exports.getAllBelongings = catchAsync(async (req, res, next) => { - const belongings = await belongingService.getAllBelongings() - - res.status(200).json({ - status: 'success', - results: belongings.length, - data: belongings, - }) -}) - -exports.createBelonging = catchAsync(async (req, res, next) => { - const belonging = await belongingService.createBelonging(req.body) - eventEmitter.emit('createBelonging', belonging) - - res.status(201).json({ - status: 'success', - data: belonging, - }) -}) - -exports.getBelonging = catchAsync(async (req, res, next) => { - const belonging = await belongingService.getBelonging(req.params.belongingId) - - res.status(200).json({ - status: 'success', - data: belonging, - }) -}) - -exports.updateBelonging = catchAsync(async (req, res, next) => { - const belonging = await belongingService.updateBelonging(req.params.belongingId, req.body) - await belongingService.indexAllBelongings() - - res.status(200).json({ - status: 'success', - data: belonging, - }) -}) - -exports.deleteBelonging = catchAsync(async (req, res, next) => { - await belongingService.deleteBelonging(req.params.belongingId) - // eventEmitter.emit('deleteBelonging') - - res.status(204).json({ - status: 'success', - data: null, - }) -}) diff --git a/components/belonging/belongingRoutes.js b/components/belonging/belongingRoutes.js deleted file mode 100644 index 1ed1d21..0000000 --- a/components/belonging/belongingRoutes.js +++ /dev/null @@ -1,26 +0,0 @@ -const express = require('express') -const belongingController = require('./belongingController') -const perm = require('../../middlewares/permissionMiddleware') -const loginCheck = require('../../middlewares/loginCheck') - -const router = new express.Router() - -router.use(loginCheck.loginProtect) - -router.get('/admin/get-all', perm.requires('getallbelongings'), belongingController.getAllBelongings) - -router.post('/admin/create', perm.requires('createabelonging'), belongingController.createBelonging) - -router.post('/admin/index-all', perm.requires('indexbelongings'), belongingController.indexAllBelongings) - -router.post('/admin/index-one/:belongingId', perm.requires('indexbelongings'), belongingController.indexOneBelonging) - -router.get('/admin/search', perm.requires('searchbelongings'), belongingController.searchBelongings) - -router.get('/admin/get-one/:belongingId', perm.requires('getabelonging'), belongingController.getBelonging) - -router.put('/admin/update/:belongingId', perm.requires('updateabelonging'), belongingController.updateBelonging) - -router.delete('/admin/delete/:belongingId', perm.requires('deleteabelonging'), belongingController.deleteBelonging) - -module.exports = router diff --git a/components/belonging/belongingService.js b/components/belonging/belongingService.js deleted file mode 100644 index ab62140..0000000 --- a/components/belonging/belongingService.js +++ /dev/null @@ -1,182 +0,0 @@ -const Belonging = require('./belongingModel') -const AppError = require('../../globalErrorHandler/AppError') -const { client } = require('../../setup/typesense') -const TsApiFeatures = require('../../utils/TsApiFeatures') - -const getAllBelongings = async () => { - return Belonging.find({}, null, null).populate('category') -} - -const createBelonging = async (data) => { - return Belonging.create(data, null) -} - -const getBelonging = async (belongingId) => { - let belonging = await Belonging.findById(belongingId, null, null) - - if (!belonging) { - throw new AppError('NoBelongingFound', 404, {}) - } - - const documentUrls = await belonging.getDocumentsSignedUrls() - const mainImageUrls = await belonging.getMainImagedSignedUrl() - - belonging = JSON.parse(JSON.stringify(belonging)) - - belonging.signedDocumentUrls = documentUrls - belonging.signedMainImageUrls = mainImageUrls - - return belonging -} - -const updateBelonging = async (belongingId, data) => { - const belonging = await Belonging.findOneAndUpdate({ _id: belongingId }, data, { new: true }) - - if (!belonging) { - throw new AppError('NoBelongingFound', 404, {}) - } - - return belonging -} - -const deleteBelonging = async (belongingId) => { - const belonging = await Belonging.findByIdAndUpdate(belongingId, { isActive: false }, { new: true }) - - if (!belonging) { - throw new AppError('NoBelongingFound', 404, {}) - } - - return null -} - -const ensureCollectionExists = async () => { - const collections = await client.collections().retrieve() - const collectionExists = collections.some((collection) => collection.name === 'belongings') - - if (collectionExists) { - await client.collections('belongings').delete() - } - - const schema = getTypeSenseSchema() - - await client.collections().create(schema) - console.log('Collection "belongings" created successfully') -} - -const searchBelongings = async (inputQuery) => { - inputQuery = updateInputQuery(inputQuery, {}) - - const tsFeatures = new TsApiFeatures(client, 'belongings', inputQuery).filter().facet().sort().limitFields().paginate().search() - - let { facets, items, totalCount, pagesCount } = await tsFeatures.execute() - - return { facets, belongings: items, totalCount, pagesCount } -} - -const updateInputQuery = (inputQuery, extraQueries, features) => { - for (const [key, value] of Object.entries(extraQueries)) { - inputQuery[key] = value - } - - inputQuery.query_by = 'name,unit,tags,description,_id' - - return inputQuery -} - -const indexAllBelongings = async () => { - const batchSize = 20 - const count = await Belonging.countDocuments({isActive: true}, null) - const rounds = Math.ceil(count / batchSize) - - await ensureCollectionExists() - - for (let i = 1; i <= rounds; i++) { - console.log(`Indexing ${batchSize} belongings, Round ${i}`) - - const belongings = await Belonging.find({isActive: true}, null, null) - .populate('category') - .skip((i - 1) * batchSize) - .limit(batchSize) - - let documents = [] - - for (const belonging of belongings) { - - const combinedTags = belonging.tags.join(' '); - - const data = { - _id: belonging._id, - isActive: belonging.isActive, - // belongingCode: belonging.belongingCode, - name: belonging.name, - category: belonging.category?.englishName || '', - nativeCategory: belonging.category?.nativeName || '', - imageUrl: belonging.mainImageUrls[0] || 'https://cloud.mbmanager.ir/images/item-no-image.png', - documents: belonging.documentUrls || [] , - description: belonging.description || '', - unit: belonging.unit || '', - tags: combinedTags || '', - coordinates: belonging.location.coordinates || [0, 0], - createdAt: Math.floor(new Date(belonging.createdAt).getTime() / 1000), // Store as timestamp in seconds - id: belonging.belongingCode.toString(), - } - - documents.push(data) - } - - await client.collections('belongings').documents().import(documents, { action: 'emplace' }) - } - - return { totalCount: count } -} - -const indexOneBelonging = async (belongingId) => { - const belonging = await getBelonging(belongingId) - - console.log(`Belonging ${belonging._id} indexing`) - - const combinedTags = belonging.tags.join(' '); - - const data = { - _id: belonging._id, - isActive: belonging.isActive, - // belongingCode: belonging.belongingCode, - name: belonging.name, - category: belonging.category?.englishName || '', - nativeCategory: belonging.category?.nativeName || '', - imageUrl: belonging.mainImageUrls[0] || '', - documents: belonging.documentUrls || [] , - description: belonging.description || '', - unit: belonging.unit || '', - tags: combinedTags || '', - coordinates: belonging.location.coordinates || [0, 0], - createdAt: Math.floor(new Date(belonging.createdAt).getTime() / 1000), // Store as timestamp in seconds - id: belonging.belongingCode.toString(), - } - - await client.collections('belongings').documents().upsert(data) - -} - -const getTypeSenseSchema = () => { - return { - name: 'belongings', - enable_nested_fields: true, - fields: [ - { name: '_id', type: 'string', facet: false, infix: false }, - { name: 'name', type: 'string', facet: false, infix: true }, - // { name: 'belongingCode', type: 'int64', facet: false }, - { name: 'category', type: 'string', facet: true, infix: true }, - { name: 'nativeCategory', type: 'string', facet: false, infix: true }, - { name: 'description', type: 'string', facet: false }, - { name: 'unit', type: 'string', facet: false, infix: true }, - { name: 'tags', type: 'string', facet: false, infix: true }, - { name: 'coordinates', type: 'geopoint' }, - { name: 'createdAt', type: 'int64', facet: false }, - { name: 'imageUrl', type: 'string', facet: false, infix: true }, - ], - default_sorting_field: 'createdAt', - } -} - -module.exports = { updateBelonging, indexAllBelongings, getBelonging, getAllBelongings, createBelonging, deleteBelonging, searchBelongings, indexOneBelonging } diff --git a/components/belonging/events/createBelonging.js b/components/belonging/events/createBelonging.js deleted file mode 100644 index c79edce..0000000 --- a/components/belonging/events/createBelonging.js +++ /dev/null @@ -1,6 +0,0 @@ -const belongingService = require('../belongingService') -module.exports = (eventEmitter) => { - eventEmitter.on('createBelonging', async (belongingData) => { - await belongingService.indexOneBelonging(belongingData._id) - }) -} diff --git a/components/belonging/events/deleteBelonging.js b/components/belonging/events/deleteBelonging.js deleted file mode 100644 index 59d618f..0000000 --- a/components/belonging/events/deleteBelonging.js +++ /dev/null @@ -1,6 +0,0 @@ -const belongingService = require('../belongingService') -module.exports = (eventEmitter) => { - eventEmitter.on('deleteBelonging', async (belongingData) => { - await belongingService.indexAllBelongings() - }) -} diff --git a/components/category/categorySeeder.js b/components/category/categorySeeder.js index 2fdaa03..66995a3 100644 --- a/components/category/categorySeeder.js +++ b/components/category/categorySeeder.js @@ -6,6 +6,7 @@ const runSeeder = async () => { { nativeName: 'ساختمان', englishName: 'Building' }, { nativeName: 'زمین', englishName: 'Lot' }, { nativeName: 'کالا', englishName: 'Product' }, + { nativeName: 'خودرو', englishName: 'Car' }, ] await Category.deleteMany({}) diff --git a/components/commodity/commodityModel.js b/components/commodity/commodityModel.js new file mode 100644 index 0000000..4bd8be2 --- /dev/null +++ b/components/commodity/commodityModel.js @@ -0,0 +1,42 @@ +const mongoose = require('mongoose') +const ObjectID = mongoose.Schema.Types.ObjectId + +const commoditySchema = new mongoose.Schema( + { + name: { + type: String, + required: true, + }, + code: { + type: String, + unique: true, + }, + category: { + type: ObjectID, + ref: 'Category', + }, + count: { + type: Number, + default: 0, + }, + unit: { + type: String, + required: true, // e.g. عدد، کیلوگرم، متر، ... + }, + warehouse: { + type: ObjectID, + ref: 'Warehouse', + }, + description: String, + isActive: { + type: Boolean, + default: true, + }, + }, + { + timestamps: true, + } +) + +const Commodity = mongoose.model('Commodity', commoditySchema) +module.exports = Commodity diff --git a/components/commodityTransaction/commodityTransactionModel.js b/components/commodityTransaction/commodityTransactionModel.js new file mode 100644 index 0000000..be46275 --- /dev/null +++ b/components/commodityTransaction/commodityTransactionModel.js @@ -0,0 +1,48 @@ +const mongoose = require('mongoose') +const ObjectID = mongoose.Schema.Types.ObjectId + +const commodityTransactionSchema = new mongoose.Schema( + { + commodity: { + type: ObjectID, + ref: 'Commodity', + required: true, + }, + type: { + type: String, + enum: ['IN', 'OUT', 'TRANSFER'], // ورود، خروج، انتقال بین انبارها + required: true, + }, + count: { + type: Number, + required: true, + }, + unit: { + type: String, + required: true, + }, + sourceWarehouse: { + type: ObjectID, + ref: 'Warehouse', + }, + destinationWarehouse: { + type: ObjectID, + ref: 'Warehouse', + }, + user: { + type: ObjectID, + ref: 'User', + }, + date: { + type: Date, + default: Date.now, + }, + description: String, + }, + { + timestamps: true, + } +) + +const CommodityTransaction = mongoose.model('CommodityTransaction', commodityTransactionSchema) +module.exports = CommodityTransaction diff --git a/components/feature/featureModel.js b/components/feature/featureModel.js index 333d28b..62d8d91 100644 --- a/components/feature/featureModel.js +++ b/components/feature/featureModel.js @@ -23,7 +23,7 @@ const featureSchema = new mongoose.Schema({ }, featureType: { type: String, - enum: ['string', 'boolean', 'number', 'date'], + enum: ['string', 'textarea', 'boolean', 'number', 'date'], default: 'string', }, filterType: { diff --git a/components/feature/featureSeeder.js b/components/feature/featureSeeder.js index abc2d85..6639e44 100644 --- a/components/feature/featureSeeder.js +++ b/components/feature/featureSeeder.js @@ -1,836 +1,684 @@ const Feature = require('./featureModel') +const Category = require('../category/categoryModel') const chalk = require('chalk') const runSeeder = async () => { - const features2 = [ + // Fetch all categories dynamically so we don't rely on hardcoded ObjectIDs + const allCategories = await Category.find({}) + const categoryMap = {} + for (const cat of allCategories) { + categoryMap[cat.englishName] = cat._id + } + + const lot = categoryMap['Lot'] + const building = categoryMap['Building'] + const car = categoryMap['Car'] + + if (!lot || !building || !car) { + console.log(chalk.red('One or more required categories not found. Run category seeder first.')) + console.log('Found categories:', Object.keys(categoryMap)) + return + } + + // ───────────────────────────────────────────────────────────────────────── + // Helper: builds the shared registration/ownership/legal block for a given + // category ObjectID. Used for both Lot and Building. + // ───────────────────────────────────────────────────────────────────────── + const sharedRealEstateFields = (category) => [ { - _id: '669d533e90cf9da0fdfabdcb', - nativeName: 'مدل پردازنده', - englishName: 'lcpu', - group: '63822b8fa964fe72ec7be6a0', - predefinedValues: [ - '12450H', - '13500H', - '13620H', - '13700H', - '13900H', - '13500HX', - '13500HX', - '13650HX', - '13700HX', - '13900HX', - '13950HX', - '13980HX', - '7735H', - '7940H', - '7840H', - '7745HX', - '7845HX', - '7940HX', - '7945HX', - '125U', - '135U', - '155U', - '165U', - '125H', - '135H', - '155H', - '165H', - '134U', - '164U', - '185H', - '14500HX', - '14700HX', - '14900HX', - '8945H', - '8845H', - '8840H', - 'X1E-84-100', - 'X1E-80-100', - 'X1E-78-100', - 'X1P-64-100', - ], - isFilterable: true, + nativeName: 'نام مالک', + englishName: 'ownerName', + category, + featureType: 'string', filterType: 'exact', - tag: 'پردازنده', - priority: 50, - }, - { - _id: '669d533e90cf9da0fdfabdcc', - nativeName: 'برند پردازنده', - englishName: 'lcpubrand', - group: '63822b8fa964fe72ec7be6a0', - predefinedValues: ['Intel', 'AMD'], - isFilterable: true, - filterType: 'exact', - tag: 'پردازنده', - priority: 30, - }, - { - _id: '669d533e90cf9da0fdfabdcd', - nativeName: 'برند کارت گرافیک', - englishName: 'lgpubrand', - group: '63822b8fa964fe72ec7be6a0', - predefinedValues: ['Nvidia', 'AMD', 'Intel'], - isFilterable: true, - filterType: 'exact', - tag: 'کارت گرافیک', - }, - { - _id: '669d533e90cf9da0fdfabdce', - nativeName: 'مدل کارت گرافیک', - englishName: 'lgpumodel', - group: '63822b8fa964fe72ec7be6a0', - predefinedValues: ['NVIDIA GeForce GTX 4060', 'AMD Radeon RX 7600M XT'], - isFilterable: true, - filterType: 'exact', - tag: 'کارت گرافیک', - }, - { - _id: '669d533e90cf9da0fdfabdcf', - nativeName: 'رزولوشن صفحه نمایش', - englishName: 'ldisres', - group: '63822b8fa964fe72ec7be6a0', - predefinedValues: ['Full HD', '2.5K', '3.2K', '4K'], - isFilterable: true, - filterType: 'exact', - tag: 'کارت گرافیک', - }, - { - _id: '669d533e90cf9da0fdfabdd0', - nativeName: 'رم', - englishName: 'lramcap', - group: '63822b8fa964fe72ec7be6a0', - predefinedValues: [8, 16, 32], - unit: 'GB', - isFilterable: true, - filterType: 'exact', - tag: 'رم', - }, - { - _id: '669d533e90cf9da0fdfabdd1', - nativeName: 'نوع رم', - englishName: 'lramtype', - group: '63822b8fa964fe72ec7be6a0', - predefinedValues: ['DDR4', 'DDR5', 'LPDDR5X'], - isFilterable: true, - filterType: 'exact', - tag: 'رم', - }, - { - _id: '669d533e90cf9da0fdfabdd2', - nativeName: 'فرکانس رم', - englishName: 'lramfreq', - group: '63822b8fa964fe72ec7be6a0', - predefinedValues: [3200, 4800, 5200, 6400], - unit: 'Mhz', - isFilterable: true, - filterType: 'exact', - tag: 'رم', - }, - { - _id: '669d533e90cf9da0fdfabdd3', - nativeName: 'حافظه HDD', - englishName: 'lhdd', - group: '63822b8fa964fe72ec7be6a0', - predefinedValues: ['دارد', 'ندارد'], isFilterable: false, - filterType: 'bool', - tag: 'حافظه', + tag: 'مالکیت', + priority: 200, }, { - _id: '669d533e90cf9da0fdfabdd4', - nativeName: 'حافظه SSD', - englishName: 'lssd', - group: '63822b8fa964fe72ec7be6a0', - predefinedValues: ['دارد', 'ندارد'], - isFilterable: false, - filterType: 'bool', - tag: 'حافظه', - }, - { - _id: '669d533e90cf9da0fdfabdd5', - nativeName: 'حجم حافظه SSD', - englishName: 'lssdcap', - group: '63822b8fa964fe72ec7be6a0', - predefinedValues: [128, 256, 512, 1024, 2048], - isFilterable: true, - unit: 'GB', + nativeName: 'پلاک ثبتی', + englishName: 'registrationPlate', + category, + featureType: 'string', filterType: 'exact', - tag: 'حافظه', + isFilterable: false, + tag: 'ثبت', + priority: 195, + }, + { + nativeName: 'شماره اصلی', + englishName: 'mainNumber', + category, + featureType: 'string', + filterType: 'exact', + isFilterable: false, + tag: 'ثبت', + priority: 194, + }, + { + nativeName: 'شماره فرعی', + englishName: 'subNumber', + category, + featureType: 'string', + filterType: 'exact', + isFilterable: false, + tag: 'ثبت', + priority: 193, + }, + { + nativeName: 'بخش ثبتی', + englishName: 'registrationSection', + category, + featureType: 'string', + filterType: 'exact', + isFilterable: true, + tag: 'ثبت', + priority: 192, + }, + { + nativeName: 'قطعه', + englishName: 'plotNumber', + category, + featureType: 'string', + filterType: 'exact', + isFilterable: false, + tag: 'ثبت', + priority: 191, + }, + { + nativeName: 'ناحیه', + englishName: 'district', + category, + featureType: 'string', + filterType: 'exact', + isFilterable: true, + tag: 'موقعیت', + priority: 190, + }, + { + nativeName: 'کدپستی', + englishName: 'postalCode', + category, + featureType: 'string', + filterType: 'exact', + isFilterable: false, + tag: 'موقعیت', + priority: 189, + }, + { + nativeName: 'شناسه ملی جغرافیایی', + englishName: 'nationalGeoId', + category, + featureType: 'string', + filterType: 'exact', + isFilterable: false, + tag: 'ثبت', + priority: 188, + }, + { + nativeName: 'آدرس', + englishName: 'address', + category, + featureType: 'textarea', + filterType: 'exact', + isFilterable: false, + tag: 'موقعیت', + priority: 187, + }, + { + nativeName: 'حدود مفروزات', + englishName: 'propertyBoundaries', + category, + featureType: 'textarea', + filterType: 'exact', + isFilterable: false, + tag: 'ثبت', + priority: 186, + }, + { + nativeName: 'مستندات مالکیت', + englishName: 'ownershipDocs', + category, + featureType: 'textarea', + filterType: 'exact', + isFilterable: false, + tag: 'مالکیت', + priority: 185, + }, + { + nativeName: 'وضعیت خاص', + englishName: 'specialStatus', + category, + featureType: 'string', + filterType: 'exact', + predefinedValues: ['توقیف', 'رهن', 'وراثتی', 'اوقافی', 'دولتی', 'در دست ساخت', 'مسدود'], + isFilterable: true, + tag: 'مالکیت', + priority: 184, + }, + { + nativeName: 'نوع سند', + englishName: 'documentType', + category, + featureType: 'string', + filterType: 'exact', + predefinedValues: ['سند تک‌برگ', 'منگوله‌دار', 'وقفی', 'مشاع', 'بنچاق', 'قولنامه‌ای'], + isFilterable: true, + tag: 'سند', + priority: 183, }, ] const features = [ - { - _id: '63839aed70a51325208b0584', - fullNativeName: 'ابعاد (میلی متر) مشخصات فیزیکی لپتاپ', - nativeName: 'ابعاد (میلی متر)', - englishName: 'ldim', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'مشخصات فیزیکی', - priority: 0, - }, - { - _id: '63839f834c94732a541ce1fd', - fullNativeName: 'وزن (کیلوگرم) مشخصات فیزیکی لپتاپ', - nativeName: 'وزن (کیلوگرم)', - englishName: 'lwe', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'مشخصات فیزیکی', - priority: 0, - }, - { - _id: '63f22c7d358e3427d4428214', - fullNativeName: 'جنس بدنه مشخصات فیزیکی لپتاپ', - nativeName: 'جنس بدنه', - englishName: 'lbmat', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'مشخصات فیزیکی', - priority: 0, - }, - { - _id: '659e2ba341b67ad77d249b17', - fullNativeName: 'سیستم کولینگ و خنک کنندگی مشخصات فیزیکی لپتاپ', - nativeName: 'سیستم کولینگ و خنک کنندگی', - englishName: 'lcooling', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'مشخصات فیزیکی', - priority: 0, - }, - { - _id: '63846e760df5e6a52824d2dc', - fullNativeName: 'ابعاد (اینچ) صفحه نمایش لپتاپ', - nativeName: 'ابعاد (اینچ)', - englishName: 'ldisdim', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'صفحه نمایش', - priority: 0, - }, - { - _id: '63846e7d0df5e6a52824d2e4', - fullNativeName: 'وضوح صفحه نمایش لپتاپ', - nativeName: 'وضوح', - englishName: 'ldisqua', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'صفحه نمایش', - priority: 0, - }, - { - _id: '63846e7f0df5e6a52824d2ec', - fullNativeName: 'رفرش ریت (نرخ نوسازی) صفحه نمایش لپتاپ', - nativeName: 'رفرش ریت (نرخ نوسازی)', - englishName: 'ldisref', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'صفحه نمایش', - priority: 0, - }, - { - _id: '63846e840df5e6a52824d2f4', - fullNativeName: 'میزان روشنایی (نیت) صفحه نمایش لپتاپ', - nativeName: 'میزان روشنایی (نیت)', - englishName: 'ldisbri', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'صفحه نمایش', - priority: 0, - }, - { - _id: '63f22c65358e3427d44281ec', - fullNativeName: 'گستره رنگی صفحه نمایش لپتاپ', - nativeName: 'گستره رنگی', - englishName: 'ldiscol', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'صفحه نمایش', - priority: 0, - }, - { - _id: '63f22c71358e3427d4428200', - fullNativeName: 'نوع پنل صفحه نمایش لپتاپ', - nativeName: 'نوع پنل', - englishName: 'ldispan', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'صفحه نمایش', - priority: 0, - }, - { - _id: '644002e3be9b64da57f3f23b', - fullNativeName: 'رزولوشن صفحه نمایش لپتاپ', - nativeName: 'رزولوشن', - englishName: 'ldisres', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'صفحه نمایش', - priority: 0, - }, - { - _id: '64ce668d74a30bf0da051d90', - fullNativeName: 'نسبت تصویر صفحه نمایش لپتاپ', - nativeName: 'نسبت تصویر', - englishName: 'ldisasp', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'صفحه نمایش', - priority: 0, - }, - { - _id: '65d59ff54497f0662ddcad44', - fullNativeName: 'عمق رنگ(Color depth) صفحه نمایش لپتاپ', - nativeName: 'عمق رنگ(Color depth)', - englishName: 'ldisdep', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'صفحه نمایش', - priority: 0, - }, - { - _id: '63846ec40df5e6a52824d30a', - fullNativeName: 'شرکت سازنده پردازنده لپتاپ', - nativeName: 'شرکت سازنده', - englishName: 'lcpubrand', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'پردازنده', - priority: 0, - }, + // ────────────────────────────────────────────────────────────────────── + // زمین (Lot) features + // ────────────────────────────────────────────────────────────────────── + ...sharedRealEstateFields(lot), { - _id: '63846ec70df5e6a52824d313', - fullNativeName: 'سری پردازنده پردازنده لپتاپ', - nativeName: 'سری پردازنده', - englishName: 'lcpuser', - group: '63822b8fa964fe72ec7be6a0', + nativeName: 'متراژ', + englishName: 'area', + category: lot, + featureType: 'number', + filterType: 'range', + unit: 'متر مربع', isFilterable: true, - tag: 'پردازنده', - priority: 0, + tag: 'ابعاد', + priority: 100, }, { - _id: '63846eca0df5e6a52824d31c', - fullNativeName: 'مدل پردازنده پردازنده لپتاپ', - nativeName: 'مدل پردازنده', - englishName: 'lcpumodel', - group: '63822b8fa964fe72ec7be6a0', + nativeName: 'کاربری زمین', + englishName: 'landUsage', + category: lot, + featureType: 'string', + filterType: 'exact', + predefinedValues: ['مسکونی', 'تجاری', 'کشاورزی', 'صنعتی', 'ویلایی', 'باغی', 'آموزشی', 'اداری'], isFilterable: true, - tag: 'پردازنده', - priority: 0, + tag: 'کاربری', + priority: 95, }, { - _id: '63f22c57358e3427d44281d8', - fullNativeName: 'قدرت پردازنده پردازنده لپتاپ', - nativeName: 'قدرت پردازنده ', - englishName: 'lcpupow', - group: '63822b8fa964fe72ec7be6a0', + nativeName: 'نوع ملک', + englishName: 'propertyType', + category: lot, + featureType: 'string', + filterType: 'exact', + predefinedValues: ['زمین مسکونی', 'زمین تجاری', 'زمین کشاورزی', 'زمین صنعتی', 'باغ', 'مزرعه'], isFilterable: true, - tag: 'پردازنده', - priority: 0, + tag: 'مشخصات', + priority: 90, }, { - _id: '63868a7f042e7c1f4e5a88cc', - fullNativeName: 'شرکت سازنده کارت گرافیک لپتاپ', - nativeName: 'شرکت سازنده', - englishName: 'lgpubrand', - group: '63822b8fa964fe72ec7be6a0', + nativeName: 'موقعیت جغرافیایی', + englishName: 'geoPosition', + category: lot, + featureType: 'string', + filterType: 'exact', + predefinedValues: ['شمالی', 'جنوبی', 'شرقی', 'غربی', 'کنج'], isFilterable: true, - tag: 'کارت گرافیک', - priority: 0, + tag: 'موقعیت', + priority: 80, }, { - _id: '639325b39ba61a7bcaf25e87', - fullNativeName: 'مدل کارت گرافیک کارت گرافیک لپتاپ', - nativeName: 'مدل کارت گرافیک', - englishName: 'lgpumodel', - group: '63822b8fa964fe72ec7be6a0', + nativeName: 'عرض گذر', + englishName: 'streetWidth', + category: lot, + featureType: 'number', + filterType: 'range', + unit: 'متر', isFilterable: true, - tag: 'کارت گرافیک', - priority: 0, + tag: 'ابعاد', + priority: 75, }, { - _id: '63f22c8f358e3427d4428228', - fullNativeName: 'توان گرافیک کارت گرافیک لپتاپ', - nativeName: 'توان گرافیک', - englishName: 'lgpuw', - group: '63822b8fa964fe72ec7be6a0', + nativeName: 'تعداد بر', + englishName: 'facadeCount', + category: lot, + featureType: 'number', + filterType: 'range', isFilterable: true, - tag: 'کارت گرافیک', - priority: 0, + tag: 'ابعاد', + priority: 70, }, { - _id: '63f22ccc358e3427d4428264', - fullNativeName: 'مقدار حافظه گرافیک کارت گرافیک لپتاپ', - nativeName: 'مقدار حافظه گرافیک', - englishName: 'lgpuvram', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'کارت گرافیک', - priority: 0, + nativeName: 'طول زمین', + englishName: 'landLength', + category: lot, + featureType: 'number', + filterType: 'range', + unit: 'متر', + isFilterable: false, + tag: 'ابعاد', + priority: 65, }, { - _id: '63f22cb4358e3427d442824f', - fullNativeName: 'مقدار حافظه حافظه و رم لپتاپ', - nativeName: 'مقدار حافظه', - englishName: 'lssd', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'حافظه و رم', - priority: 0, + nativeName: 'عرض زمین', + englishName: 'landWidth', + category: lot, + featureType: 'number', + filterType: 'range', + unit: 'متر', + isFilterable: false, + tag: 'ابعاد', + priority: 64, }, { - _id: '63f22cdb358e3427d4428279', - fullNativeName: 'نوع حافظه حافظه و رم لپتاپ', - nativeName: 'نوع حافظه', - englishName: 'lssdtype', - group: '63822b8fa964fe72ec7be6a0', + nativeName: 'جهت ساختمان', + englishName: 'buildingDirection', + category: lot, + featureType: 'string', + filterType: 'exact', + predefinedValues: ['شمالی', 'جنوبی', 'شرقی', 'غربی'], isFilterable: true, - tag: 'حافظه و رم', - priority: 0, + tag: 'موقعیت', + priority: 60, }, { - _id: '63f22ce4358e3427d442828e', - fullNativeName: 'مقدار رم حافظه و رم لپتاپ', - nativeName: 'مقدار رم', - englishName: 'lramcap', - group: '63822b8fa964fe72ec7be6a0', + nativeName: 'امکانات زیرساختی', + englishName: 'utilities', + category: lot, + featureType: 'string', + filterType: 'multi', + predefinedValues: ['آب', 'برق', 'گاز', 'فاضلاب', 'تلفن', 'اینترنت'], isFilterable: true, - tag: 'حافظه و رم', - priority: 0, + tag: 'امکانات', + priority: 55, }, { - _id: '63f22ced358e3427d44282a3', - fullNativeName: 'نوع رم حافظه و رم لپتاپ', - nativeName: 'نوع رم', - englishName: 'lramtype', - group: '63822b8fa964fe72ec7be6a0', + nativeName: 'قیمت هر متر مربع', + englishName: 'pricePerMeter', + category: lot, + featureType: 'number', + filterType: 'range', + unit: 'تومان', isFilterable: true, - tag: 'حافظه و رم', - priority: 0, + tag: 'قیمت', + priority: 50, }, { - _id: '63f22cf5358e3427d44282b8', - fullNativeName: 'سرعت رم حافظه و رم لپتاپ', - nativeName: 'سرعت رم', - englishName: 'lramspeed', - group: '63822b8fa964fe72ec7be6a0', + nativeName: 'منطقه شهرداری', + englishName: 'municipalityZone', + category: lot, + featureType: 'string', + filterType: 'exact', isFilterable: true, - tag: 'حافظه و رم', - priority: 0, - }, - { - _id: '64ce6d0274a30bf0da052796', - fullNativeName: 'ارتقا پذیری رم حافظه و رم لپتاپ', - nativeName: 'ارتقا پذیری رم', - englishName: 'lramupgr', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'حافظه و رم', - priority: 0, - }, - { - _id: '64ce6d1774a30bf0da0527b3', - fullNativeName: 'ارتقا پذیری هارد حافظه و رم لپتاپ', - nativeName: 'ارتقا پذیری هارد', - englishName: 'lssdupgr', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'حافظه و رم', - priority: 0, - }, - { - _id: '63f22d3c358e3427d44282e1', - fullNativeName: 'توان باتری (Wh) باتری و انرژی لپتاپ', - nativeName: 'توان باتری (Wh)', - englishName: 'lbattwh', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'باتری و انرژی', - priority: 0, - }, - { - _id: '63f22d3e358e3427d44282f7', - fullNativeName: 'خروجی آداپتور (W) باتری و انرژی لپتاپ', - nativeName: 'خروجی آداپتور (W)', - englishName: 'ladapw', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'باتری و انرژی', - priority: 0, - }, - { - _id: '63f22d54358e3427d4428323', - fullNativeName: 'نوع باتری باتری و انرژی لپتاپ', - nativeName: 'نوع باتری', - englishName: 'lbattype', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'باتری و انرژی', - priority: 0, - }, - { - _id: '63f4b0e817976077a6f30a12', - fullNativeName: 'نام محصول مشخصات تولید لپتاپ', - nativeName: 'نام محصول', - englishName: 'lname', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'مشخصات تولید', - priority: 0, - }, - { - _id: '63f4b0f817976077a6f30a29', - fullNativeName: 'کد محصول مشخصات تولید لپتاپ', - nativeName: 'کد محصول', - englishName: 'lcode', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'مشخصات تولید', - priority: 0, + tag: 'موقعیت', + priority: 45, }, + // ────────────────────────────────────────────────────────────────────── + // ساختمان (Building) features + // ────────────────────────────────────────────────────────────────────── + ...sharedRealEstateFields(building), + + { + nativeName: 'متراژ', + englishName: 'area', + category: building, + featureType: 'number', + filterType: 'range', + unit: 'متر مربع', + isFilterable: true, + tag: 'ابعاد', + priority: 100, + }, + { + nativeName: 'نوع ملک', + englishName: 'propertyType', + category: building, + featureType: 'string', + filterType: 'exact', + predefinedValues: ['آپارتمان', 'ویلا', 'خانه مستقل', 'دوبلکس', 'پنت‌هاوس', 'مغازه', 'دفتر', 'سوله'], + isFilterable: true, + tag: 'مشخصات', + priority: 99, + }, + { + nativeName: 'تعداد اتاق', + englishName: 'roomCount', + category: building, + featureType: 'number', + filterType: 'range', + isFilterable: true, + tag: 'مشخصات', + priority: 95, + }, + { + nativeName: 'طبقه', + englishName: 'floor', + category: building, + featureType: 'number', + filterType: 'range', + isFilterable: true, + tag: 'مشخصات', + priority: 90, + }, + { + nativeName: 'تعداد طبقات کل', + englishName: 'totalFloors', + category: building, + featureType: 'number', + filterType: 'range', + isFilterable: true, + tag: 'مشخصات', + priority: 85, + }, + { + nativeName: 'سال ساخت', + englishName: 'buildYear', + category: building, + featureType: 'number', + filterType: 'range', + isFilterable: true, + tag: 'عمومی', + priority: 80, + }, + { + nativeName: 'پارکینگ', + englishName: 'parking', + category: building, + featureType: 'boolean', + filterType: 'bool', + isFilterable: true, + tag: 'امکانات', + priority: 75, + }, + { + nativeName: 'انباری', + englishName: 'storage', + category: building, + featureType: 'boolean', + filterType: 'bool', + isFilterable: true, + tag: 'امکانات', + priority: 74, + }, + { + nativeName: 'آسانسور', + englishName: 'elevator', + category: building, + featureType: 'boolean', + filterType: 'bool', + isFilterable: true, + tag: 'امکانات', + priority: 73, + }, + { + nativeName: 'بالکن', + englishName: 'balcony', + category: building, + featureType: 'boolean', + filterType: 'bool', + isFilterable: true, + tag: 'امکانات', + priority: 72, + }, + { + nativeName: 'وضعیت بنا', + englishName: 'houseCondition', + category: building, + featureType: 'string', + filterType: 'exact', + predefinedValues: ['نوساز', 'بازسازی شده', 'کلنگی', 'قدیمی'], + isFilterable: true, + tag: 'مشخصات', + priority: 70, + }, + { + nativeName: 'سیستم گرمایش', + englishName: 'heatingSystem', + category: building, + featureType: 'string', + filterType: 'exact', + predefinedValues: ['شوفاژ', 'پکیج', 'فن‌کوئل', 'گرمای کفی', 'بخاری'], + isFilterable: true, + tag: 'امکانات', + priority: 65, + }, + { + nativeName: 'سیستم سرمایش', + englishName: 'coolingSystem', + category: building, + featureType: 'string', + filterType: 'exact', + predefinedValues: ['کولر آبی', 'اسپیلت', 'فن‌کوئل', 'چیلر'], + isFilterable: true, + tag: 'امکانات', + priority: 64, + }, + { + nativeName: 'متریال کف', + englishName: 'floorMaterial', + category: building, + featureType: 'string', + filterType: 'exact', + predefinedValues: ['سرامیک', 'پارکت', 'موزاییک', 'سنگ', 'اپوکسی'], + isFilterable: false, + tag: 'مشخصات', + priority: 60, + }, + { + nativeName: 'موقعیت جغرافیایی', + englishName: 'geoPosition', + category: building, + featureType: 'string', + filterType: 'exact', + predefinedValues: ['شمالی', 'جنوبی', 'شرقی', 'غربی', 'کنج'], + isFilterable: true, + tag: 'موقعیت', + priority: 55, + }, + { + nativeName: 'منطقه شهرداری', + englishName: 'municipalityZone', + category: building, + featureType: 'string', + filterType: 'exact', + isFilterable: true, + tag: 'موقعیت', + priority: 50, + }, + + // ────────────────────────────────────────────────────────────────────── + // خودرو (Car) features + // ────────────────────────────────────────────────────────────────────── + { + nativeName: 'شماره VIN', + englishName: 'vinNumber', + category: car, + featureType: 'string', + filterType: 'exact', + isFilterable: false, + tag: 'شناسه', + priority: 100, + }, + { + nativeName: 'نوع پلاک', + englishName: 'plateType', + category: car, + featureType: 'string', + filterType: 'exact', + predefinedValues: [ + 'پلاک ملی', + 'پلاک منطقه آزاد اروند دائمی', + 'پلاک منطقه آزاد اروند موقت', + 'پلاک گذر موقت', + ], + isFilterable: true, + tag: 'پلاک', + priority: 95, + }, + { + nativeName: 'شماره پلاک', + englishName: 'plateNumber', + category: car, + featureType: 'string', + filterType: 'exact', + isFilterable: false, + tag: 'پلاک', + priority: 90, + }, { - _id: '64bbf4ec05f1a17c78c7d77f', - fullNativeName: 'برند مشخصات تولید لپتاپ', nativeName: 'برند', - englishName: 'lbrand', - group: '63822b8fa964fe72ec7be6a0', + englishName: 'brand', + category: car, + featureType: 'string', + filterType: 'exact', + predefinedValues: [ + 'ایران‌خودرو', + 'سایپا', + 'بهمن', + 'کیا', + 'هیوندا', + 'تویوتا', + 'بنز', + 'BMW', + 'آئودی', + 'پژو', + 'رنو', + 'فولکس واگن', + 'شورولت', + 'فورد', + 'لکسوس', + ], isFilterable: true, - tag: 'مشخصات تولید', - priority: 0, + tag: 'مشخصات', + priority: 85, }, { - _id: '64ce70a474a30bf0da0530e6', - fullNativeName: 'گارانتی کمپانی سازنده مشخصات تولید لپتاپ', - nativeName: 'گارانتی کمپانی سازنده', - englishName: 'lgur', - group: '63822b8fa964fe72ec7be6a0', + nativeName: 'مدل خودرو', + englishName: 'model', + category: car, + featureType: 'string', + filterType: 'exact', isFilterable: true, - tag: 'مشخصات تولید', - priority: 0, + tag: 'مشخصات', + priority: 80, }, { - _id: '64ce754b74a30bf0da05399c', - fullNativeName: 'لایسنس ویندوز اورجینال مشخصات تولید لپتاپ', - nativeName: 'لایسنس ویندوز اورجینال', - englishName: 'lwlic', - group: '63822b8fa964fe72ec7be6a0', + nativeName: 'سال تولید', + englishName: 'productionYear', + category: car, + featureType: 'number', + filterType: 'range', isFilterable: true, - tag: 'مشخصات تولید', - priority: 0, + tag: 'مشخصات', + priority: 75, }, { - _id: '6580df72f565d67d75dacdf2', - fullNativeName: 'سال (نسل) معرفی مدل مشخصات تولید لپتاپ', - nativeName: 'سال (نسل) معرفی مدل', - englishName: 'lgen', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'مشخصات تولید', - priority: 0, - }, - { - _id: '64ce6ba874a30bf0da0526a4', - fullNativeName: 'Wifi ارتباطات و شبکه لپتاپ', - nativeName: 'Wifi', - englishName: 'lwifi', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'ارتباطات و شبکه', - priority: 0, - }, - { - _id: '64ce6bb374a30bf0da0526c1', - fullNativeName: 'Bluetooth ارتباطات و شبکه لپتاپ', - nativeName: 'Bluetooth', - englishName: 'lblu', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'ارتباطات و شبکه', - priority: 0, - }, - { - _id: '64ce6fd374a30bf0da052fb5', - fullNativeName: 'وبکم ارتباطات و شبکه لپتاپ', - nativeName: 'وبکم', - englishName: 'lcam', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'ارتباطات و شبکه', - priority: 0, - }, - { - _id: '64ce6fe374a30bf0da052fe4', - fullNativeName: 'کیفیت وبکم ارتباطات و شبکه لپتاپ', - nativeName: 'کیفیت وبکم', - englishName: 'lcamqu', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'ارتباطات و شبکه', - priority: 0, - }, - { - _id: '64ce67c374a30bf0da051f8e', - fullNativeName: 'تاندربولت پورت ها لپتاپ', - nativeName: 'تاندربولت', - englishName: 'ltb', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'پورت ها', - priority: 0, - }, - { - _id: '64ce69bd74a30bf0da05229b', - fullNativeName: 'Type C پورت ها لپتاپ', - nativeName: 'Type C', - englishName: 'ltypc', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'پورت ها', - priority: 0, - }, - { - _id: '64ce69d674a30bf0da052305', - fullNativeName: 'PD پورت ها لپتاپ', - nativeName: 'PD', - englishName: 'lpd', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'پورت ها', - priority: 0, - }, - { - _id: '64ce6a5174a30bf0da052400', - fullNativeName: 'HDMI 2.0 پورت ها لپتاپ', - nativeName: 'HDMI 2.0', - englishName: 'lhdm2', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'پورت ها', - priority: 0, - }, - { - _id: '64ce6a9774a30bf0da0524a5', - fullNativeName: 'HDMI 2.1 پورت ها لپتاپ', - nativeName: 'HDMI 2.1', - englishName: 'lhdm21', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'پورت ها', - priority: 0, - }, - { - _id: '64ce6b5374a30bf0da05258e', - fullNativeName: 'USB 2.0 پورت ها لپتاپ', - nativeName: 'USB 2.0', - englishName: 'lusb2', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'پورت ها', - priority: 0, - }, - { - _id: '64ce6b5c74a30bf0da0525ab', - fullNativeName: 'USB 3.0 پورت ها لپتاپ', - nativeName: 'USB 3.0', - englishName: 'lusb3', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'پورت ها', - priority: 0, - }, - { - _id: '64ce721674a30bf0da053390', - fullNativeName: 'USB 3.2 پورت ها لپتاپ', - nativeName: 'USB 3.2', - englishName: 'lusb32', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'پورت ها', - priority: 0, - }, - { - _id: '64ce721674a30bf0da053392', - fullNativeName: 'RJ45 پورت ها لپتاپ', - nativeName: 'RJ45', - englishName: 'lrj45', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'پورت ها', - priority: 0, - }, - { - _id: '65a0071b0c1fc5615d7a8237', - fullNativeName: 'Jack 3.5 پورت ها لپتاپ', - nativeName: 'Jack 3.5', - englishName: 'ljack', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'پورت ها', - priority: 0, - }, - { - _id: '65a0076a0c1fc5615d7a82f2', - fullNativeName: 'Sd Card Reader پورت ها لپتاپ', - nativeName: 'Sd Card Reader', - englishName: 'lsdreader', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'پورت ها', - priority: 0, - }, - - { - _id: '65a52fb77b78648044177ec2', - fullNativeName: 'USB-C 4 پورت ها لپتاپ', - nativeName: 'USB-C 4', - englishName: 'lusbc4', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'پورت ها', - priority: 0, - }, - { - _id: '668a564f55b0c17771f0eeb9', - fullNativeName: 'Mini DisplayPort پورت ها لپتاپ', - nativeName: 'Mini DisplayPort', - englishName: 'lminidp', - group: '63822b8fa964fe72ec7be6a0', - isFilterable: true, - tag: 'پورت ها', - priority: 0, - }, - { - _id: '63d578a58678085f9874e016', - fullNativeName: 'وزن مشخصات فیزیکی کوله پشتی', - nativeName: 'وزن', - englishName: 'bpweight', - group: '63d3e9119a1819889d6d00b9', - isFilterable: true, - tag: 'مشخصات فیزیکی', - priority: 0, - }, - { - _id: '63f4c48e17976077a6f3113b', - fullNativeName: 'ابعاد مشخصات فیزیکی کوله پشتی', - nativeName: 'ابعاد', - englishName: 'bpdim', - group: '63d3e9119a1819889d6d00b9', - isFilterable: true, - tag: 'مشخصات فیزیکی', - priority: 0, - }, - { - _id: '63f4c49517976077a6f3114c', - fullNativeName: 'جنس مشخصات فیزیکی کوله پشتی', - nativeName: 'جنس', - englishName: 'bpmat', - group: '63d3e9119a1819889d6d00b9', - isFilterable: true, - tag: 'مشخصات فیزیکی', - priority: 0, - }, - { - _id: '63f4c53b17976077a6f311c7', - fullNativeName: 'مناسب تا سایز مشخصات فیزیکی کوله پشتی', - nativeName: 'مناسب تا سایز', - englishName: 'bpsize', - group: '63d3e9119a1819889d6d00b9', - isFilterable: true, - tag: 'مشخصات فیزیکی', - priority: 0, - }, - { - _id: '63f4c55117976077a6f311d9', - fullNativeName: 'رنگ مشخصات فیزیکی کوله پشتی', nativeName: 'رنگ', - englishName: 'bpcolor', - group: '63d3e9119a1819889d6d00b9', + englishName: 'color', + category: car, + featureType: 'string', + filterType: 'exact', + predefinedValues: ['سفید', 'مشکی', 'نقره‌ای', 'خاکستری', 'قرمز', 'آبی', 'سبز', 'طلایی', 'کرم'], isFilterable: true, - tag: 'مشخصات فیزیکی', - priority: 0, + tag: 'مشخصات', + priority: 70, }, { - _id: '63f4c4ed17976077a6f3116d', - fullNativeName: 'ضد آب قابلیت ها کوله پشتی', - nativeName: 'ضد آب', - englishName: 'bpwatpf', - group: '63d3e9119a1819889d6d00b9', + nativeName: 'کارکرد (کیلومتر)', + englishName: 'mileage', + category: car, + featureType: 'number', + filterType: 'range', + unit: 'کیلومتر', isFilterable: true, - tag: 'قابلیت ها', - priority: 0, + tag: 'وضعیت', + priority: 65, }, { - _id: '63f4c4f417976077a6f3117f', - fullNativeName: 'ضد رطوبت قابلیت ها کوله پشتی', - nativeName: 'ضد رطوبت', - englishName: 'bpmoprf', - group: '63d3e9119a1819889d6d00b9', + nativeName: 'نوع سوخت', + englishName: 'fuelType', + category: car, + featureType: 'string', + filterType: 'exact', + predefinedValues: ['بنزین', 'گازوئیل', 'برقی', 'هیبریدی', 'CNG', 'دوگانه‌سوز'], isFilterable: true, - tag: 'قابلیت ها', - priority: 0, + tag: 'موتور', + priority: 60, }, { - _id: '63f4c4fe17976077a6f31191', - fullNativeName: 'ضد ضربه قابلیت ها کوله پشتی', - nativeName: 'ضد ضربه', - englishName: 'bpdmgprf', - group: '63d3e9119a1819889d6d00b9', + nativeName: 'نوع گیربکس', + englishName: 'transmission', + category: car, + featureType: 'string', + filterType: 'exact', + predefinedValues: ['دستی', 'اتوماتیک', 'نیمه‌اتوماتیک', 'CVT'], isFilterable: true, - tag: 'قابلیت ها', - priority: 0, + tag: 'موتور', + priority: 55, }, { - _id: '63f4c51217976077a6f311a3', - fullNativeName: 'پورت USB قابلیت ها کوله پشتی', - nativeName: 'پورت USB', - englishName: 'bpusb', - group: '63d3e9119a1819889d6d00b9', + nativeName: 'ظرفیت موتور', + englishName: 'engineCapacity', + category: car, + featureType: 'number', + filterType: 'range', + unit: 'سی‌سی', isFilterable: true, - tag: 'قابلیت ها', - priority: 0, + tag: 'موتور', + priority: 50, }, { - _id: '63f4c52c17976077a6f311b5', - fullNativeName: 'محفظه لپتاپ قابلیت ها کوله پشتی', - nativeName: 'محفظه لپتاپ', - englishName: 'bplap', - group: '63d3e9119a1819889d6d00b9', + nativeName: 'تعداد سیلندر', + englishName: 'cylinderCount', + category: car, + featureType: 'number', + filterType: 'exact', + predefinedValues: ['3', '4', '6', '8'], isFilterable: true, - tag: 'قابلیت ها', - priority: 0, + tag: 'موتور', + priority: 45, }, { - _id: '64bbf55f05f1a17c78c7d8d6', - fullNativeName: 'نام اختصاصی کد محصول کوله پشتی', - nativeName: 'نام اختصاصی', - englishName: 'bpspname', - group: '63d3e9119a1819889d6d00b9', + nativeName: 'وضعیت بدنه', + englishName: 'bodyCondition', + category: car, + featureType: 'string', + filterType: 'exact', + predefinedValues: ['صفر', 'بدون خش', 'خط و خش جزئی', 'رنگ شده', 'تصادفی'], isFilterable: true, - tag: 'کد محصول', - priority: 0, + tag: 'وضعیت', + priority: 40, }, { - _id: '64bbf56405f1a17c78c7d8ed', - fullNativeName: 'برند کد محصول کوله پشتی', - nativeName: 'برند', - englishName: 'bpbrand', - group: '63d3e9119a1819889d6d00b9', + nativeName: 'گواهی معاینه فنی', + englishName: 'technicalInspection', + category: car, + featureType: 'boolean', + filterType: 'bool', isFilterable: true, - tag: 'کد محصول', - priority: 0, + tag: 'وضعیت', + priority: 35, + }, + { + nativeName: 'بیمه بدنه', + englishName: 'bodyInsurance', + category: car, + featureType: 'boolean', + filterType: 'bool', + isFilterable: true, + tag: 'وضعیت', + priority: 30, + }, + { + nativeName: 'نوع بدنه', + englishName: 'bodyType', + category: car, + featureType: 'string', + filterType: 'exact', + predefinedValues: ['سدان', 'هاچبک', 'شاسی‌بلند', 'وانت', 'کوپه', 'مینی‌ون', 'کراس‌اوور'], + isFilterable: true, + tag: 'مشخصات', + priority: 25, }, ] await Feature.deleteMany({}) console.log(chalk.red('Deleted all feature records.')) - await Feature.insertMany(features, null) + await Feature.insertMany(features) console.log(chalk.green.bold(`FEATURES: Created ${features.length} feature(s)`)) } diff --git a/components/belonging/belongingModel.js b/components/property/belongingModel.js similarity index 95% rename from components/belonging/belongingModel.js rename to components/property/belongingModel.js index 85e2341..f8fd5d6 100644 --- a/components/belonging/belongingModel.js +++ b/components/property/belongingModel.js @@ -38,6 +38,10 @@ const belongingSchema = new mongoose.Schema( mainImageUrls: [String], nationalGeoCode: String, tags: [String], + status: { + type: ObjectID, + ref: 'BelongingStatus', + }, isActive: { type: Boolean, default: true, diff --git a/components/property/buildingModel.js b/components/property/buildingModel.js new file mode 100644 index 0000000..c67ebc3 --- /dev/null +++ b/components/property/buildingModel.js @@ -0,0 +1,42 @@ +const mongoose = require('mongoose') +const Property = require('./propertyModel') + +const buildingSchema = new mongoose.Schema({ + builtArea: { + type: Number, // مساحت زیربنا به متر مربع + }, + floorsCount: { + type: Number, // تعداد طبقات + }, + unitsCount: { + type: Number, // تعداد واحدها + }, + constructionYear: { + type: Number, // سال ساخت + }, + elevator: { + type: Boolean, // آسانسور + }, + parking: { + type: Boolean, // پارکینگ + }, + warehouse: { + type: Boolean, // انبار + }, + heatingSystem: { + type: String, // سیستم گرمایش + }, + coolingSystem: { + type: String, // سیستم سرمایش + }, + structureType: { + type: String, // نوع سازه (اسکلت فلزی، بتنی، ...) + }, + flooringType: { + type: String, // جنس کف (سرامیک، سنگ، لمینت، ...) + }, +}) + +const Building = Property.discriminator('Building', buildingSchema) + +module.exports = Building diff --git a/components/property/events/createProperty.js b/components/property/events/createProperty.js new file mode 100644 index 0000000..56cbb2d --- /dev/null +++ b/components/property/events/createProperty.js @@ -0,0 +1,6 @@ +const propertyService = require('../propertyService') +module.exports = (eventEmitter) => { + eventEmitter.on('createProperty', async (propertyData) => { + await propertyService.indexOneProperty(propertyData._id) + }) +} diff --git a/components/property/events/deleteProperty.js b/components/property/events/deleteProperty.js new file mode 100644 index 0000000..e4eb056 --- /dev/null +++ b/components/property/events/deleteProperty.js @@ -0,0 +1,6 @@ +const propertyService = require('../propertyService') +module.exports = (eventEmitter) => { + eventEmitter.on('deleteProperty', async (propertyData) => { + // Handle property deletion event if needed + }) +} diff --git a/components/property/landModel.js b/components/property/landModel.js new file mode 100644 index 0000000..bcf7db4 --- /dev/null +++ b/components/property/landModel.js @@ -0,0 +1,30 @@ +const mongoose = require('mongoose') +const Property = require('./propertyModel') + +const landSchema = new mongoose.Schema({ + landArea: { + type: Number, // مساحت زمین به متر مربع + }, + zoningType: { + type: String, // نوع کاربری (مسکونی، کشاورزی، تجاری و ...) + }, + boundaryCoordinates: { + type: [[Number]], // مختصات چندضلعی حدود ملک + }, + streetWidth: { + type: Number, // عرض گذر (متر) + }, + facadeCount: { + type: Number, // تعداد بر + }, + landLength: { + type: Number, // طول زمین (متر) + }, + landWidth: { + type: Number, // عرض زمین (متر) + }, +}) + +const Land = Property.discriminator('Land', landSchema) + +module.exports = Land diff --git a/components/property/propertyController.js b/components/property/propertyController.js new file mode 100644 index 0000000..875b065 --- /dev/null +++ b/components/property/propertyController.js @@ -0,0 +1,81 @@ +const catchAsync = require('../../globalErrorHandler/catchAsync') +const propertyService = require('./propertyService') +const { eventEmitter } = require('../../events/eventEmitter') + +exports.searchProperties = catchAsync(async (req, res, next) => { + const { facets, belongings, totalCount, pagesCount } = await propertyService.searchProperties(req.query) + + res.status(200).json({ + status: 'success', + facets, + properties: belongings, + totalCount, + pagesCount, + }) +}) + +exports.indexAllProperties = catchAsync(async (req, res, next) => { + const { indexedCount } = await propertyService.indexAllProperties() + + res.status(200).json({ + status: 'success', + indexedCount, + }) +}) + +exports.indexOneProperty = catchAsync(async (req, res, next) => { + await propertyService.indexOneProperty(req.params.propertyId) + + res.status(200).json({ + status: 'success', + }) +}) + +exports.getAllProperties = catchAsync(async (req, res, next) => { + const properties = await propertyService.getAllProperties() + + res.status(200).json({ + status: 'success', + results: properties.length, + data: properties, + }) +}) + +exports.createProperty = catchAsync(async (req, res, next) => { + const property = await propertyService.createProperty(req.body) + eventEmitter.emit('createProperty', property) + + res.status(201).json({ + status: 'success', + data: property, + }) +}) + +exports.getProperty = catchAsync(async (req, res, next) => { + const property = await propertyService.getProperty(req.params.propertyId) + + res.status(200).json({ + status: 'success', + data: property, + }) +}) + +exports.updateProperty = catchAsync(async (req, res, next) => { + const property = await propertyService.updateProperty(req.params.propertyId, req.body) + await propertyService.indexAllProperties() + + res.status(200).json({ + status: 'success', + data: property, + }) +}) + +exports.deleteProperty = catchAsync(async (req, res, next) => { + await propertyService.deleteProperty(req.params.propertyId) + // eventEmitter.emit('deleteProperty') + + res.status(204).json({ + status: 'success', + data: null, + }) +}) diff --git a/components/property/propertyModel.js b/components/property/propertyModel.js new file mode 100644 index 0000000..72b59cb --- /dev/null +++ b/components/property/propertyModel.js @@ -0,0 +1,130 @@ +const mongoose = require('mongoose') +const ObjectID = mongoose.Schema.Types.ObjectId +const AutoIncrement = require('mongoose-sequence')(mongoose) +const { getMultipleSignedUrls } = require('../file/fileService') + +const propertySchemaOptions = { + discriminatorKey: 'propertyType', + toJSON: { virtuals: true }, + toObject: { virtuals: true }, + timestamps: true, +} + +const propertySchema = new mongoose.Schema( + { + name: { + type: String, + required: false, // Auto-generated: ownerName + mainNumber/subNumber + }, + category: { + type: ObjectID, + ref: 'Category', + }, + description: String, + relatedProjects: [String], + location: { + type: { + type: String, + enum: ['Point'], + required: false, + }, + coordinates: { + type: [Number], // [longitude, latitude] + required: false, + }, + }, + documentUrls: { + type: [String], + default: [], + }, + mainImageUrls: [String], + nationalGeoCode: { + type: String, + required: true, // شناسه ملی جغرافیایی الزامی است + }, + tags: [String], + status: { + type: ObjectID, + ref: 'PropertyStatus', + }, + isActive: { + type: Boolean, + default: true, + select: false, + }, + // Title Deed General & Administrative (شناسه و اداره ثبت) + uniqueId: String, // شناسه یکتا سند مالکیت (۱۸ رقمی) + serialNumber: String, // شماره سریال چاپی سند + provinceRegistry: String, + localRegistryOffice: String, + registrationDate: String, // تاریخ ثبت سند + + // Owner Specifications (مشخصات مالک) + ownerName: String, + fatherName: String, + idCardNumber: String, + nationalId: String, + issuePlace: String, + birthDate: String, + nationality: { + type: String, + default: 'ایران', + }, + ownershipDocs: String, + + // Property Specifications (مشخصات ملک) + registrationPlate: String, + mainNumber: String, + subNumber: String, + separatedFrom: String, + plotNumber: String, + registrationSection: String, + district: String, + blockNumber: String, + sideDirection: String, + floor: String, + zoningType: String, + specialStatus: String, + documentType: String, + address: String, + postalCode: String, + propertyBoundaries: String, + restrictions: String, + + // دانگ: سهم ملک از ۶ دانگ (۱ تا ۶) + dong: { + type: Number, + min: 1, + max: 6, + default: 6, + }, + + features: [ + { + definition: { + type: ObjectID, + ref: 'Feature', + required: true, + }, + value: mongoose.Schema.Types.Mixed, + }, + ], + }, + propertySchemaOptions +) + +propertySchema.plugin(AutoIncrement, { inc_field: 'propertyCode' }) + +propertySchema.methods.getDocumentsSignedUrls = function () { + return getMultipleSignedUrls(this.documentUrls, 'property-documents') +} + +propertySchema.methods.getMainImagedSignedUrl = function () { + return getMultipleSignedUrls(this.mainImageUrls, 'property-images') +} + +propertySchema.index({ location: '2dsphere' }) + +const Property = mongoose.model('Property', propertySchema) + +module.exports = Property diff --git a/components/property/propertyRoutes.js b/components/property/propertyRoutes.js new file mode 100644 index 0000000..7cfbd53 --- /dev/null +++ b/components/property/propertyRoutes.js @@ -0,0 +1,26 @@ +const express = require('express') +const propertyController = require('./propertyController') +const perm = require('../../middlewares/permissionMiddleware') +const loginCheck = require('../../middlewares/loginCheck') + +const router = new express.Router() + +router.use(loginCheck.loginProtect) + +router.get('/admin/get-all', perm.requires('getallproperties', 'getallbelongings'), propertyController.getAllProperties) + +router.post('/admin/create', perm.requires('createaproperty', 'createabelonging'), propertyController.createProperty) + +router.post('/admin/index-all', perm.requires('indexproperties', 'indexbelongings'), propertyController.indexAllProperties) + +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/get-one/:propertyId', perm.requires('getaproperty', 'getabelonging'), propertyController.getProperty) + +router.put('/admin/update/:propertyId', perm.requires('updateaproperty', 'updateabelonging'), propertyController.updateProperty) + +router.delete('/admin/delete/:propertyId', perm.requires('deleteaproperty', 'deleteabelonging'), propertyController.deleteProperty) + +module.exports = router diff --git a/components/property/propertyService.js b/components/property/propertyService.js new file mode 100644 index 0000000..ca0bcf0 --- /dev/null +++ b/components/property/propertyService.js @@ -0,0 +1,202 @@ +const Property = require('./propertyModel') +const Land = require('./landModel') +const Building = require('./buildingModel') +const AppError = require('../../globalErrorHandler/AppError') +const { client } = require('../../setup/typesense') +const TsApiFeatures = require('../../utils/TsApiFeatures') + +const getAllProperties = async () => { + return Property.find({}, null, null).populate('category').populate('status') +} + +const createProperty = async (data) => { + if (data.propertyType === 'Land' || data.propertyType === 'زمین') { + return Land.create(data) + } else if (data.propertyType === 'Building' || data.propertyType === 'ساختمان') { + return Building.create(data) + } + return Property.create(data) +} + +const getProperty = async (propertyId) => { + let property = await Property.findById(propertyId, null, null).populate('category').populate('status') + + if (!property) { + throw new AppError('NoPropertyFound', 404, {}) + } + + const documentUrls = await property.getDocumentsSignedUrls() + const mainImageUrls = await property.getMainImagedSignedUrl() + + property = JSON.parse(JSON.stringify(property)) + + property.signedDocumentUrls = documentUrls + property.signedMainImageUrls = mainImageUrls + + return property +} + +const updateProperty = async (propertyId, data) => { + const property = await Property.findOneAndUpdate({ _id: propertyId }, data, { new: true }) + + if (!property) { + throw new AppError('NoPropertyFound', 404, {}) + } + + return property +} + +const deleteProperty = async (propertyId) => { + const property = await Property.findByIdAndUpdate(propertyId, { isActive: false }, { new: true }) + + if (!property) { + throw new AppError('NoPropertyFound', 404, {}) + } + + return null +} + +const ensureCollectionExists = async (forceRecreate = false) => { + try { + const collections = await client.collections().retrieve() + const collectionExists = collections.some((collection) => collection.name === 'properties') + + if (collectionExists && forceRecreate) { + await client.collections('properties').delete() + } + + if (!collectionExists || forceRecreate) { + const schema = getTypeSenseSchema() + await client.collections().create(schema) + console.log('Collection "properties" created successfully') + } + } catch (err) { + console.error('Error ensuring properties collection exists:', err) + } +} + +const searchProperties = async (inputQuery) => { + await ensureCollectionExists(false) + inputQuery = updateInputQuery(inputQuery, {}) + + const tsFeatures = new TsApiFeatures(client, 'properties', inputQuery).filter().facet().sort().limitFields().paginate().search() + + let { facets, items, totalCount, pagesCount } = await tsFeatures.execute() + + return { facets, belongings: items, totalCount, pagesCount } +} + +const updateInputQuery = (inputQuery, extraQueries) => { + for (const [key, value] of Object.entries(extraQueries)) { + inputQuery[key] = value + } + + inputQuery.query_by = 'name,nationalGeoCode,tags,description,_id' + + return inputQuery +} + +const indexAllProperties = async () => { + const batchSize = 20 + const count = await Property.countDocuments({ isActive: true }, null) + const rounds = Math.max(1, Math.ceil(count / batchSize)) + + await ensureCollectionExists(true) + + for (let i = 1; i <= rounds; i++) { + const properties = await Property.find({ isActive: true }, null, null) + .populate('category') + .skip((i - 1) * batchSize) + .limit(batchSize) + + if (properties.length === 0) break + + let documents = [] + + for (const property of properties) { + const combinedTags = Array.isArray(property.tags) ? property.tags.join(' ') : (property.tags || '') + + const data = { + _id: property._id.toString(), + isActive: property.isActive, + name: property.name || '', + category: property.category?.englishName || '', + nativeCategory: property.category?.nativeName || '', + imageUrl: property.mainImageUrls?.[0] || 'https://cloud.mbmanager.ir/images/item-no-image.png', + documents: property.documentUrls || [], + description: property.description || '', + nationalGeoCode: property.nationalGeoCode || '', + tags: combinedTags, + coordinates: property.location?.coordinates || [0, 0], + createdAt: Math.floor(new Date(property.createdAt || Date.now()).getTime() / 1000), + id: property.propertyCode ? property.propertyCode.toString() : property._id.toString(), + } + + documents.push(data) + } + + if (documents.length > 0) { + await client.collections('properties').documents().import(documents, { action: 'emplace' }) + } + } + + return { totalCount: count } +} + +const indexOneProperty = async (propertyId) => { + await ensureCollectionExists(false) + const property = await getProperty(propertyId) + + console.log(`Property ${property._id} indexing`) + + const combinedTags = Array.isArray(property.tags) ? property.tags.join(' ') : (property.tags || '') + + const data = { + _id: property._id.toString(), + isActive: property.isActive, + name: property.name || '', + category: property.category?.englishName || '', + nativeCategory: property.category?.nativeName || '', + imageUrl: property.mainImageUrls?.[0] || '', + documents: property.documentUrls || [], + description: property.description || '', + nationalGeoCode: property.nationalGeoCode || '', + tags: combinedTags, + coordinates: property.location?.coordinates || [0, 0], + createdAt: Math.floor(new Date(property.createdAt || Date.now()).getTime() / 1000), + id: property.propertyCode ? property.propertyCode.toString() : property._id.toString(), + } + + await client.collections('properties').documents().upsert(data) +} + +const getTypeSenseSchema = () => { + return { + name: 'properties', + enable_nested_fields: true, + fields: [ + { name: '_id', type: 'string', facet: false, infix: false }, + { name: 'name', type: 'string', facet: false, infix: true }, + { name: 'category', type: 'string', facet: true, infix: true }, + { name: 'nativeCategory', type: 'string', facet: false, infix: true }, + { name: 'description', type: 'string', facet: false }, + { name: 'nationalGeoCode', type: 'string', facet: false, infix: true }, + { name: 'tags', type: 'string', facet: false, infix: true }, + { name: 'coordinates', type: 'geopoint' }, + { name: 'createdAt', type: 'int64', facet: false }, + { name: 'imageUrl', type: 'string', facet: false, infix: true }, + ], + default_sorting_field: 'createdAt', + } +} + +module.exports = { + updateProperty, + indexAllProperties, + getProperty, + getAllProperties, + createProperty, + deleteProperty, + searchProperties, + indexOneProperty, +} diff --git a/components/propertyStatus/propertyStatusController.js b/components/propertyStatus/propertyStatusController.js new file mode 100644 index 0000000..40c6b04 --- /dev/null +++ b/components/propertyStatus/propertyStatusController.js @@ -0,0 +1,48 @@ +const catchAsync = require('../../globalErrorHandler/catchAsync') +const propertyStatusService = require('./propertyStatusService') + +exports.getAllStatuses = catchAsync(async (req, res, next) => { + const statuses = await propertyStatusService.getAllStatuses() + + res.status(200).json({ + status: 'success', + results: statuses.length, + data: statuses, + }) +}) + +exports.getStatus = catchAsync(async (req, res, next) => { + const status = await propertyStatusService.getStatus(req.params.statusId) + + res.status(200).json({ + status: 'success', + data: status, + }) +}) + +exports.createStatus = catchAsync(async (req, res, next) => { + const status = await propertyStatusService.createStatus(req.body) + + res.status(201).json({ + status: 'success', + data: status, + }) +}) + +exports.updateStatus = catchAsync(async (req, res, next) => { + const status = await propertyStatusService.updateStatus(req.params.statusId, req.body) + + res.status(200).json({ + status: 'success', + data: status, + }) +}) + +exports.deleteStatus = catchAsync(async (req, res, next) => { + await propertyStatusService.deleteStatus(req.params.statusId) + + res.status(204).json({ + status: 'success', + data: null, + }) +}) diff --git a/components/propertyStatus/propertyStatusModel.js b/components/propertyStatus/propertyStatusModel.js new file mode 100644 index 0000000..6776eb9 --- /dev/null +++ b/components/propertyStatus/propertyStatusModel.js @@ -0,0 +1,28 @@ +const mongoose = require('mongoose') + +const propertyStatusSchema = new mongoose.Schema( + { + nativeName: { + type: String, + required: true, + }, + englishName: { + type: String, + required: true, + unique: true, + }, + // Color token for UI badge (success/info/warn/danger/secondary/contrast) + color: { + type: String, + default: 'secondary', + }, + }, + { + toJSON: { virtuals: true }, + toObject: { virtuals: true }, + timestamps: true, + } +) + +const PropertyStatus = mongoose.model('PropertyStatus', propertyStatusSchema) +module.exports = PropertyStatus diff --git a/components/propertyStatus/propertyStatusRoutes.js b/components/propertyStatus/propertyStatusRoutes.js new file mode 100644 index 0000000..1eef7d8 --- /dev/null +++ b/components/propertyStatus/propertyStatusRoutes.js @@ -0,0 +1,20 @@ +const express = require('express') +const propertyStatusController = require('./propertyStatusController') +const perm = require('../../middlewares/permissionMiddleware') +const loginCheck = require('../../middlewares/loginCheck') + +const router = new express.Router() + +router.use(loginCheck.loginProtect) + +router.get('/admin/get-all', perm.requires('getallpropertystatuses', 'getallbelongingstatuses'), propertyStatusController.getAllStatuses) + +router.get('/admin/get-one/:statusId', perm.requires('getapropertystatus', 'getabelongingstatus'), propertyStatusController.getStatus) + +router.post('/admin/create', perm.requires('createapropertystatus', 'createabelongingstatus'), propertyStatusController.createStatus) + +router.put('/admin/update/:statusId', perm.requires('updateapropertystatus', 'updateabelongingstatus'), propertyStatusController.updateStatus) + +router.delete('/admin/delete/:statusId', perm.requires('deleteapropertystatus', 'deleteabelongingstatus'), propertyStatusController.deleteStatus) + +module.exports = router diff --git a/components/propertyStatus/propertyStatusSeeder.js b/components/propertyStatus/propertyStatusSeeder.js new file mode 100644 index 0000000..497ff8e --- /dev/null +++ b/components/propertyStatus/propertyStatusSeeder.js @@ -0,0 +1,32 @@ +const PropertyStatus = require('./propertyStatusModel') +const chalk = require('chalk') + +const runSeeder = async () => { + const statuses = [ + // ── اصلی ──────────────────────────────────────────────────────────── + { nativeName: 'موجود', englishName: 'available', color: 'success' }, + { nativeName: 'فروخته شده', englishName: 'sold', color: 'contrast' }, + { nativeName: 'قولنامه', englishName: 'reserved', color: 'warn' }, + { nativeName: 'بلاک', englishName: 'blocked', color: 'danger' }, + + // ── تکمیلی ────────────────────────────────────────────────────────── + { nativeName: 'در حال بررسی', englishName: 'in_review', color: 'info' }, + { nativeName: 'رهن', englishName: 'mortgaged', color: 'warn' }, + { nativeName: 'وراثتی', englishName: 'inherited', color: 'secondary' }, + { nativeName: 'مشاع', englishName: 'joint', color: 'secondary' }, + { nativeName: 'توقیف', englishName: 'seized', color: 'danger' }, + { nativeName: 'در دست ساخت', englishName: 'under_construction', color: 'info' }, + ] + + await PropertyStatus.deleteMany({}) + console.log(chalk.red('Deleted all property status records.')) + + await PropertyStatus.create(statuses) + console.log( + chalk.green.bold( + `PROPERTY STATUS: Created ${statuses.length} status(es)` + ) + ) +} + +module.exports = { runSeeder } diff --git a/components/propertyStatus/propertyStatusService.js b/components/propertyStatus/propertyStatusService.js new file mode 100644 index 0000000..bf8fdd5 --- /dev/null +++ b/components/propertyStatus/propertyStatusService.js @@ -0,0 +1,42 @@ +const PropertyStatus = require('./propertyStatusModel') +const AppError = require('../../globalErrorHandler/AppError') + +const getAllStatuses = async () => { + return PropertyStatus.find({}) +} + +const getStatus = async (statusId) => { + const status = await PropertyStatus.findById(statusId) + + if (!status) { + throw new AppError('NoPropertyStatusFound', 404, {}) + } + + return status +} + +const createStatus = async (data) => { + return PropertyStatus.create(data) +} + +const updateStatus = async (statusId, data) => { + const status = await PropertyStatus.findByIdAndUpdate(statusId, data, { new: true, runValidators: true }) + + if (!status) { + throw new AppError('NoPropertyStatusFound', 404, {}) + } + + return status +} + +const deleteStatus = async (statusId) => { + const status = await PropertyStatus.findByIdAndDelete(statusId) + + if (!status) { + throw new AppError('NoPropertyStatusFound', 404, {}) + } + + return null +} + +module.exports = { getAllStatuses, getStatus, createStatus, updateStatus, deleteStatus } diff --git a/components/transaction/transactionModel.js b/components/transaction/transactionModel.js index 33a0f7f..a3c0203 100644 --- a/components/transaction/transactionModel.js +++ b/components/transaction/transactionModel.js @@ -34,10 +34,9 @@ const transactionSchema = new mongoose.Schema( type: String, required: true, }, - belongingId: { + propertyId: { type: ObjectID, - ref: 'Belonging', - required: true, + ref: 'Property', }, }, { @@ -47,14 +46,11 @@ const transactionSchema = new mongoose.Schema( } ) -// Add individual indexes for frequently queried fields transactionSchema.index({ type: 1 }) transactionSchema.index({ status: 1 }) transactionSchema.index({ value: 1 }) - -// Add compound indexes for common query patterns in aggregation -transactionSchema.index({ belongingId: 1, status: 1 }) -transactionSchema.index({ belongingId: 1, type: 1, status: 1 }) +transactionSchema.index({ propertyId: 1, status: 1 }) +transactionSchema.index({ propertyId: 1, type: 1, status: 1 }) const Transaction = mongoose.model('Transaction', transactionSchema) module.exports = Transaction diff --git a/components/transaction/transactionService.js b/components/transaction/transactionService.js index 8db3d45..8ec625a 100644 --- a/components/transaction/transactionService.js +++ b/components/transaction/transactionService.js @@ -1,5 +1,5 @@ const Transaction = require('./transactionModel') -const Belonging = require('../belonging/belongingModel') +const Property = require('../property/propertyModel') const AppError = require('../../globalErrorHandler/AppError') const mongoose = require('mongoose') const ObjectID = mongoose.Schema.Types.ObjectId @@ -17,17 +17,16 @@ const filterObj = (obj, ...allowedFields) => { const createTransaction = async (data) => { if (data.type == 'withdraw') { - // Aggregate transactions to calculate inventory const result = await Transaction.aggregate([ { $match: { - belongingId: ObjectID, + propertyId: ObjectID, status: 'finished', }, }, { $group: { - _id: '$belongingId', + _id: '$propertyId', totalInventory: { $sum: '$value', }, @@ -35,7 +34,7 @@ const createTransaction = async (data) => { }, ]) - if (result[0].totalInventory <= 0) throw new AppError('Invalid Transaction', 404, {}) + if (result[0]?.totalInventory <= 0) throw new AppError('Invalid Transaction', 404, {}) } return Transaction.create({ @@ -46,7 +45,7 @@ const createTransaction = async (data) => { date: data.Date, description: data.description, userId: data.userId, - belongingId: data.belongingId, + propertyId: data.propertyId || data.belongingId, userName: data.userName, }) } @@ -55,28 +54,26 @@ const getUserTransaction = async (userId) => { return Transaction.find({ userId }, null, null) } -const getTransactionByBelongingId = async (belongingId) => { - return Transaction.find({ belongingId }, null, null) +const getTransactionByBelongingId = async (propertyId) => { + return Transaction.find({ propertyId }, null, null) } -const getBelongingInventory = async (belongingId) => { - const belonging = await Belonging.findById(belongingId, null, null) +const getBelongingInventory = async (propertyId) => { + const property = await Property.findById(propertyId, null, null) - if (!belonging) { - throw new AppError('NoBelongingFound', 404, {}) + if (!property) { + throw new AppError('NoPropertyFound', 404, {}) } - // Simplified aggregation to calculate inventory metrics const result = await Transaction.aggregate([ { $match: { - belongingId: belonging._id, + propertyId: property._id, $or: [{ status: 'finished' }, { status: 'pending' }], }, }, { $facet: { - // Calculate total inventory from finished transactions finishedInventory: [ { $match: { status: 'finished' } }, { @@ -86,7 +83,6 @@ const getBelongingInventory = async (belongingId) => { }, }, ], - // Calculate pending deposits pendingDepositsInventory: [ { $match: { @@ -101,7 +97,6 @@ const getBelongingInventory = async (belongingId) => { }, }, ], - // Calculate pending withdrawals pendingWithdrawsInventory: [ { $match: { @@ -119,7 +114,6 @@ const getBelongingInventory = async (belongingId) => { }, }, { - // Restructure the output to get only the values $project: { finishedInventory: { $ifNull: [{ $arrayElemAt: ['$finishedInventory.total', 0] }, 0] }, pendingDepositsInventory: { $ifNull: [{ $arrayElemAt: ['$pendingDepositsInventory.total', 0] }, 0] }, @@ -152,21 +146,27 @@ const deleteTransaction = async (transactionId) => { return null } -const ensureCollectionExists = async () => { - const collections = await client.collections().retrieve() - const collectionExists = collections.some((collection) => collection.name === 'transactions') +const ensureCollectionExists = async (forceRecreate = false) => { + try { + const collections = await client.collections().retrieve() + const collectionExists = collections.some((collection) => collection.name === 'transactions') - if (collectionExists) { - await client.collections('transactions').delete() + if (collectionExists && forceRecreate) { + await client.collections('transactions').delete() + } + + if (!collectionExists || forceRecreate) { + const schema = getTypeSenseSchema() + await client.collections().create(schema) + console.log('Collection "transactions" created successfully') + } + } catch (err) { + console.error('Error ensuring transactions collection exists:', err) } - - const schema = getTypeSenseSchema() - - await client.collections().create(schema) - console.log('Collection "transactions" created successfully') } const searchTransactions = async (inputQuery) => { + await ensureCollectionExists(false) inputQuery = updateInputQuery(inputQuery, {}) const tsFeatures = new TsApiFeatures(client, 'transactions', inputQuery).filter().facet().sort().limitFields().paginate().search() @@ -176,7 +176,7 @@ const searchTransactions = async (inputQuery) => { return { facets, transactions: items, totalCount, pagesCount } } -const updateInputQuery = (inputQuery, extraQueries, features) => { +const updateInputQuery = (inputQuery, extraQueries) => { for (const [key, value] of Object.entries(extraQueries)) { inputQuery[key] = value } @@ -189,38 +189,40 @@ const updateInputQuery = (inputQuery, extraQueries, features) => { const indexAllTransactions = async () => { const batchSize = 20 const count = await Transaction.countDocuments({}, null) - const rounds = Math.ceil(count / batchSize) + const rounds = Math.max(1, Math.ceil(count / batchSize)) - await ensureCollectionExists() + await ensureCollectionExists(true) for (let i = 1; i <= rounds; i++) { - console.log(`Indexing ${batchSize} transactions, Round ${i}`) - const transactions = await Transaction.find({}, null, null) .skip((i - 1) * batchSize) .limit(batchSize) - .populate('belongingId') + .populate('propertyId') + + if (transactions.length === 0) break let documents = [] for (const transaction of transactions) { const data = { - _id: transaction._id, - type: transaction.type, - status: transaction.status, + _id: transaction._id.toString(), + type: transaction.type || '', + status: transaction.status || '', source: transaction.source || '', - value: transaction.value, - description: transaction.description, - createdAt: Math.floor(new Date(transaction.createdAt).getTime() / 1000), // Store as timestamp in seconds - belongingName: transaction.belongingId.name, - belongingId: transaction.belongingId._id, + value: transaction.value || 0, + description: transaction.description || '', + createdAt: Math.floor(new Date(transaction.createdAt || Date.now()).getTime() / 1000), + belongingName: transaction.propertyId?.name || '', + belongingId: transaction.propertyId?._id ? transaction.propertyId._id.toString() : '', userName: transaction.userName || '', } documents.push(data) } - await client.collections('transactions').documents().import(documents, { action: 'emplace' }) + if (documents.length > 0) { + await client.collections('transactions').documents().import(documents, { action: 'emplace' }) + } } return { totalCount: count } diff --git a/config/.env b/config/.env index ce1b819..abbdb1d 100644 --- a/config/.env +++ b/config/.env @@ -1,15 +1,17 @@ -MONGODB_URL=mongodb://root:yBi0FXMb1rWNhFxu@services.irn12.chabokan.net:58178/my-app?authSource=admin +MONGODB_URL=mongodb://admin:*135792468*@localhost:27018/marbm?authSource=admin NODE_ENV=development PORT=3000 JWT_SECRET=Y@RJ$WT!@K0NS@MPL! -REDIS_URL=redis://:GFis6zU4DXW9Q7NQ@services.irn12.chabokan.net:19794/0 +REDIS_URL=redis://:135792468@localhost:6380/0 LOGIN_ATTEMPT_LIMIT=900 SUPER_ADMIN_ROLE_ID=66a2da5b9b989a8909948848 -TYPE_SENSE_URL=bm-typesense.chbk.app -TYPE_SENSE_API_KEY=rHV4vZePdfBYFC9q -AWS_ENDPOINT=bm-minio-2.chbk.app -AWS_URL=https://bm-minio-2.chbk.app -AWS_ACCESS_KEY=lwcAHMbCea4jeKMtj6SOjMtmETY93rBb -AWS_SECRET_KEY=xXKrceoCV3Wx35d0JmULgNvHJ0UlTQnO -AWS_REGION=default +TYPE_SENSE_URL=localhost:8109 +TYPE_SENSE_PROTOCOL=http +TYPE_SENSE_API_KEY=vYQq7pTgl6ACoRy +AWS_ENDPOINT=localhost +AWS_PORT=9002 +AWS_USE_SSL=false +AWS_ACCESS_KEY=admin +AWS_SECRET_KEY=135792468 +AWS_REGION=us-east-1 API_URL=http://localhost:3000 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index b1b91bc..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,56 +0,0 @@ -services: - # api-server: - # build: ./ - # ports: - # - 3000:4000 - # environment: - # - MONGODB_URL=mongodb://admin:*135792468*@bm_mongo:27017/marbm?authSource=admin - # - NODE_ENV=development - # - PORT=3000 - # - REDIS_URL=redis://:135792468@bm_redis:6379/0 - bm_minio: - image: minio/minio - ports: - - 9000:9000 - - 9001:9001 - environment: - MINIO_ROOT_USER: admin - MINIO_ROOT_PASSWORD: 135792468 - volumes: - - bm_minio:/upload/ - command: server /data --console-address ":9001" - bm_mongo: - image: mongo:8.0 - ports: - - 27018:27017 - environment: - - MONGO_INITDB_ROOT_USERNAME=admin - - MONGO_INITDB_ROOT_PASSWORD=*135792468* - - MONGO_INITDB_DATABASE=test - volumes: - - bm_mongo:/data/db - - bm_redis: - image: redis - command: redis-server --save 20 1 --loglevel warning --requirepass 135792468 - volumes: - - bm_redis:/data/ - ports: - - 6379:6379 - - bm_typesense: - image: typesense/typesense:27.1 - command: ./typesense-server - ports: - - '8108:8108' - environment: - TYPESENSE_API_KEY: vYQq7pTgl6ACoRy - TYPESENSE_DATA_DIR: /data - volumes: - - bm_typesense:/data - -volumes: - bm_mongo: - bm_redis: - bm_minio: - bm_typesense: diff --git a/events/index.js b/events/index.js index 6486b2f..1a209b5 100644 --- a/events/index.js +++ b/events/index.js @@ -1,23 +1,19 @@ // events/index.js const { eventEmitter } = require('./eventEmitter') -const createBelonging = require('../components/belonging/events/createBelonging') -const deleteBelonging = require('../components/belonging/events/deleteBelonging') +const createProperty = require('../components/property/events/createProperty') +const deleteProperty = require('../components/property/events/deleteProperty') const registerEventHandlers = () => { // Register all event handlers - createBelonging(eventEmitter) - deleteBelonging(eventEmitter) + createProperty(eventEmitter) + deleteProperty(eventEmitter) // Add additional error monitoring if needed eventEmitter.on('error', (error) => { - // You can add additional error handling here - // For example, sending to a monitoring service console.error('Event handler error:', error) }) - // Log when events are registered (optional) console.log('Event handlers registered successfully') } -// Helper function to safely emit events (can be used in other files) const safeEmit = async (eventName, ...args) => { try { return eventEmitter.emit(eventName, ...args) diff --git a/seeder/seederDBConnection.js b/seeder/seederDBConnection.js index 7ee6ac3..e8016bd 100644 --- a/seeder/seederDBConnection.js +++ b/seeder/seederDBConnection.js @@ -1,42 +1,32 @@ const mongoose = require('mongoose') -const port = process.env.PORT || 3000 const env = process.env.NODE_ENV || 'development' const redisClient = require('../setup/redis') const seederRunner = require('./seederRunner') const chalk = require('chalk') module.exports = (app) => { - let connectionString - if (env === 'development') { - connectionString = process.env.MONGODB_URL - } else if (env === 'test') { - connectionString = process.env.MONGODB_URL - } else if (env === 'production') { - connectionString = process.env.MONGODB_URL - } - console.log(env) + let connectionString = process.env.MONGODB_URL + console.log(`Environment: ${env}`) + mongoose .connect(connectionString, {}) - .then(function () { + .then(async function () { console.log('MongoDB Database Connected') - redisClient.connect().then((r) => { - console.log('Redis connection Established') - app.listen(port, function () { - console.log('Server Started On Port ' + port) + await redisClient.connect().catch((err) => console.log('Redis connect warning:', err.message)) + console.log('Redis connection Established') - console.log( - chalk.red.bold( - '-------- Running Seeders ---------' - ) - ) - seederRunner.run().then((r) => { - console.log(chalk.blue.bold('Seeding is done.')) - process.exit(0) - }) - }) - }) + console.log(chalk.red.bold('-------- Running Seeders ---------')) + try { + await seederRunner.run() + console.log(chalk.blue.bold('Seeding and indexing completed successfully.')) + } catch (err) { + console.error('Seeding error:', err) + } finally { + process.exit(0) + } }) .catch(function (err) { - console.log(err) + console.error('Database connection error:', err) + process.exit(1) }) } diff --git a/seeder/seederRunner.js b/seeder/seederRunner.js index 6316fa9..be543d4 100644 --- a/seeder/seederRunner.js +++ b/seeder/seederRunner.js @@ -1,8 +1,17 @@ const userSeeder = require('../components/user/userSeeder') const categorySeeder = require('../components/category/categorySeeder') const roleSeeder = require('../components/role/roleSeeder') +const featureSeeder = require('../components/feature/featureSeeder') +const propertyStatusSeeder = require('../components/propertyStatus/propertyStatusSeeder') +const propertyService = require('../components/property/propertyService') +const transactionService = require('../components/transaction/transactionService') + module.exports.run = async () => { await userSeeder.runSeeder() await categorySeeder.runSeeder() await roleSeeder.runSeeder() + await featureSeeder.runSeeder() + await propertyStatusSeeder.runSeeder() + await propertyService.indexAllProperties() + await transactionService.indexAllTransactions() } diff --git a/setup/typesense.js b/setup/typesense.js index bc6ef8f..31e9b0c 100644 --- a/setup/typesense.js +++ b/setup/typesense.js @@ -1,11 +1,16 @@ const TypeSense = require('typesense') +const tsUrlParts = (process.env.TYPE_SENSE_URL || 'localhost:8108').split(':') +const host = tsUrlParts[0] || 'localhost' +const port = tsUrlParts[1] || process.env.TYPE_SENSE_PORT || '8108' +const protocol = process.env.TYPE_SENSE_PROTOCOL || 'http' + let client = new TypeSense.Client({ nodes: [ { - host: process.env.TYPE_SENSE_URL, - port: '443', - protocol: 'https', + host: host, + port: port, + protocol: protocol, }, ], apiKey: process.env.TYPE_SENSE_API_KEY, diff --git a/utils/TsApiFeatures.js b/utils/TsApiFeatures.js index 222f499..d684542 100644 --- a/utils/TsApiFeatures.js +++ b/utils/TsApiFeatures.js @@ -166,8 +166,9 @@ class TsApiFeatures { per_page: this.queryOptions.per_page, }) }) + const tsProtocol = process.env.TYPE_SENSE_PROTOCOL || 'http' const config = { - url: 'https://' + process.env.TYPE_SENSE_URL + '/multi_search?x-typesense-api-key=' + process.env.TYPE_SENSE_API_KEY, + url: tsProtocol + '://' + process.env.TYPE_SENSE_URL + '/multi_search?x-typesense-api-key=' + process.env.TYPE_SENSE_API_KEY, method: 'POST', data: { searches, diff --git a/utils/upload/bucket.js b/utils/upload/bucket.js index 78d3a9b..996af24 100644 --- a/utils/upload/bucket.js +++ b/utils/upload/bucket.js @@ -4,11 +4,18 @@ const fs = require('fs') // Upload File to s3 bucket exports.uploadObject = async (file, bucketName) => { + const bucketExists = await minioClient.bucketExists(bucketName).catch(() => false) + if (!bucketExists) { + await minioClient.makeBucket(bucketName, process.env.AWS_REGION || 'us-east-1') + } + const fileStream = fs.createReadStream(file.path) const uploadedFile = await minioClient.putObject(bucketName, `${file.filename}-${file.originalName}`, fileStream) - uploadedFile.fileURL = 'https://' + process.env.AWS_ENDPOINT + '/' + bucketName + '/' + file.filename + '-' + file.originalName + const protocol = process.env.AWS_USE_SSL === 'true' ? 'https://' : 'http://' + const hostPort = process.env.AWS_PORT ? `${process.env.AWS_ENDPOINT}:${process.env.AWS_PORT}` : process.env.AWS_ENDPOINT + uploadedFile.fileURL = protocol + hostPort + '/' + bucketName + '/' + file.filename + '-' + file.originalName uploadedFile.fileName = file.filename + '-' + file.originalName diff --git a/utils/upload/minio.js b/utils/upload/minio.js index 67ac351..93518b6 100644 --- a/utils/upload/minio.js +++ b/utils/upload/minio.js @@ -1,30 +1,24 @@ const Minio = require('minio') -const endPoint = process.env.AWS_ENDPOINT +const endPoint = process.env.AWS_ENDPOINT || 'localhost' const accessKey = process.env.AWS_ACCESS_KEY const secretKey = process.env.AWS_SECRET_KEY -const region = process.env.AWS_REGION -const port = parseInt(process.env.AWS_PORT) +const region = process.env.AWS_REGION || 'us-east-1' +const port = parseInt(process.env.AWS_PORT) || 9000 +const useSSL = process.env.AWS_USE_SSL === 'true' -let client - -if (process.env.NODE_ENV === 'test') { - client = new Minio.Client({ - endPoint, - useSSL: true, - accessKey, - port, - secretKey, - region, - }) -} else { - client = new Minio.Client({ - endPoint, - useSSL: true, - accessKey, - secretKey, - region, - }) +const clientOptions = { + endPoint, + useSSL, + accessKey, + secretKey, + region, } +if (port) { + clientOptions.port = port +} + +const client = new Minio.Client(clientOptions) + module.exports = client