From 0b13ddccae96f77cfbd8268853407af87e0f5359 Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Fri, 17 Oct 2025 20:14:02 +0530 Subject: [PATCH 1/4] userListAPI lode update_and status set --- src/adapters/postgres/fields-adapter.ts | 150 ++++++++++++++++++++++++ src/adapters/postgres/user-adapter.ts | 50 +++++--- src/adapters/userservicelocator.ts | 3 +- src/user/user.controller.ts | 12 +- 4 files changed, 194 insertions(+), 21 deletions(-) diff --git a/src/adapters/postgres/fields-adapter.ts b/src/adapters/postgres/fields-adapter.ts index a3607b8d..ced6a69b 100644 --- a/src/adapters/postgres/fields-adapter.ts +++ b/src/adapters/postgres/fields-adapter.ts @@ -1970,6 +1970,156 @@ 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 + ): Promise> { + 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 { + // Single query to fetch all custom fields for all items + const query = ` + SELECT DISTINCT + fv."itemId", + 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 + WHERE fv."itemId" = ANY($1) + ) fv ON ${joinCond} + INNER JOIN public."Fields" f ON fv."fieldId" = f."fieldId" + WHERE fv."itemId" = ANY($1) + ORDER BY fv."itemId", f."fieldId"; + `; + + let results = await this.fieldsRepository.query(query, [itemIds]); + + // 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 = {}; + + // 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 = {}; + itemIds.forEach(itemId => { + emptyResult[itemId] = []; + }); + return emptyResult; + } + } + public async getFieldsByIds(fieldIds: string[]) { return this.fieldsRepository.find({ where: { diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 6c6b30ff..eac773a3 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -356,11 +356,12 @@ export class PostgresUserService implements IServicelocator { tenantId: string, request: any, response: any, - userSearchDto: UserSearchDto + userSearchDto: UserSearchDto, + includeCustomFields: boolean = true ) { const apiId = APIID.USER_LIST; try { - const findData = await this.findAllUserDetails(userSearchDto, tenantId); + const findData = await this.findAllUserDetails(userSearchDto, tenantId, includeCustomFields); if (findData === false) { LoggerUtil.error( @@ -403,7 +404,7 @@ export class PostgresUserService implements IServicelocator { } - async findAllUserDetails(userSearchDto, tenantId?: string) { + async findAllUserDetails(userSearchDto, tenantId?: string, includeCustomFields: boolean = true) { let { limit, offset, filters, exclude, sort } = userSearchDto; let excludeCohortIdes; let excludeUserIdes; @@ -574,7 +575,7 @@ export class PostgresUserService implements IServicelocator { } //Get user core fields data - const query = `SELECT U."userId",U."enrollmentId", U."username",U."email", U."firstName", U."name",UTM."tenantId", U."middleName", U."lastName", U."gender", U."dob", R."name" AS role, U."mobile", U."createdBy",U."updatedBy", U."createdAt", U."updatedAt", U."status", COUNT(*) OVER() AS total_count + const query = `SELECT U."userId",U."enrollmentId", U."username",U."email", U."firstName", U."name",UTM."tenantId", U."middleName", U."lastName", U."gender", U."dob", R."name" AS role, U."mobile", U."createdBy",U."updatedBy", U."createdAt", U."updatedAt", U."status", UTM."status" AS "platformStatus", COUNT(*) OVER() AS total_count FROM public."Users" U LEFT JOIN public."CohortMembers" CM ON CM."userId" = U."userId" @@ -583,28 +584,41 @@ export class PostgresUserService implements IServicelocator { LEFT JOIN public."UserTenantMapping" UTM ON UTM."userId" = U."userId" LEFT JOIN public."Roles" R - ON R."roleId" = UR."roleId" ${whereCondition} GROUP BY U."userId",UTM."tenantId", R."name" ${orderingCondition} ${offset} ${limit}`; + ON R."roleId" = UR."roleId" ${whereCondition} GROUP BY U."userId",UTM."tenantId", UTM."status", R."name" ${orderingCondition} ${offset} ${limit}`; const userDetails = await this.usersRepository.query(query); if (userDetails.length > 0) { result.totalCount = parseInt(userDetails[0].total_count, 10); - // Get user custom field data - for (const userData of userDetails) { - const customFields = await this.fieldsService.getCustomFieldDetails( - userData.userId, 'Users' + // OPTIMIZED: Conditionally fetch custom fields only when requested + if (includeCustomFields) { + // OPTIMIZED: Batch fetch custom fields for all users in one query (instead of N+1 queries) + const userIds = userDetails.map(user => user.userId); + const bulkCustomFields = await this.fieldsService.getBulkCustomFieldDetails( + userIds, 'Users' ); - userData["customFields"] = Array.isArray(customFields) - ? customFields.map((data) => ({ - fieldId: data?.fieldId, - label: data?.label, - selectedValues: data?.selectedValues, - type: data?.type, - })) - : []; + // Map custom fields back to users (in-memory operation - fast!) + for (const userData of userDetails) { + const customFields = bulkCustomFields[userData.userId] || []; + + userData["customFields"] = Array.isArray(customFields) + ? customFields.map((data) => ({ + fieldId: data?.fieldId, + label: data?.label, + selectedValues: data?.selectedValues, + type: data?.type, + })) + : []; - result.getUserDetails.push(userData); + result.getUserDetails.push(userData); + } + } else { + // Skip custom fields fetch - much faster for listing + for (const userData of userDetails) { + userData["customFields"] = []; + result.getUserDetails.push(userData); + } } } else { return false; diff --git a/src/adapters/userservicelocator.ts b/src/adapters/userservicelocator.ts index b20d5609..9cabba67 100644 --- a/src/adapters/userservicelocator.ts +++ b/src/adapters/userservicelocator.ts @@ -25,7 +25,8 @@ export interface IServicelocator { tenantId: string, request: any, response: any, - userSearchDto: UserSearchDto + userSearchDto: UserSearchDto, + includeCustomFields?: boolean ); resetUserPassword( request: any, diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 15cbd4f0..3b1f3216 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -192,16 +192,24 @@ export class UserController { @ApiHeader({ name: "tenantid", }) + @ApiQuery({ + name: "includeCustomFields", + description: "Include custom fields in response (default: true). Set to false for faster response.", + required: false, + type: Boolean, + }) public async searchUser( @Headers() headers, @Req() request: Request, @Res() response: Response, - @Body() userSearchDto: UserSearchDto + @Body() userSearchDto: UserSearchDto, + @Query("includeCustomFields") includeCustomFields: string = "true" ) { const tenantId = headers["tenantid"]; + const shouldIncludeCustomFields = includeCustomFields !== "false"; return await this.userAdapter .buildUserAdapter() - .searchUser(tenantId, request, response, userSearchDto); + .searchUser(tenantId, request, response, userSearchDto, shouldIncludeCustomFields); } @Post("/password-reset-link") From 358f82594b3a7000c5ee033a0891054b2df1262f Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Tue, 4 Nov 2025 20:59:25 +0530 Subject: [PATCH 2/4] Implement user-to-tenant mapping update logic --- src/adapters/postgres/user-adapter.ts | 42 +++++++++++++- src/adapters/userservicelocator.ts | 5 ++ .../decorators/getTenantId.decorator.ts | 27 +++++++++ src/common/utils/api-id.config.ts | 1 + src/common/utils/response.messages.ts | 1 + src/config/tenant.config.ts | 30 ++++++++++ src/user/dto/user-search.dto.ts | 10 ++++ src/user/user.controller.ts | 55 +++++++++++++++---- 8 files changed, 158 insertions(+), 13 deletions(-) create mode 100644 src/common/decorators/getTenantId.decorator.ts create mode 100644 src/config/tenant.config.ts diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index eac773a3..748ebf90 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -403,6 +403,44 @@ export class PostgresUserService implements IServicelocator { } } + /** + * Multi-tenant user list service function + * Calls the existing searchUser function + */ + async searchUserMultiTenant( + request: any, + response: any, + userSearchDto: UserSearchDto + ) { + const apiId = APIID.USER_HIERARCHY_VIEW; + + let searchUserData = await this.findAllUserDetails(userSearchDto, null, false); + + if (!(searchUserData && searchUserData.getUserDetails?.length)) { + return APIResponse.error( + response, + apiId, + API_RESPONSES.USER_NOT_FOUND, + API_RESPONSES.NOT_FOUND, + HttpStatus.NOT_FOUND + ); + } + // Fetch and assign custom fields for each user + for (let user of searchUserData.getUserDetails) { + const parentTenantCustomFieldData = await this.fieldsService.getCustomFieldDetails(user.userId, 'Users'); + user.customFields = parentTenantCustomFieldData || []; + } + + LoggerUtil.log(API_RESPONSES.USER_HIERARCHY_VIEW_SUCCESS, apiId); + return await APIResponse.success( + response, + apiId, + searchUserData, + HttpStatus.OK, + API_RESPONSES.USER_HIERARCHY_VIEW_SUCCESS + ); + } + async findAllUserDetails(userSearchDto, tenantId?: string, includeCustomFields: boolean = true) { let { limit, offset, filters, exclude, sort } = userSearchDto; @@ -994,7 +1032,7 @@ export class PostgresUserService implements IServicelocator { if (userDto?.customFields?.length > 0) { // additionalData?: { tenantId?: string, contextType?: string, createdBy?: string, updatedBy?: string } let additionalData = { - tenantId : userDto.userData?.tenantId, + tenantId: userDto.userData?.tenantId, contextType: "USER", createdBy: userDto.userData?.createdBy, updatedBy: userDto.userData?.updatedBy @@ -1540,7 +1578,7 @@ export class PostgresUserService implements IServicelocator { // Prepare additional data for FieldValues table const additionalData = { - tenantId: userCreateDto.tenantCohortRoleMapping?.[0]?.tenantId || null, + tenantId: userCreateDto.tenantCohortRoleMapping?.[0]?.tenantId || null, contextType: "USER", createdBy: userCreateDto.createdBy, updatedBy: userCreateDto.updatedBy, diff --git a/src/adapters/userservicelocator.ts b/src/adapters/userservicelocator.ts index 9cabba67..1e1cb96c 100644 --- a/src/adapters/userservicelocator.ts +++ b/src/adapters/userservicelocator.ts @@ -28,6 +28,11 @@ export interface IServicelocator { userSearchDto: UserSearchDto, includeCustomFields?: boolean ); + searchUserMultiTenant( + request: any, + response: any, + userSearchDto: UserSearchDto + ); resetUserPassword( request: any, username: string, diff --git a/src/common/decorators/getTenantId.decorator.ts b/src/common/decorators/getTenantId.decorator.ts new file mode 100644 index 00000000..f436d0ab --- /dev/null +++ b/src/common/decorators/getTenantId.decorator.ts @@ -0,0 +1,27 @@ +import { createParamDecorator, ExecutionContext, BadRequestException } from '@nestjs/common'; +import { isUUID } from 'class-validator'; + +export const GetTenantId = createParamDecorator( + (data: unknown, ctx: ExecutionContext): string => { + const request = ctx.switchToHttp().getRequest(); + const tenantId = request.headers.tenantid; + + // Check if tenantId is present + if (!tenantId) { + throw new BadRequestException('tenantid header is required'); + } + + // Check if tenantId is a non-empty string + if (typeof tenantId !== 'string' || tenantId.trim().length === 0) { + throw new BadRequestException('tenantid must be a non-empty string'); + } + + // Check if tenantId is a valid UUID format + if (!isUUID(tenantId)) { + throw new BadRequestException('tenantid must be a valid UUID format'); + } + + return tenantId; + }, +); + diff --git a/src/common/utils/api-id.config.ts b/src/common/utils/api-id.config.ts index db03fde4..3174cae6 100644 --- a/src/common/utils/api-id.config.ts +++ b/src/common/utils/api-id.config.ts @@ -4,6 +4,7 @@ export const APIID = { SUGGEST_USERNAME: "api.suggest.username", USER_UPDATE: "api.user.update", USER_LIST: "api.user.list", + USER_HIERARCHY_VIEW: "api.user.hierarchyView", USER_RESET_PASSWORD: "api.user.resetPassword", USER_RESET_PASSWORD_LINK: "api.user.sendLinkForResetPassword", USER_FORGOT_PASSWORD: "api.user.forgotPassword", diff --git a/src/common/utils/response.messages.ts b/src/common/utils/response.messages.ts index 64aaa07c..1654ec5d 100644 --- a/src/common/utils/response.messages.ts +++ b/src/common/utils/response.messages.ts @@ -97,6 +97,7 @@ export const API_RESPONSES = { //get User Details USER_GET_SUCCESSFULLY: "User details fetched successfully.", + USER_HIERARCHY_VIEW_SUCCESS: "User hierarchy view fetched successfully.", USER_GET_BY_EMAIL_SUCCESSFULLY: "User details fetched successfully by email", USER_GET_BY_PHONE_SUCCESSFULLY: "User details fetched successfully by phone", USER_GET_BY_USERNAME_SUCCESSFULLY: diff --git a/src/config/tenant.config.ts b/src/config/tenant.config.ts new file mode 100644 index 00000000..ee2cdecb --- /dev/null +++ b/src/config/tenant.config.ts @@ -0,0 +1,30 @@ +export interface TenantConfig { + tenantId: string; + name: string; +} + +export const ALLOWED_TENANTS: TenantConfig[] = [ + { + tenantId: 'e39447df-069d-4ccf-b92c-576f70b350f3', + name: 'Pratham' + } +]; + +/** + * Check if a tenant ID is allowed + * @param tenantId - Tenant ID to validate + * @returns boolean indicating if tenant is allowed + */ +export function isAllowedTenant(tenantId: string): boolean { + return ALLOWED_TENANTS.some(tenant => tenant.tenantId === tenantId); +} + +/** + * Get tenant configuration by ID + * @param tenantId - Tenant ID to lookup + * @returns TenantConfig or undefined if not found + */ +export function getTenantConfig(tenantId: string): TenantConfig | undefined { + return ALLOWED_TENANTS.find(tenant => tenant.tenantId === tenantId); +} + diff --git a/src/user/dto/user-search.dto.ts b/src/user/dto/user-search.dto.ts index d7406477..cb84329b 100644 --- a/src/user/dto/user-search.dto.ts +++ b/src/user/dto/user-search.dto.ts @@ -298,6 +298,16 @@ export class UserSearchDto { return this.sort ? this.sort[1] : undefined; } + @ApiPropertyOptional({ + type: String, + description: "Include custom fields in response (default: true). Set to false for faster response.", + default: "true", + }) + @Expose() + @IsOptional() + @IsString() + includeCustomFields?: string = "true"; + constructor(partial: Partial) { Object.assign(this, partial); } diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 3b1f3216..e24917de 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -55,6 +55,8 @@ import { OtpSendDTO } from "./dto/otpSend.dto"; import { OtpVerifyDTO } from "./dto/otpVerify.dto"; import { UploadS3Service } from "src/common/services/upload-S3.service"; import { GetUserId } from "src/common/decorators/getUserId.decorator"; +import { GetTenantId } from "src/common/decorators/getTenantId.decorator"; +import { isAllowedTenant, getTenantConfig } from "src/config/tenant.config"; export interface UserData { context: string; tenantId: string; @@ -181,8 +183,8 @@ export class UserController { @UseFilters(new AllExceptionsFilter(APIID.USER_LIST)) @Post("/list") - // @UseGuards(JwtAuthGuard) - // @ApiBasicAuth("access-token") + @UseGuards(JwtAuthGuard) + @ApiBasicAuth("access-token") @ApiCreatedResponse({ description: "User list." }) @ApiBody({ type: UserSearchDto }) @UsePipes(ValidationPipe) @@ -192,21 +194,14 @@ export class UserController { @ApiHeader({ name: "tenantid", }) - @ApiQuery({ - name: "includeCustomFields", - description: "Include custom fields in response (default: true). Set to false for faster response.", - required: false, - type: Boolean, - }) public async searchUser( @Headers() headers, @Req() request: Request, @Res() response: Response, - @Body() userSearchDto: UserSearchDto, - @Query("includeCustomFields") includeCustomFields: string = "true" + @Body() userSearchDto: UserSearchDto ) { const tenantId = headers["tenantid"]; - const shouldIncludeCustomFields = includeCustomFields !== "false"; + const shouldIncludeCustomFields = userSearchDto.includeCustomFields !== "false"; return await this.userAdapter .buildUserAdapter() .searchUser(tenantId, request, response, userSearchDto, shouldIncludeCustomFields); @@ -231,6 +226,44 @@ export class UserController { ); } + @UseFilters(new AllExceptionsFilter(APIID.USER_HIERARCHY_VIEW)) + @Post("/user/v1/users-hierarchy-view") + @UseGuards(JwtAuthGuard) + @ApiBasicAuth("access-token") + @ApiCreatedResponse({ description: "Multi-tenant user list." }) + @ApiForbiddenResponse({ description: "Tenant is not authorized to access this resource." }) + @ApiBody({ type: UserSearchDto }) + @UsePipes(ValidationPipe) + @SerializeOptions({ + strategy: "excludeAll", + }) + @ApiHeader({ + name: "tenantid", + required: true, + description: "Tenant ID (must be a valid UUID)", + }) + public async searchUserMultiTenant( + @GetTenantId() tenantId: string, + @Req() request: Request, + @Res() response: Response, + @Body() userSearchDto: UserSearchDto + ) { + // Check if tenant ID is in the allowed list + if (!isAllowedTenant(tenantId)) { + const tenantConfig = getTenantConfig(tenantId); + return response.status(403).json({ + statusCode: 403, + message: "Access denied. Tenant is not authorized to access this resource.", + error: "Forbidden", + tenantId: tenantId + }); + } + + return await this.userAdapter + .buildUserAdapter() + .searchUserMultiTenant(request, response, userSearchDto); + } + @Post("/forgot-password") @ApiOkResponse({ description: "Forgot password reset successfully." }) @ApiBody({ type: ForgotPasswordDto }) From 667d6a1abc12bebbd8377e24aac4b926b444a4c2 Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Thu, 6 Nov 2025 19:45:02 +0530 Subject: [PATCH 3/4] add --- src/adapters/postgres/user-adapter.ts | 96 ++++++++++++++++--- .../postgres/userTenantMapping-adapter.ts | 3 +- 2 files changed, 86 insertions(+), 13 deletions(-) diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 748ebf90..018f591e 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -50,6 +50,7 @@ import { randomInt } from 'crypto'; import { UUID } from "aws-sdk/clients/cloudtrail"; import { AutomaticMemberService } from "src/automatic-member/automatic-member.service"; import { KafkaService } from "src/kafka/kafka.service"; +import { isAllowedTenant } from "src/config/tenant.config"; interface UpdateField { userId: string; // Required @@ -1954,27 +1955,98 @@ export class PostgresUserService implements IServicelocator { } - async assignUserToTenantAndRoll(tenantsData, createdBy) { + async assignUserToTenantAndRoll(tenantsData, createdBy, userType?: string) { try { const tenantId = tenantsData?.tenantRoleMapping?.tenantId; const userId = tenantsData?.userId; const roleId = tenantsData?.tenantRoleMapping?.roleId; if (roleId) { - const data = await this.userRoleMappingRepository.save({ - userId: userId, - tenantId: tenantId, - roleId: roleId, - createdBy: createdBy, - }); + // Check if userType is 'assignTenant' and handle accordingly + if (userType === 'assignedUserToChildTenant') { + // Find existing role mapping for this user + const existingRoleMapping = await this.userRoleMappingRepository.findOne({ + where: { userId: userId }, + }); + + if (existingRoleMapping) { + // Check if existing tenantId matches the config file tenant + if (isAllowedTenant(existingRoleMapping.tenantId)) { + // Update existing mapping with new tenantId and roleId + existingRoleMapping.tenantId = tenantId; + existingRoleMapping.roleId = roleId; + existingRoleMapping.createdBy = createdBy; + await this.userRoleMappingRepository.save(existingRoleMapping); + LoggerUtil.log(`Updated role mapping for user ${userId} from tenant ${existingRoleMapping.tenantId} to ${tenantId}`); + } else { + // Create new mapping if existing tenant is not in config + await this.userRoleMappingRepository.save({ + userId: userId, + tenantId: tenantId, + roleId: roleId, + createdBy: createdBy, + }); + } + } else { + // No existing mapping, create new one + await this.userRoleMappingRepository.save({ + userId: userId, + tenantId: tenantId, + roleId: roleId, + createdBy: createdBy, + }); + } + } else { + // Default behavior - create new mapping + const data = await this.userRoleMappingRepository.save({ + userId: userId, + tenantId: tenantId, + roleId: roleId, + createdBy: createdBy, + }); + } } if (tenantId) { - const data = await this.userTenantMappingRepository.save({ - userId: userId, - tenantId: tenantId, - createdBy: createdBy, - }); + // Check if userType is 'assignTenant' and handle accordingly + if (userType === 'assignedUserToChildTenant') { + // Find existing tenant mapping for this user + const existingMapping = await this.userTenantMappingRepository.findOne({ + where: { userId: userId }, + }); + + if (existingMapping) { + // Check if existing tenantId matches the config file tenant + if (isAllowedTenant(existingMapping.tenantId)) { + // Update existing mapping with new tenantId + existingMapping.tenantId = tenantId; + existingMapping.createdBy = createdBy; + await this.userTenantMappingRepository.save(existingMapping); + LoggerUtil.log(`Updated tenant mapping for user ${userId} from ${existingMapping.tenantId} to ${tenantId}`); + } else { + // Create new mapping if existing tenant is not in config + await this.userTenantMappingRepository.save({ + userId: userId, + tenantId: tenantId, + createdBy: createdBy, + }); + } + } else { + // No existing mapping, create new one + await this.userTenantMappingRepository.save({ + userId: userId, + tenantId: tenantId, + createdBy: createdBy, + }); + } + } else { + // Default behavior - create new mapping + const data = await this.userTenantMappingRepository.save({ + userId: userId, + tenantId: tenantId, + createdBy: createdBy, + }); + } } LoggerUtil.log(API_RESPONSES.USER_TENANT); diff --git a/src/adapters/postgres/userTenantMapping-adapter.ts b/src/adapters/postgres/userTenantMapping-adapter.ts index c1646def..383854d7 100644 --- a/src/adapters/postgres/userTenantMapping-adapter.ts +++ b/src/adapters/postgres/userTenantMapping-adapter.ts @@ -141,7 +141,8 @@ export class PostgresAssignTenantService await this.postgresUserService.assignUserToTenantAndRoll( tenantsData, - request["user"].userId + request["user"].userId, + 'assignedUserToChildTenant' ); LoggerUtil.log( From 1d669ec53757662d3b236fe6912c2bff54468ac8 Mon Sep 17 00:00:00 2001 From: souravbhowmik1999 Date: Fri, 7 Nov 2025 11:02:01 +0530 Subject: [PATCH 4/4] add_global_custom_field --- src/adapters/postgres/fields-adapter.ts | 132 ++++++++++++------ src/adapters/postgres/user-adapter.ts | 5 +- src/adapters/userservicelocator.ts | 1 + src/fields/fields.module.ts | 3 +- src/forms/forms.module.ts | 3 +- src/sso/sso.module.ts | 4 +- src/user/user.controller.ts | 40 +----- .../entities/tenant.entity.ts | 3 + 8 files changed, 112 insertions(+), 79 deletions(-) diff --git a/src/adapters/postgres/fields-adapter.ts b/src/adapters/postgres/fields-adapter.ts index d0422ced..7c0c8964 100644 --- a/src/adapters/postgres/fields-adapter.ts +++ b/src/adapters/postgres/fields-adapter.ts @@ -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, @InjectRepository(FieldValues) - private fieldsValuesRepository: Repository + private fieldsValuesRepository: Repository, + @InjectRepository(Tenants) + private tenantsRepository: Repository ) { } async getFormCustomField(requiredData, response) { @@ -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") { @@ -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; @@ -1990,7 +2017,8 @@ export class PostgresFieldsService implements IServicelocatorfields { */ public async getBulkCustomFieldDetails( itemIds: string[], - tableName: string + tableName: string, + tenantId?: string ): Promise> { if (!itemIds || itemIds.length === 0) { return {}; @@ -2004,28 +2032,50 @@ export class PostgresFieldsService implements IServicelocatorfields { } try { - // Single query to fetch all custom fields for all items + 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" - FROM public."${tableName}" u - LEFT JOIN ( - SELECT DISTINCT ON (fv."fieldId", fv."itemId") fv.* - FROM public."FieldValues" fv - WHERE fv."itemId" = ANY($1) - ) fv ON ${joinCond} - INNER JOIN public."Fields" f ON fv."fieldId" = f."fieldId" - WHERE fv."itemId" = ANY($1) - ORDER BY fv."itemId", f."fieldId"; - `; - - let results = await this.fieldsRepository.query(query, [itemIds]); + 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( diff --git a/src/adapters/postgres/user-adapter.ts b/src/adapters/postgres/user-adapter.ts index 018f591e..6c4ba121 100644 --- a/src/adapters/postgres/user-adapter.ts +++ b/src/adapters/postgres/user-adapter.ts @@ -409,6 +409,7 @@ export class PostgresUserService implements IServicelocator { * Calls the existing searchUser function */ async searchUserMultiTenant( + tenantId: string, request: any, response: any, userSearchDto: UserSearchDto @@ -428,7 +429,7 @@ export class PostgresUserService implements IServicelocator { } // Fetch and assign custom fields for each user for (let user of searchUserData.getUserDetails) { - const parentTenantCustomFieldData = await this.fieldsService.getCustomFieldDetails(user.userId, 'Users'); + const parentTenantCustomFieldData = await this.fieldsService.getCustomFieldDetails(user.userId, 'Users', false, tenantId); user.customFields = parentTenantCustomFieldData || []; } @@ -634,7 +635,7 @@ export class PostgresUserService implements IServicelocator { // OPTIMIZED: Batch fetch custom fields for all users in one query (instead of N+1 queries) const userIds = userDetails.map(user => user.userId); const bulkCustomFields = await this.fieldsService.getBulkCustomFieldDetails( - userIds, 'Users' + userIds, 'Users',tenantId ); // Map custom fields back to users (in-memory operation - fast!) diff --git a/src/adapters/userservicelocator.ts b/src/adapters/userservicelocator.ts index 1e1cb96c..ac494d87 100644 --- a/src/adapters/userservicelocator.ts +++ b/src/adapters/userservicelocator.ts @@ -29,6 +29,7 @@ export interface IServicelocator { includeCustomFields?: boolean ); searchUserMultiTenant( + tenantId: string, request: any, response: any, userSearchDto: UserSearchDto diff --git a/src/fields/fields.module.ts b/src/fields/fields.module.ts index 3ade8fbf..0e2f6d24 100644 --- a/src/fields/fields.module.ts +++ b/src/fields/fields.module.ts @@ -7,10 +7,11 @@ import { FieldValues } from "./entities/fields-values.entity"; import { TypeOrmModule } from "@nestjs/typeorm"; import { PostgresModule } from "src/adapters/postgres/postgres-module"; import { FieldsService } from "./fields.service"; +import { Tenants } from "src/userTenantMapping/entities/tenant.entity"; @Module({ imports: [ - TypeOrmModule.forFeature([Fields, FieldValues]), + TypeOrmModule.forFeature([Fields, FieldValues, Tenants]), HttpModule, PostgresModule, ], diff --git a/src/forms/forms.module.ts b/src/forms/forms.module.ts index b10b699f..216089cf 100644 --- a/src/forms/forms.module.ts +++ b/src/forms/forms.module.ts @@ -6,10 +6,11 @@ import { TypeOrmModule } from "@nestjs/typeorm"; import { PostgresFieldsService } from "src/adapters/postgres/fields-adapter"; import { Fields } from "src/fields/entities/fields.entity"; import { FieldValues } from "src/fields/entities/fields-values.entity"; +import { Tenants } from "src/userTenantMapping/entities/tenant.entity"; @Module({ controllers: [FormsController], - imports: [TypeOrmModule.forFeature([Form, Fields, FieldValues])], + imports: [TypeOrmModule.forFeature([Form, Fields, FieldValues, Tenants])], providers: [FormsService, PostgresFieldsService], }) export class FormsModule {} diff --git a/src/sso/sso.module.ts b/src/sso/sso.module.ts index 666a7082..dfd465a2 100644 --- a/src/sso/sso.module.ts +++ b/src/sso/sso.module.ts @@ -14,6 +14,7 @@ import { FieldValues } from '../fields/entities/fields-values.entity'; import { PostgresModule } from '../adapters/postgres/postgres-module'; import { PostgresRoleService } from '../adapters/postgres/rbac/role-adapter'; import { PostgresFieldsService } from '../adapters/postgres/fields-adapter'; +import { Tenants } from '../userTenantMapping/entities/tenant.entity'; @Module({ imports: [ @@ -26,7 +27,8 @@ import { PostgresFieldsService } from '../adapters/postgres/fields-adapter'; UserRoleMapping, RolePrivilegeMapping, Fields, - FieldValues + FieldValues, + Tenants ]) ], controllers: [SsoController], diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index e24917de..734fe205 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -193,14 +193,15 @@ export class UserController { }) @ApiHeader({ name: "tenantid", + required: true, + description: "Tenant ID (must be a valid UUID)", }) public async searchUser( - @Headers() headers, + @GetTenantId() tenantId: string, @Req() request: Request, @Res() response: Response, @Body() userSearchDto: UserSearchDto ) { - const tenantId = headers["tenantid"]; const shouldIncludeCustomFields = userSearchDto.includeCustomFields !== "false"; return await this.userAdapter .buildUserAdapter() @@ -261,7 +262,7 @@ export class UserController { return await this.userAdapter .buildUserAdapter() - .searchUserMultiTenant(request, response, userSearchDto); + .searchUserMultiTenant(tenantId, request, response, userSearchDto); } @Post("/forgot-password") @@ -426,42 +427,15 @@ export class UserController { }) @ApiHeader({ name: "tenantid", - description: "Tenant ID for filtering users within specific tenant (Required)", - required: true + required: true, + description: "Tenant ID (must be a valid UUID)", }) public async getUsersByHierarchicalLocation( - @Headers() headers, + @GetTenantId() tenantId: string, @Req() request: Request, @Res() response: Response, @Body() hierarchicalFiltersDto: HierarchicalLocationFiltersDto ) { - const tenantId = headers["tenantid"]; - const apiId = APIID.USER_LIST; - - // Comprehensive tenantId validation - const tenantValidation = this.validateTenantId(tenantId); - if (!tenantValidation.isValid) { - LoggerUtil.error( - `TenantId validation failed: ${tenantValidation.error}`, - `Received tenantId: ${tenantId}`, - apiId - ); - - return response.status(400).json({ - id: apiId, - ver: "1.0", - ts: new Date().toISOString(), - params: { - resmsgid: "", - status: "failed", - err: tenantValidation.error, - errmsg: "Invalid tenant information" - }, - responseCode: 400, - result: {} - }); - } - return await this.userAdapter .buildUserAdapter() .getUsersByHierarchicalLocation(tenantId, request, response, hierarchicalFiltersDto); diff --git a/src/userTenantMapping/entities/tenant.entity.ts b/src/userTenantMapping/entities/tenant.entity.ts index 90fd21fd..b223121c 100644 --- a/src/userTenantMapping/entities/tenant.entity.ts +++ b/src/userTenantMapping/entities/tenant.entity.ts @@ -30,6 +30,9 @@ export class Tenants { }) status: TenantStatus; + @Column({ type: "uuid", nullable: true }) + parentId: string; + @Column({ type: "uuid", nullable: true }) createdBy: string;