Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 219 additions & 19 deletions src/adapters/postgres/fields-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@ import { LoggerUtil } from "src/common/logger/LoggerUtil";
import { API_RESPONSES } from "@utils/response.messages";
import { FieldValuesDeleteDto } from "src/fields/dto/field-values-delete.dto";
import { check } from "prettier";
import { Tenants } from "src/userTenantMapping/entities/tenant.entity";
@Injectable()
export class PostgresFieldsService implements IServicelocatorfields {
constructor(
@InjectRepository(Fields)
private fieldsRepository: Repository<Fields>,
@InjectRepository(FieldValues)
private fieldsValuesRepository: Repository<FieldValues>
private fieldsValuesRepository: Repository<FieldValues>,
@InjectRepository(Tenants)
private tenantsRepository: Repository<Tenants>
) { }

async getFormCustomField(requiredData, response) {
Expand Down Expand Up @@ -1864,7 +1867,8 @@ export class PostgresFieldsService implements IServicelocatorfields {
public async getCustomFieldDetails(
itemId: string,
tableName: string,
fieldOption?: boolean
fieldOption?: boolean,
tenantId?: string
) {
let joinCond;
if (tableName === "Users") {
Expand All @@ -1873,24 +1877,47 @@ export class PostgresFieldsService implements IServicelocatorfields {
joinCond = `fv."itemId" = u."cohortId"`;
}
try {
let tenantFilter = '';
let queryParams: any[] = [itemId];

// If tenantId is provided, fetch parent tenant ID and build filter
if (tenantId) {
const tenant = await this.tenantsRepository.findOne({
where: { tenantId: tenantId }
});

if (tenant?.parentId) {
// Include both tenant and parent tenant in filter
tenantFilter = 'AND f."tenantId" IN ($2, $3)';
queryParams = [itemId, tenantId, tenant.parentId];
LoggerUtil.log(`Fetching custom fields for tenant ${tenantId} and parent ${tenant.parentId}`);
} else {
// Only tenant filter
tenantFilter = 'AND f."tenantId" = $2';
queryParams = [itemId, tenantId];
LoggerUtil.log(`Fetching custom fields for tenant ${tenantId} only`);
}
}

const query = `
SELECT DISTINCT
f."fieldId",
f."label",
fv."value",
f."type",
f."fieldParams",
f."sourceDetails"
FROM public."${tableName}" u
LEFT JOIN (
SELECT DISTINCT ON (fv."fieldId", fv."itemId") fv.*
FROM public."FieldValues" fv
) fv ON ${joinCond}
INNER JOIN public."Fields" f ON fv."fieldId" = f."fieldId"
WHERE fv."itemId" = $1;
`;

let result = await this.fieldsRepository.query(query, [itemId]);
SELECT DISTINCT
f."fieldId",
f."label",
fv."value",
f."type",
f."fieldParams",
f."sourceDetails",
f."tenantId"
FROM public."${tableName}" u
LEFT JOIN (
SELECT DISTINCT ON (fv."fieldId", fv."itemId") fv.*
FROM public."FieldValues" fv
) fv ON ${joinCond}
INNER JOIN public."Fields" f ON fv."fieldId" = f."fieldId"
WHERE fv."itemId" = $1 ${tenantFilter};
`;

let result = await this.fieldsRepository.query(query, queryParams);
result = result.map(async (data) => {
const allIds = data.value;
let optionValues;
Expand Down Expand Up @@ -1982,6 +2009,179 @@ export class PostgresFieldsService implements IServicelocatorfields {
}
}

/**
* Batch fetch custom fields for multiple items (optimized for N+1 query problem)
* @param itemIds - Array of item IDs (userIds or cohortIds)
* @param tableName - Table name ('Users' or 'Cohort')
* @returns Object mapping itemId to their custom fields array
*/
public async getBulkCustomFieldDetails(
itemIds: string[],
tableName: string,
tenantId?: string
): Promise<Record<string, any[]>> {
if (!itemIds || itemIds.length === 0) {
return {};
}

let joinCond: string;
if (tableName === "Users") {
joinCond = `fv."itemId" = u."userId"`;
} else if (tableName === "Cohort") {
joinCond = `fv."itemId" = u."cohortId"`;
}

try {
let tenantFilter = '';
let fieldTenantFilter = '';
let queryParams: any[] = [itemIds];

// If tenantId is provided, fetch parent tenant ID and build filter
if (tenantId) {
const tenant = await this.tenantsRepository.findOne({
where: { tenantId: tenantId }
});

if (tenant?.parentId) {
// Include both tenant and parent tenant in filter
tenantFilter = 'AND fv."tenantId" IN ($2, $3)';
queryParams = [itemIds, tenantId, tenant.parentId];
} else {
// Only tenant filter
tenantFilter = 'AND fv."tenantId" = $2';
queryParams = [itemIds, tenantId];
}
}

const query = `
SELECT DISTINCT
fv."itemId",
f."fieldId",
f."label",
fv."value",
f."type",
f."fieldParams",
f."sourceDetails",
f."tenantId"
FROM public."${tableName}" u
LEFT JOIN (
SELECT DISTINCT ON (fv."fieldId", fv."itemId") fv.*
FROM public."FieldValues" fv
WHERE fv."itemId" = ANY($1) ${tenantFilter}
) fv ON ${joinCond}
INNER JOIN public."Fields" f ON fv."fieldId" = f."fieldId"
WHERE fv."itemId" = ANY($1) ${fieldTenantFilter}
ORDER BY fv."itemId", f."fieldId";
`;
console.log("query -->> ", query);
console.log("queryParams -->> ", queryParams);
let results = await this.fieldsRepository.query(query, queryParams);

// Process all results
const processedResults = await Promise.all(
results.map(async (data) => {
const allIds = data.value;
let processedValue = [];
let allSelectedValues;
const selectedValues = data.value;
const allFieldsOptions = data?.fieldParams?.options
? data.fieldParams.options
: null;

if (data.sourceDetails) {
if (data.sourceDetails.source === "fieldparams") {
allFieldsOptions.forEach((option) => {
const selectedOptionKey = option.value;

if (data.type === "checkbox" || data.type === "drop_down") {
if (selectedValues.includes(selectedOptionKey)) {
allSelectedValues = {
id: option?.value,
value: option?.value,
label: option?.label,
};
processedValue.push(allSelectedValues);
}
} else {
if (selectedValues.includes(selectedOptionKey)) {
allSelectedValues = {
id: option?.name,
value: option?.value,
label: option?.label,
order: option?.order,
};
processedValue.push(allSelectedValues);
}
}
});
} else if (data.sourceDetails.source === "table") {
const whereCond = `"${data.sourceDetails.table}_id" IN (${allIds})`;
const labels = await this.findDynamicOptions(
data.sourceDetails.table,
whereCond
);
const tableName = data.sourceDetails.table;

const idField = `${tableName}_id`;
const nameField = `${tableName}_name`;

processedValue = labels.map((data) => ({
id: data[idField],
value: data[nameField],
}));
} else if (data.sourceDetails?.externalsource) {
processedValue = data?.value;
}
} else {
processedValue = selectedValues;
}

return {
itemId: data.itemId,
fieldId: data.fieldId,
label: data.label,
type: data.type,
selectedValues: processedValue,
};
})
);

// Group by itemId
const groupedByItemId: Record<string, any[]> = {};

// Initialize all itemIds with empty arrays
itemIds.forEach(itemId => {
groupedByItemId[itemId] = [];
});

// Group results by itemId
processedResults.forEach((field) => {
if (!groupedByItemId[field.itemId]) {
groupedByItemId[field.itemId] = [];
}
groupedByItemId[field.itemId].push({
fieldId: field.fieldId,
label: field.label,
selectedValues: field.selectedValues,
type: field.type,
});
});

return groupedByItemId;
} catch (error) {
LoggerUtil.error(
`${API_RESPONSES.SERVER_ERROR}`,
`Error in getBulkCustomFieldDetails: ${error.message}`
);
// Return empty object for all items on error
const emptyResult: Record<string, any[]> = {};
itemIds.forEach(itemId => {
emptyResult[itemId] = [];
});
return emptyResult;
}
}

public async getFieldsByIds(fieldIds: string[]) {
return this.fieldsRepository.find({
where: {
Expand Down
Loading