Spaces:
Runtime error
Runtime error
| import { Injectable } from '@nestjs/common'; | |
| import { BaseQueryCoreDto } from './dto/base-query-core.dto'; | |
| import * as _ from 'lodash'; | |
| import { Prisma } from '@prisma/client'; | |
| () | |
| export class BaseQueryCoreService { | |
| // Cache for storing relationship metadata to avoid repeated lookups | |
| private relationshipCache: Map<string, 'one-to-many' | 'one-to-one'> = | |
| 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 }; | |
| } | |
| } | |