import { Injectable } from '@nestjs/common'; import { BaseQueryCoreDto } from './dto/base-query-core.dto'; import * as _ from 'lodash'; import { Prisma } from '@prisma/client'; @Injectable() export class BaseQueryCoreService { // Cache for storing relationship metadata to avoid repeated lookups private relationshipCache: Map = new Map(); generatePrismaQuery(query: BaseQueryCoreDto = {}) { const tempQuery: any = { ...query }; if (typeof tempQuery?.orderBy === 'string') { tempQuery.orderBy = [tempQuery?.orderBy]; } if (typeof tempQuery?.include === 'string') { tempQuery.include = [tempQuery?.include]; } if (typeof tempQuery?.search_column === 'string') { tempQuery.search_column = [tempQuery?.search_column]; } // Add this: Convert multi_search to array if it's a string if (typeof tempQuery?.multi_search === 'string') { tempQuery.multi_search = [tempQuery?.multi_search]; } if (tempQuery?.orderBy?.length > 0) { const orderBy = {}; tempQuery.orderBy.forEach((ob) => { let sortField = ob.split('|')[0]; sortField = sortField.split('.'); if (sortField.length > 1) { orderBy[sortField[0]] = this.getOrderByArray( sortField, ob.split('|')[1], ); } else { orderBy[sortField[0]] = ob.split('|')[1]; } }); tempQuery.orderBy = orderBy; } if (tempQuery?.include?.length > 0) { const formattedQry = this.formatIncludeQueryArray( tempQuery.include.sort(), ); const { include } = this.getIncludeArray(formattedQry); tempQuery.include = include; } if (tempQuery?.search_column?.length > 0 && tempQuery?.search?.length > 0) { const searchType = tempQuery?.search_type || 'contains'; const searchCondition = this.createSearchCondition( tempQuery.search, searchType, ); const formattedSearchQry = this.formatSearchQueryArray( tempQuery.search_column.sort(), searchCondition, ); tempQuery['where'] = formattedSearchQry; } // Handle multi-criteria search if (tempQuery?.multi_search?.length > 0) { const multiSearchConditions = this.formatMultiSearchQueryArray( tempQuery.multi_search, ); tempQuery['where'] = { AND: multiSearchConditions }; } delete tempQuery?.search_column; delete tempQuery?.search; delete tempQuery?.search_type; delete tempQuery?.multi_search; return tempQuery; } formatIncludeQueryArray(queryArr: any, baseVal: any = true) { let returnObj = {}; queryArr.map((qry, i) => { const rslt = this.formatArray(qry, baseVal); if (i > 0) { returnObj = _.merge(returnObj, rslt); } else { returnObj = rslt; } }); return returnObj; } formatSearchQueryArray(queryArr: any, baseVal: any) { const searchConditions: any[] = []; queryArr.forEach((qry: any) => { searchConditions.push(this.formatArray(qry, baseVal)); }); return { OR: searchConditions }; } formatMultiSearchQueryArray(multiSearchArr: string[]) { // Group conditions by column const columnGroups: { [key: string]: any[] } = {}; multiSearchArr.forEach((searchString: string) => { const [searchTerm, column, searchType] = searchString.split('|'); if (searchTerm && column) { const condition = this.createSearchCondition( searchTerm.trim(), (searchType || 'contains').trim() as 'contains' | 'equals', ); const formattedCondition = this.formatMultiSearchArray( column, condition, ); // Group by column name if (!columnGroups[column]) { columnGroups[column] = []; } columnGroups[column].push(formattedCondition); } }); // Convert grouped conditions to final query structure const finalConditions: any[] = []; Object.keys(columnGroups).forEach((column) => { const conditions = columnGroups[column]; if (conditions.length === 1) { // Single condition for this column finalConditions.push(conditions[0]); } else { // Multiple conditions for same column - use OR finalConditions.push({ OR: conditions }); } }); return finalConditions; } createSearchCondition( searchValue: string, searchType: 'contains' | 'equals', ) { const condition: any = {}; if (searchType === 'contains') { condition.contains = searchValue; condition.mode = 'insensitive'; } else if (searchType === 'equals') { condition.equals = searchValue; } return condition; } formatArray(qry: any, baseVal: any) { const returnObj = {}; const qArr = qry.split('.'); if (qArr.length > 1) { const ky = qArr[0]; qArr.shift(); if (typeof returnObj[ky] === 'undefined') { returnObj[ky] = this.formatArray(qArr.join('.'), baseVal); } else { returnObj[ky] = { ...returnObj[ky], ...this.formatArray(qArr.join('.'), baseVal), }; } } else { returnObj[qArr[0]] = baseVal; } return returnObj; } /** * Format array specifically for multi-search functionality * This method handles nested relations and applies dynamic relationship detection */ formatMultiSearchArray(qry: any, baseVal: any) { const returnObj = {}; const qArr = qry.split('.'); if (qArr.length > 1) { const ky = qArr[0]; qArr.shift(); // Recursively build nested condition const nestedCondition = this.formatMultiSearchArray( qArr.join('.'), baseVal, ); if (typeof returnObj[ky] === 'undefined') { // Apply dynamic relation wrapping for where conditions returnObj[ky] = this.wrapRelationCondition(ky, nestedCondition); } else { returnObj[ky] = { ...returnObj[ky], ...this.wrapRelationCondition(ky, nestedCondition), }; } } else { returnObj[qArr[0]] = baseVal; } return returnObj; } /** * Wrap relation conditions with proper Prisma syntax * For one-to-many relations, use 'some' * For one-to-one relations, use 'is' * Dynamically detects relationship type from Prisma DMMF */ private wrapRelationCondition(relationName: string, condition: any) { const relationType = this.detectRelationType(relationName); if (relationType === 'one-to-many') { return { some: condition }; } else { return { is: condition }; } } /** * Dynamically detect if a relation is one-to-many or one-to-one * Uses Prisma's DMMF (Data Model Meta Format) to introspect the schema */ private detectRelationType( relationName: string, ): 'one-to-many' | 'one-to-one' { // Check cache first if (this.relationshipCache.has(relationName)) { return this.relationshipCache.get(relationName)!; } try { // Access Prisma's DMMF (Data Model Meta Format) for schema introspection const dmmf = Prisma.dmmf; // Search through all models to find the relation for (const model of dmmf.datamodel.models) { const field = model.fields.find((f) => f.name === relationName); if (field && field.kind === 'object') { // Check if it's a list (array) which indicates one-to-many const relationType = field.isList ? 'one-to-many' : 'one-to-one'; // Cache the result this.relationshipCache.set(relationName, relationType); return relationType; } } // Default to one-to-one if not found (safer default for filtering) // This prevents incorrect 'some' usage on non-existent relations this.relationshipCache.set(relationName, 'one-to-one'); return 'one-to-one'; } catch { // Fallback: If DMMF is not accessible, use one-to-one as safe default console.warn( `Could not detect relation type for "${relationName}". Defaulting to one-to-one.`, ); this.relationshipCache.set(relationName, 'one-to-one'); return 'one-to-one'; } } getOrderByArray(fieldArr: any, sort) { fieldArr.shift(); const incO: any = {}; incO[fieldArr[0]] = fieldArr.length > 1 ? this.getOrderByArray(fieldArr, sort) : sort; return incO; } getIncludeArray(incArr: any) { const include = {}; for (const key in incArr) { if (incArr.hasOwnProperty(key)) { if (incArr[key] === true) { include[key] = true; } else { include[key] = this.getIncludeArray(incArr[key]); } } } return { include }; } }