import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Patch, Post, Put, Req, UseFilters, UseGuards, UsePipes, ValidationPipe, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { PermissionFlagType } from 'twenty-shared/constants'; import { FeatureFlagKey } from 'twenty-shared/types'; import { Repository } from 'typeorm'; import { parseEndingBeforeRestRequest } from 'src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util'; import { parseLimitRestRequest } from 'src/engine/api/rest/input-request-parsers/limit-parser-utils/parse-limit-rest-request.util'; import { parseStartingAfterRestRequest } from 'src/engine/api/rest/input-request-parsers/starting-after-parser-utils/parse-starting-after-rest-request.util'; import { paginateByIdCursor, type RestCursorPageInfo, } from 'src/engine/api/rest/metadata/utils/paginate-by-id-cursor.util'; import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; import { ApplicationRestApiExceptionFilter } from 'src/engine/core-modules/application/application-rest-api-exception.filter'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard'; import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; import { UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataException, FieldMetadataExceptionCode, } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { FieldMetadataRestApiExceptionFilter } from 'src/engine/metadata-modules/field-metadata/filters/field-metadata-rest-api-exception.filter'; import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service'; import { fromFieldMetadataEntityToFieldMetadataDto } from 'src/engine/metadata-modules/field-metadata/utils/from-field-metadata-entity-to-field-metadata-dto.util'; import { toLegacyFieldMetadataCreateResponse, toLegacyFieldMetadataDeleteResponse, toLegacyFieldMetadataFindOneResponse, toLegacyFieldMetadataListResponse, toLegacyFieldMetadataUpdateResponse, } from 'src/engine/metadata-modules/field-metadata/utils/to-legacy-field-metadata-response.util'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util'; import { computeUniqueFieldMetadataIdsFromFlatIndexMaps } from 'src/engine/metadata-modules/index-metadata/utils/compute-unique-field-metadata-ids-from-flat-index-maps.util'; import { PermissionsRestApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-rest-api-exception.filter'; @Controller('rest/metadata/fields') @UseGuards( JwtAuthGuard, WorkspaceAuthGuard, SettingsPermissionGuard(PermissionFlagType.DATA_MODEL), ) @UseFilters( PermissionsRestApiExceptionFilter, FieldMetadataRestApiExceptionFilter, ApplicationRestApiExceptionFilter, ) @UsePipes(new ValidationPipe()) export class FieldMetadataController { constructor( @InjectRepository(FieldMetadataEntity) private readonly fieldMetadataRepository: Repository, private readonly fieldMetadataService: FieldMetadataService, private readonly featureFlagService: FeatureFlagService, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} private async loadUniqueFieldMetadataIds( workspaceId: string, ): Promise> { const { flatIndexMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, flatMapsKeys: ['flatIndexMaps'] }, ); return computeUniqueFieldMetadataIdsFromFlatIndexMaps(flatIndexMaps); } @Get() async findMany( @Req() request: AuthenticatedRequest, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ) { const { items, pageInfo, totalCount } = await paginateByIdCursor({ repository: this.fieldMetadataRepository, workspaceId, limit: parseLimitRestRequest(request), startingAfter: parseStartingAfterRestRequest(request), endingBefore: parseEndingBeforeRestRequest(request), }); const uniqueFieldMetadataIds = await this.loadUniqueFieldMetadataIds(workspaceId); const result: { data: FieldMetadataDTO[]; pageInfo: RestCursorPageInfo; totalCount: number; } = { data: items.map((item) => fromFieldMetadataEntityToFieldMetadataDto(item, uniqueFieldMetadataIds), ), pageInfo, totalCount, }; return (await this.isNewMetadataFormat(workspaceId)) ? result : toLegacyFieldMetadataListResponse(result); } @Get(':id') async findOne( @Param('id', new ParseUUIDPipe()) id: string, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ) { const field = await this.fieldMetadataRepository.findOne({ where: { id, workspaceId }, }); if (!field) { throw new FieldMetadataException( 'Field metadata not found', FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND, ); } const uniqueFieldMetadataIds = await this.loadUniqueFieldMetadataIds(workspaceId); const result = fromFieldMetadataEntityToFieldMetadataDto( field, uniqueFieldMetadataIds, ); return (await this.isNewMetadataFormat(workspaceId)) ? result : toLegacyFieldMetadataFindOneResponse(result); } @Post() async createOne( @Body() input: CreateFieldInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ) { const flatField = await this.fieldMetadataService.createOneField({ createFieldInput: input, workspaceId, }); const result = fromFlatFieldMetadataToFieldMetadataDto(flatField); return (await this.isNewMetadataFormat(workspaceId)) ? result : toLegacyFieldMetadataCreateResponse(result); } @Patch(':id') async updateOnePatch( @Param('id', new ParseUUIDPipe()) id: string, @Body() update: UpdateFieldInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ) { return this.handleUpdate({ id, update, workspaceId }); } @Put(':id') async updateOnePut( @Param('id', new ParseUUIDPipe()) id: string, @Body() update: UpdateFieldInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ) { return this.handleUpdate({ id, update, workspaceId }); } @Delete(':id') async deleteOne( @Param('id', new ParseUUIDPipe()) id: string, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ) { const flatField = await this.fieldMetadataService.deleteOneField({ deleteOneFieldInput: { id }, workspaceId, }); const result = fromFlatFieldMetadataToFieldMetadataDto(flatField); return (await this.isNewMetadataFormat(workspaceId)) ? result : toLegacyFieldMetadataDeleteResponse(result); } private async handleUpdate({ id, update, workspaceId, }: { id: string; update: UpdateFieldInput; workspaceId: string; }) { const flatField = await this.fieldMetadataService.updateOneField({ updateFieldInput: { ...update, id }, workspaceId, }); const result = fromFlatFieldMetadataToFieldMetadataDto(flatField); return (await this.isNewMetadataFormat(workspaceId)) ? result : toLegacyFieldMetadataUpdateResponse(result); } private async isNewMetadataFormat(workspaceId: string): Promise { return this.featureFlagService.isFeatureEnabled( FeatureFlagKey.IS_REST_METADATA_API_NEW_FORMAT_DIRECT, workspaceId, ); } }