streamflix-api / src /core /base-query-core /base-query-core.service.ts
Akshar2325
✨ feat(admin): introduce super admin models and authentication
f3abb0d
Raw
History Blame
8.84 kB
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<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 };
}
}