Skip to content

Commit

Permalink
Release 1.1.7 (#52)
Browse files Browse the repository at this point in the history
- Add FilteredProductsBuilder, FiltersSQLRawFactory for build raw sql
- Fix filtered products, available filters behavior
  • Loading branch information
mayer1a authored Sep 10, 2024
2 parents eea6f78 + 0556a3e commit 1bd85c1
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 113 deletions.
73 changes: 73 additions & 0 deletions Sources/App/Controllers/Products/FilteredProductsBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// FilteredProductsBuilder.swift
//
//
// Created by Artem Mayer on 10.09.2024.
//

import Foundation
import FluentSQL

final class FilteredProductsBuilder {

private var categoriesWhereCondition: String = ""
private var filtersWhereCondition: String = ""
private var filtersHavingCondition: SQLQueryString = ""

@discardableResult
func setCategoriesWhereCondition(for categoriesIDs: [UUID]?) -> FilteredProductsBuilder {
guard let categoriesIDs else { return self }
let categoriesList = categoriesIDs.map({ "'\($0)'" }).joined(separator: ",")
categoriesWhereCondition = "CP.CATEGORY_ID IN (\(categoriesList))"
return self
}

@discardableResult
func setFiltersWhereCondition(for filters: [String: [String]]) -> FilteredProductsBuilder {
let whereCondition = filters.reduce("") { partialResult, pair in
guard !pair.value.isEmpty else { return partialResult }

let values = pair.value.map({ "'\($0)'" }).joined(separator: ",")
if partialResult.isEmpty {
return "(P_PROPERTY.CODE = '\(pair.key)' AND P_VALUE.VALUE IN (\(values))"
} else {
return partialResult + "\n\tOR P_PROPERTY.CODE = '\(pair.key)' AND P_VALUE.VALUE IN (\(values))"
}
}

if !whereCondition.isEmpty {
filtersWhereCondition = "\(whereCondition)\n)"
filtersHavingCondition = "HAVING COUNT(DISTINCT P_VALUE.VALUE) = \(bind: filters.count)"
}

return self
}

func build() -> SQLQueryString? {
let whereCondition: SQLQueryString

if !categoriesWhereCondition.isEmpty, !filtersWhereCondition.isEmpty {
whereCondition = "\(unsafeRaw: categoriesWhereCondition)\n\tAND \(unsafeRaw: filtersWhereCondition)"
} else if !filtersWhereCondition.isEmpty {
whereCondition = "\(unsafeRaw: filtersWhereCondition)"
} else {
return nil
}

return """
SELECT DISTINCT P.*
FROM
PRODUCTS P
JOIN PRODUCT_VARIANTS PV ON P.ID = PV.PRODUCT_ID
JOIN "product_variants+property_values" PVPV ON PV.ID = PVPV.PRODUCT_VARIANT_ID
JOIN PROPERTY_VALUES P_VALUE ON PVPV.PROPERTY_VALUE_ID = P_VALUE.ID
JOIN PRODUCT_PROPERTIES P_PROPERTY ON P_VALUE.PRODUCT_PROPERTY_ID = P_PROPERTY.ID
JOIN "categories+products" CP ON CP.PRODUCT_ID = P.ID
WHERE
\(whereCondition)
GROUP BY P.ID
\(filtersHavingCondition)
"""
}

}
102 changes: 102 additions & 0 deletions Sources/App/Controllers/Products/FiltersSQLRawFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//
// FiltersSQLRawFactory.swift
//
//
// Created by Artem Mayer on 10.09.2024.
//

import Foundation
import FluentSQL

final class FiltersSQLRawFactory {

private init() {}

static func makeAvailableFiltersSQL(for categoriesIDs: [UUID], filters: [String: [String]]) -> SQLQueryString {
"""
\(getFilteredProducts(for: categoriesIDs, filters: filters))
\(getAvailableFilters(for: Array(filters.keys)))
"""
}

private static func getFilteredProducts(for categoriesIDs: [UUID], filters: [String: [String]]) -> SQLQueryString {
let codeValueFiltersSQL: SQLQueryString = filters.map {
let values = $0.value.map({ "'\($0)'" }).joined(separator: ",")
return "(P_PROPERTY.CODE='\(unsafeRaw: $0.key)' AND P_VALUE.VALUE IN (\(unsafeRaw: values)))"
}.joined(separator: "\n\t\t\tOR ")

return """
WITH FILTERED_PRODUCTS AS (
SELECT DISTINCT P.ID
FROM
PRODUCTS P
JOIN PRODUCT_VARIANTS PV ON P.ID = PV.PRODUCT_ID
JOIN "product_variants+property_values" PVPV ON PV.ID = PVPV.PRODUCT_VARIANT_ID
JOIN PROPERTY_VALUES P_VALUE ON PVPV.PROPERTY_VALUE_ID = P_VALUE.ID
JOIN PRODUCT_PROPERTIES P_PROPERTY ON P_VALUE.PRODUCT_PROPERTY_ID = P_PROPERTY.ID
JOIN "categories+products" CP ON CP.PRODUCT_ID = P.ID
WHERE
\(getWhereCategoriesCondition(for: categoriesIDs))
AND (
\(codeValueFiltersSQL)
)
GROUP BY P.ID
HAVING COUNT(DISTINCT P_VALUE.VALUE) >= \(bind: filters.count - 1)
)
"""
}

private static func getAvailableFilters(for filtersCodes: [String]) -> SQLQueryString {
var selectedFiltersCodes: [String] = []

var resultSQL = filtersCodes.map { key -> String in
selectedFiltersCodes.append(key)
return getSelectFilterTypeSQL(for: key, excludeKeys: nil)
}.joined(separator: "\nUNION ALL\n")

resultSQL += "\nUNION ALL\n\(getSelectFilterTypeSQL(for: nil, excludeKeys: selectedFiltersCodes))"

return "\(unsafeRaw: resultSQL)"
}

/// - WARNING: Either `key` or `excludeKeys` must be not nil
private static func getSelectFilterTypeSQL(for key: String?, excludeKeys: [String]?) -> String {
let whereCondition: String

if let key {
whereCondition = "WHERE P_PROPERTY.CODE = '\(key)'"
} else if let excludeKeys {
whereCondition = excludeKeys.reduce("") { initialResult, selectedKey -> String in
if initialResult.isEmpty {
return "WHERE P_PROPERTY.CODE != '\(selectedKey)'"
} else {
return initialResult + " AND P_PROPERTY.CODE != '\(selectedKey)'"
}
}
} else {
fatalError("keys or excludeKeys must be not nil!")
}

return """
SELECT
P_PROPERTY.CODE AS property_code,
P_PROPERTY.NAME AS property_name,
P_VALUE.VALUE AS property_value,
COUNT(DISTINCT P.ID) AS count
FROM
PRODUCTS P
JOIN PRODUCT_VARIANTS PV ON P.ID = PV.PRODUCT_ID
JOIN "product_variants+property_values" PVPV ON PV.ID = PVPV.PRODUCT_VARIANT_ID
JOIN PROPERTY_VALUES P_VALUE ON PVPV.PROPERTY_VALUE_ID = P_VALUE.ID
JOIN PRODUCT_PROPERTIES P_PROPERTY ON P_VALUE.PRODUCT_PROPERTY_ID = P_PROPERTY.ID
JOIN FILTERED_PRODUCTS FP ON P.ID = FP.ID
\(whereCondition)
GROUP BY P_PROPERTY.CODE, P_PROPERTY.NAME, P_VALUE.VALUE
"""
}

private static func getWhereCategoriesCondition(for categoriesIDs: [UUID]) -> SQLQueryString {
"CP.CATEGORY_ID IN (\(unsafeRaw: categoriesIDs.map { "'\($0.uuidString)'" }.joined(separator: ",")))"
}

}
Loading

0 comments on commit 1bd85c1

Please sign in to comment.