Files
kavehhn ca911b97a0 REFACTOR: Belonging → Property system migration + utilities update
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
2026-08-11 20:35:47 +03:30

195 lines
6.7 KiB
JavaScript

const axios = require('axios')
class TsApiFeatures {
constructor(client, collectionName, queryString) {
this.client = client
this.collectionName = collectionName
this.queryString = queryString
this.queryOptions = {}
}
search() {
// Set `q` to provided search term or default to `*` for all documents
this.queryOptions.q = this.queryString.query || '*'
// Set `query_by` to specified fields or default to `*`
this.queryOptions.query_by = this.queryString.query_by || 'name'
return this
}
filter() {
let filters = []
let rawKeys = []
// Add any additional filter conditions from queryString, if needed
Object.keys(this.queryString).forEach((key) => {
let operatorConditions = []
let normalValues = []
if (
[
'page',
'sort',
'limit',
'fields',
'per_page',
'filter',
'query',
'facet_by',
'query_by',
'highlight_full_fields',
].includes(key)
)
return
let value = this.queryString[key]
rawKeys.push(key)
if (!Array.isArray(value)) {
value = [value]
}
if (/\b(gte|gt|lte|lt|ne|eq)\b/.test(value)) {
const operatorMap = { gte: '>=', gt: '>', lte: '<=', lt: '<', ne: '!=', eq: '=' }
const match = value.match(/\b(gte|gt|lte|lt|ne|eq)\b/)
if (match) {
const operator = operatorMap[match[0]]
const cleanValue = value.replace(match[0], '').trim()
operatorConditions.push(`${key}${operator}${cleanValue}`)
}
} else {
normalValues.push(value)
}
if (operatorConditions.length > 0 && normalValues.length > 0) {
// Add all operator conditions
operatorConditions.forEach((cond) => filters.push(cond))
// Add the normal values condition
if (normalValues.length === 1) {
filters.push(`${key}:=${normalValues[0]}`)
} else {
filters.push(`${key}:=[${normalValues.join(',')}]`)
}
} else if (operatorConditions.length > 0) {
// Only operators
operatorConditions.forEach((cond) => filters.push(cond))
} else {
// Only normal values
if (normalValues.length === 1) {
filters.push(`${key}:=${normalValues[0]}`)
} else if (normalValues.length > 1) {
filters.push(`${key}:=[${normalValues.join(',')}]`)
}
}
})
// Join all conditions for TypeSense filter
if (filters.length) {
this.queryOptions.filters = filters.join(' && ')
}
const searchFilters = []
rawKeys.forEach((value, index) => {
const tempArray = JSON.parse(JSON.stringify(filters))
tempArray.splice(index, 1)
searchFilters.push(tempArray.join(' && '))
})
this.filterKeys = rawKeys
this.searchFilters = searchFilters
return this
}
sort() {
if (this.queryString.sort) {
this.queryOptions.sort_by = this.queryString.sort.split(',').join(',')
} else {
this.queryOptions.sort_by = 'createdAt:desc'
}
return this
}
limitFields() {
if (this.queryString.fields) {
this.queryOptions.include_fields = this.queryString.fields.split(',').join(',')
} else {
this.queryOptions.exclude_fields = '__v'
}
return this
}
facet() {
// Set `facet_by` based on the queryString, if available
if (this.queryString.facet_by) {
this.queryOptions.facet_by = this.queryString.facet_by
this.queryOptions.highlight_full_fields = this.queryString.highlight_full_fields
this.queryOptions.exhaustive_search = true
}
return this
}
paginate() {
const page = parseInt(this.queryString.page) || 1
const per_page = parseInt(this.queryString.per_page) || 20
this.queryOptions.page = page
this.queryOptions.per_page = per_page
return this
}
async execute() {
const searches = [
{
collection: this.collectionName,
exhaustive_search: this.queryOptions.exhaustive_search,
query_by: this.queryOptions.query_by,
filter_by: this.queryOptions.filters,
highlight_full_fields: this.queryOptions.highlight_full_fields,
q: this.queryOptions.q,
facet_by: this.queryOptions.facet_by,
max_facet_values: 100,
page: this.queryOptions.page,
per_page: this.queryOptions.per_page,
},
]
this.filterKeys.forEach((value, index) => {
searches.push({
collection: this.collectionName,
exhaustive_search: this.queryOptions.exhaustive_search,
query_by: this.queryOptions.query_by,
filter_by: this.searchFilters[index],
highlight_full_fields: this.queryOptions.highlight_full_fields,
q: this.queryOptions.q,
facet_by: value,
max_facet_values: 100,
page: this.queryOptions.page,
per_page: this.queryOptions.per_page,
})
})
const tsProtocol = process.env.TYPE_SENSE_PROTOCOL || 'http'
const config = {
url: tsProtocol + '://' + process.env.TYPE_SENSE_URL + '/multi_search?x-typesense-api-key=' + process.env.TYPE_SENSE_API_KEY,
method: 'POST',
data: {
searches,
},
}
const request = await axios(config)
const results = request.data.results[0]
let facet_counts = results.facet_counts
this.filterKeys.forEach((value, index) => {
const objIndex = facet_counts.findIndex((item) => item.field_name === value)
facet_counts[objIndex] = request.data.results[index + 1].facet_counts[0]
})
return {
facets: facet_counts,
items: results.hits.map((hit) => hit.document),
totalCount: results.found,
pagesCount: Math.ceil(results.found / this.queryOptions.per_page),
}
}
}
module.exports = TsApiFeatures