import { UseFilters, UseGuards, UsePipes } from '@nestjs/common'; import { Args, Context, Mutation, Parent, Query, ResolveField, } from '@nestjs/graphql'; import { PermissionFlagType } from 'twenty-shared/constants'; import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; import { fromFlatObjectMetadataToObjectMetadataDto } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util'; import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto'; import { CreateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input'; import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto'; import { ObjectRecordCountDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-record-count.dto'; import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; import { getEffectiveImageIdentifierFieldMetadataId } from 'src/engine/metadata-modules/object-metadata/utils/get-effective-image-identifier-field-metadata-id.util'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { ObjectRecordCountService } from 'src/engine/metadata-modules/object-metadata/object-record-count.service'; import { objectMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { SearchFieldMetadataDTO } from 'src/engine/metadata-modules/search-field-metadata/dtos/search-field-metadata.dto'; import { resolveEffectiveEntityProperty } from 'src/engine/metadata-modules/utils/resolve-effective-entity-property.util'; @UseGuards(WorkspaceAuthGuard) @MetadataResolver(() => ObjectMetadataDTO) @UsePipes(ResolverValidationPipe) @UseFilters( PreventNestToAutoLogGraphqlErrorsFilter, PermissionsGraphqlApiExceptionFilter, ) export class ObjectMetadataResolver { constructor( private readonly objectMetadataService: ObjectMetadataService, private readonly objectRecordCountService: ObjectRecordCountService, private readonly i18nService: I18nService, ) {} @ResolveField(() => Boolean, { deprecationReason: 'Use isUIEditable', }) async isUIReadOnly( @Parent() objectMetadata: ObjectMetadataDTO, ): Promise { return !objectMetadata.isUIEditable; } @UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL)) @Query(() => [ObjectRecordCountDTO]) async objectRecordCounts( @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { return this.objectRecordCountService.getRecordCounts(workspaceId); } private async resolveStandardOverride( objectMetadata: ObjectMetadataDTO, labelKey: | 'color' | 'labelPlural' | 'labelSingular' | 'description' | 'icon', context: { loaders: IDataloaders } & I18nContext, workspaceId: string, ): Promise { const i18n = this.i18nService.getI18nInstance(context.req.locale); const standardApplicationId = await context.loaders.standardApplicationIdLoader.load({ workspaceId }); const isStandardApp = objectMetadata.applicationId === standardApplicationId; const applicationCatalog = await context.loaders.applicationTranslationCatalogLoader.load({ applicationId: objectMetadata.applicationId, workspaceId, locale: context.req.locale, }); return resolveEffectiveEntityProperty({ metadataName: 'objectMetadata', baseValue: objectMetadata[labelKey], overrides: objectMetadata.overrides, property: labelKey, i18nContext: { locale: context.req.locale, i18nInstance: i18n, isStandardApp, applicationCatalog, }, }); } @ResolveField(() => String, { nullable: true }) async labelPlural( @Parent() objectMetadata: ObjectMetadataDTO, @Context() context: { loaders: IDataloaders } & I18nContext, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { return this.resolveStandardOverride( objectMetadata, 'labelPlural', context, workspaceId, ); } @ResolveField(() => String, { nullable: true }) async labelSingular( @Parent() objectMetadata: ObjectMetadataDTO, @Context() context: { loaders: IDataloaders } & I18nContext, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { return this.resolveStandardOverride( objectMetadata, 'labelSingular', context, workspaceId, ); } @ResolveField(() => String, { nullable: true }) async description( @Parent() objectMetadata: ObjectMetadataDTO, @Context() context: { loaders: IDataloaders } & I18nContext, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { return this.resolveStandardOverride( objectMetadata, 'description', context, workspaceId, ); } @UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL)) @ResolveField(() => String, { nullable: true }) async icon( @Parent() objectMetadata: ObjectMetadataDTO, @Context() context: { loaders: IDataloaders } & I18nContext, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { return this.resolveStandardOverride( objectMetadata, 'icon', context, workspaceId, ); } @ResolveField(() => String, { nullable: true }) async color( @Parent() objectMetadata: ObjectMetadataDTO, @Context() context: { loaders: IDataloaders } & I18nContext, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { return this.resolveStandardOverride( objectMetadata, 'color', context, workspaceId, ); } @ResolveField(() => UUIDScalarType, { nullable: true }) imageIdentifierFieldMetadataId( @Parent() objectMetadata: ObjectMetadataDTO, ): string | null { return getEffectiveImageIdentifierFieldMetadataId(objectMetadata); } @UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL)) @Mutation(() => ObjectMetadataDTO) async createOneObject( @Args('input') input: CreateOneObjectInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ) { try { const flatobjectMetadata = await this.objectMetadataService.createOneObject({ createObjectInput: input.object, workspaceId, }); return fromFlatObjectMetadataToObjectMetadataDto(flatobjectMetadata); } catch (error) { objectMetadataGraphqlApiExceptionHandler(error); } } @UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL)) @Mutation(() => ObjectMetadataDTO) async deleteOneObject( @Args('input') deleteObjectInput: DeleteOneObjectInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ) { try { const flatobjectMetadata = await this.objectMetadataService.deleteOneObject({ deleteObjectInput, workspaceId, }); return fromFlatObjectMetadataToObjectMetadataDto(flatobjectMetadata); } catch (error) { objectMetadataGraphqlApiExceptionHandler(error); } } @UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL)) @Mutation(() => ObjectMetadataDTO) async updateOneObject( @Args('input') updateObjectInput: UpdateOneObjectInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ) { try { const flatobjectMetadata = await this.objectMetadataService.updateOneObject({ updateObjectInput, workspaceId, }); return fromFlatObjectMetadataToObjectMetadataDto(flatobjectMetadata); } catch (error) { objectMetadataGraphqlApiExceptionHandler(error); } } @ResolveField(() => [FieldMetadataDTO], { nullable: false }) async fieldsList( @AuthWorkspace() workspace: WorkspaceEntity, @Parent() objectMetadata: ObjectMetadataDTO, @Context() context: { loaders: IDataloaders } & I18nContext, ): Promise { try { const fieldMetadataItems = await context.loaders.fieldMetadataLoader.load( { objectMetadata, workspaceId: workspace.id, locale: context.req.locale, }, ); return fieldMetadataItems; } catch (error) { objectMetadataGraphqlApiExceptionHandler(error); return []; } } @ResolveField(() => [IndexMetadataDTO], { nullable: false }) async indexMetadataList( @AuthWorkspace() workspace: WorkspaceEntity, @Parent() objectMetadata: ObjectMetadataDTO, @Context() context: { loaders: IDataloaders }, ): Promise { try { const indexMetadataItems = await context.loaders.indexMetadataLoader.load( { objectMetadata, workspaceId: workspace.id, }, ); return indexMetadataItems; } catch (error) { objectMetadataGraphqlApiExceptionHandler(error); return []; } } @ResolveField(() => [SearchFieldMetadataDTO], { nullable: false }) async searchFieldMetadataList( @AuthWorkspace() workspace: WorkspaceEntity, @Parent() objectMetadata: ObjectMetadataDTO, @Context() context: { loaders: IDataloaders }, ): Promise { try { const searchFieldMetadataItems = await context.loaders.searchFieldMetadataLoader.load({ objectMetadata, workspaceId: workspace.id, }); return searchFieldMetadataItems; } catch (error) { objectMetadataGraphqlApiExceptionHandler(error); return []; } } }