diff --git a/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-arg-handlers.ts b/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-arg-handlers.ts new file mode 100644 index 000000000000..7b3b26e558a5 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-arg-handlers.ts @@ -0,0 +1,3 @@ +import { CommonSelectedFieldsHandler } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler'; + +export const CommonArgsHandlers = [CommonSelectedFieldsHandler]; diff --git a/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler.ts b/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler.ts new file mode 100644 index 000000000000..aae9985d9e31 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler.ts @@ -0,0 +1,140 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; + +import { FieldMetadataType, ObjectsPermissions } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type'; +import { + Depth, + MAX_DEPTH, +} from 'src/engine/api/rest/input-factories/depth-input.factory'; +import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; + +@Injectable() +export class CommonSelectedFieldsHandler { + computeFromDepth = ({ + objectsPermissions, + objectMetadataMaps, + objectMetadataMapItem, + depth, + }: { + objectsPermissions: ObjectsPermissions; + objectMetadataMaps: ObjectMetadataMaps; + objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + depth: Depth | undefined; + }): CommonSelectedFieldsResult => { + const restrictedFields = + objectsPermissions[objectMetadataMapItem.id].restrictedFields; + + const { relations, relationsSelectFields } = + this.getRelationsAndRelationsSelectFields({ + objectMetadataMaps, + objectMetadataMapItem, + objectsPermissions, + depth, + }); + + const selectableFields = getAllSelectableFields({ + restrictedFields, + objectMetadata: { + objectMetadataMapItem, + }, + }); + + return { + select: { + ...selectableFields, + ...relationsSelectFields, + }, + relations, + aggregate: {}, + }; + }; + + private getRelationsAndRelationsSelectFields({ + objectMetadataMaps, + objectMetadataMapItem, + objectsPermissions, + depth, + }: { + objectMetadataMaps: ObjectMetadataMaps; + objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + objectsPermissions: ObjectsPermissions; + depth: Depth | undefined; + }) { + if (!isDefined(depth) || depth === 0) { + return { + relations: {}, + relationsSelectFields: {}, + }; + } + + let relations: { [key: string]: boolean | { [key: string]: boolean } } = {}; + + let relationsSelectFields: { + [key: string]: + | boolean + | { [key: string]: boolean | { [key: string]: boolean } }; + } = {}; + + for (const field of Object.values(objectMetadataMapItem.fieldsById)) { + if (!isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION)) + continue; + + const relationTargetObjectMetadata = + objectMetadataMaps.byId[field.relationTargetObjectMetadataId]; + + if (!isDefined(relationTargetObjectMetadata)) { + throw new BadRequestException( + `Object metadata relation target not found for relation creation payload`, + ); + } + const relationFieldSelectFields = getAllSelectableFields({ + restrictedFields: + objectsPermissions[relationTargetObjectMetadata.id].restrictedFields, + objectMetadata: { + objectMetadataMapItem: relationTargetObjectMetadata, + }, + }); + + if (Object.keys(relationFieldSelectFields).length === 0) continue; + + if ( + depth === MAX_DEPTH && + isDefined(field.relationTargetObjectMetadataId) + ) { + const { + relations: depth2Relations, + relationsSelectFields: depth2RelationsSelectFields, + } = this.getRelationsAndRelationsSelectFields({ + objectMetadataMaps, + objectMetadataMapItem: relationTargetObjectMetadata, + objectsPermissions, + depth: 1, + }) as { + relations: { [key: string]: boolean }; + relationsSelectFields: { + [key: string]: boolean; + }; + }; + + relations[field.name] = depth2Relations as { + [key: string]: boolean; + }; + + relationsSelectFields[field.name] = { + ...relationFieldSelectFields, + ...depth2RelationsSelectFields, + }; + } else { + relations[field.name] = true; + relationsSelectFields[field.name] = relationFieldSelectFields; + } + } + + return { relations, relationsSelectFields }; + } +} diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts new file mode 100644 index 000000000000..1b19a1ab5915 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts @@ -0,0 +1,257 @@ +import { Inject, Injectable } from '@nestjs/common'; + +import { ObjectsPermissions } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; +import { type ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; +import { type IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface'; +import { type IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface'; + +import { CommonSelectedFieldsHandler } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler'; +import { + CommonQueryNames, + RawSelectedFields, +} from 'src/engine/api/common/types/common-query-args.type'; +import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type'; +import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements'; +import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; +import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; +import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory'; +import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory'; +import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service'; +import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service'; +import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; +import { + PermissionsException, + PermissionsExceptionCode, + PermissionsExceptionMessage, +} from 'src/engine/metadata-modules/permissions/permissions.exception'; +import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; +import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; + +@Injectable() +export abstract class CommonBaseQueryRunnerService< + Response extends + | ObjectRecord + | ObjectRecord[] + | IConnection> + | IConnection>[], +> { + @Inject() + protected readonly workspaceQueryHookService: WorkspaceQueryHookService; + @Inject() + protected readonly queryRunnerArgsFactory: QueryRunnerArgsFactory; + @Inject() + protected readonly queryResultGettersFactory: QueryResultGettersFactory; + @Inject() + protected readonly twentyORMGlobalManager: TwentyORMGlobalManager; + @Inject() + protected readonly processNestedRelationsHelper: ProcessNestedRelationsHelper; + @Inject() + protected readonly permissionsService: PermissionsService; + @Inject() + protected readonly userRoleService: UserRoleService; + @Inject() + protected readonly apiKeyRoleService: ApiKeyRoleService; + @Inject() + protected readonly selectedFieldsHandler: CommonSelectedFieldsHandler; + @Inject() + protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService; + + public async prepareQueryRunnerContext({ + authContext, + objectMetadataItemWithFieldMaps, + }: { + authContext: WorkspaceAuthContext; + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + }) { + if (objectMetadataItemWithFieldMaps.isSystem === true) { + await this.validateSettingsPermissionsOnObjectOrThrow( + authContext, + objectMetadataItemWithFieldMaps, + ); + } + + const workspace = authContext.workspace; + + const workspaceDataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace({ + workspaceId: workspace.id, + }); + + const { roleId, objectsPermissions } = + await this.getRoleIdAndObjectsPermissions(authContext, workspace.id); + + const repository = workspaceDataSource.getRepository( + objectMetadataItemWithFieldMaps.nameSingular, + false, + roleId, + authContext, + ); + + return { + workspaceDataSource, + repository, + isExecutedByApiKey: isDefined(authContext.apiKey), + roleId, + shouldBypassPermissionChecks: false, + objectsPermissions, + }; + } + + public async computeSelectedFields({ + rawSelectedFields, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + objectsPermissions, + }: { + rawSelectedFields: RawSelectedFields; + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + objectMetadataMaps: ObjectMetadataMaps; + objectsPermissions: ObjectsPermissions; + }): Promise { + if (isDefined(rawSelectedFields.graphqlSelectedFields)) { + const graphqlQueryParser = new GraphqlQueryParser( + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + ); + + //TODO : Refacto-common - QueryParser should be moved to selected fields handler + return graphqlQueryParser.parseSelectedFields( + objectMetadataItemWithFieldMaps, + rawSelectedFields.graphqlSelectedFields, + objectMetadataMaps, + ); + } + + return this.selectedFieldsHandler.computeFromDepth({ + objectsPermissions, + objectMetadataMaps, + objectMetadataMapItem: objectMetadataItemWithFieldMaps, + depth: rawSelectedFields.depth, + }); + } + + public async enrichResultsWithGettersAndHooks({ + results, + operationName, + authContext, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + }: { + results: Response; + operationName: CommonQueryNames; + authContext: WorkspaceAuthContext; + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + objectMetadataMaps: ObjectMetadataMaps; + }) { + const resultWithGetters = await this.queryResultGettersFactory.create( + results, + objectMetadataItemWithFieldMaps, + authContext.workspace.id, + objectMetadataMaps, + ); + + await this.workspaceQueryHookService.executePostQueryHooks( + authContext, + objectMetadataItemWithFieldMaps.nameSingular, + operationName, + resultWithGetters, + ); + + return resultWithGetters; + } + + private async validateSettingsPermissionsOnObjectOrThrow( + authContext: WorkspaceAuthContext, + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + ) { + const workspace = authContext.workspace; + + if ( + Object.keys(OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS).includes( + objectMetadataItemWithFieldMaps.nameSingular, + ) + ) { + const permissionRequired: PermissionFlagType = + OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS[ + objectMetadataItemWithFieldMaps.nameSingular as keyof typeof OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS + ]; + + const userHasPermission = + await this.permissionsService.userHasWorkspaceSettingPermission({ + userWorkspaceId: authContext.userWorkspaceId, + setting: permissionRequired, + workspaceId: workspace.id, + apiKeyId: authContext.apiKey?.id, + }); + + if (!userHasPermission) { + throw new PermissionsException( + PermissionsExceptionMessage.PERMISSION_DENIED, + PermissionsExceptionCode.PERMISSION_DENIED, + ); + } + } + } + + private async getRoleIdAndObjectsPermissions( + authContext: AuthContext, + workspaceId: string, + ) { + let roleId: string; + + if ( + !isDefined(authContext.apiKey) && + !isDefined(authContext.userWorkspaceId) + ) { + throw new PermissionsException( + PermissionsExceptionMessage.NO_AUTHENTICATION_CONTEXT, + PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT, + ); + } + + if (isDefined(authContext.apiKey)) { + roleId = await this.apiKeyRoleService.getRoleIdForApiKey( + authContext.apiKey.id, + workspaceId, + ); + } else { + const userWorkspaceRoleId = + await this.userRoleService.getRoleIdForUserWorkspace({ + userWorkspaceId: authContext.userWorkspaceId, + workspaceId, + }); + + if (!isDefined(userWorkspaceRoleId)) { + throw new PermissionsException( + PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE, + PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE, + ); + } + + roleId = userWorkspaceRoleId; + } + + const objectMetadataPermissions = + await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles( + { + workspaceId: workspaceId, + roleIds: roleId ? [roleId] : undefined, + }, + ); + + if (!isDefined(objectMetadataPermissions?.[roleId])) { + throw new InternalServerError('Permissions not found for role'); + } + + return { roleId, objectsPermissions: objectMetadataPermissions?.[roleId] }; + } +} diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts new file mode 100644 index 000000000000..0ae1bdccd6a6 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts @@ -0,0 +1,193 @@ +import { Injectable } from '@nestjs/common'; + +import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; +import { FindOptionsRelations, ObjectLiteral } from 'typeorm'; + +import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; +import { + ObjectRecord, + ObjectRecordFilter, +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; + +import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service'; +import { + CommonQueryRunnerException, + CommonQueryRunnerExceptionCode, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { + CommonQueryNames, + FindOneQueryArgs, + RawSelectedFields, +} from 'src/engine/api/common/types/common-query-args.type'; +import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util'; +import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; +import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; +import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; +import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; + +@Injectable() +export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerService { + async run({ + rawSelectedFields, + args, + authContext: toValidateAuthContext, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + }: { + rawSelectedFields: RawSelectedFields; + args: FindOneQueryArgs; + authContext: AuthContext; + objectMetadataMaps: ObjectMetadataMaps; + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + }): Promise { + const authContext = toValidateAuthContext; + + if (!isWorkspaceAuthContext(authContext)) { + throw new CommonQueryRunnerException( + 'Invalid auth context', + CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT, + ); + } + + const { + workspaceDataSource, + repository, + roleId, + shouldBypassPermissionChecks, + objectsPermissions, + } = await this.prepareQueryRunnerContext({ + authContext, + objectMetadataItemWithFieldMaps, + }); + + const selectedFieldsResult = await this.computeSelectedFields({ + rawSelectedFields, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + objectsPermissions, + }); + + const processedArgs = await this.processQueryArgs({ + authContext, + objectMetadataItemWithFieldMaps, + args, + }); + + if ( + !processedArgs.filter || + Object.keys(processedArgs.filter).length === 0 + ) { + throw new CommonQueryRunnerException( + 'Missing filter argument', + CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); + } + + const queryBuilder = repository.createQueryBuilder( + objectMetadataItemWithFieldMaps.nameSingular, + ); + + //TODO : Refacto-common - QueryParser should be common branded service + const commonQueryParser = new GraphqlQueryParser( + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + ); + + commonQueryParser.applyFilterToBuilder( + queryBuilder, + objectMetadataItemWithFieldMaps.nameSingular, + processedArgs.filter ?? ({} as ObjectRecordFilter), + ); + + commonQueryParser.applyDeletedAtToBuilder( + queryBuilder, + processedArgs.filter ?? ({} as ObjectRecordFilter), + ); + + const columnsToSelect = buildColumnsToSelect({ + select: selectedFieldsResult.select, + relations: selectedFieldsResult.relations, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + }); + + const objectRecord = await queryBuilder + .setFindOptions({ + select: columnsToSelect, + }) + .getOne(); + + if (!objectRecord) { + throw new CommonQueryRunnerException( + 'Record not found', + CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND, + ); + } + + const objectRecords = [objectRecord] as ObjectRecord[]; + + if (selectedFieldsResult.relations) { + await this.processNestedRelationsHelper.processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + parentObjectRecords: objectRecords, + //TODO : Refacto-common - To fix when switching processNestedRelationsHelper to Common + relations: selectedFieldsResult.relations as Record< + string, + FindOptionsRelations + >, + limit: QUERY_MAX_RECORDS, + authContext, + workspaceDataSource, + roleId, + shouldBypassPermissionChecks, + selectedFields: selectedFieldsResult.select, + }); + } + + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); + + const results = typeORMObjectRecordsParser.processRecord({ + objectRecord: objectRecords[0], + objectName: objectMetadataItemWithFieldMaps.nameSingular, + take: 1, + totalCount: 1, + }) as ObjectRecord; + + return this.enrichResultsWithGettersAndHooks({ + results, + authContext, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + operationName: CommonQueryNames.findOne, + }); + } + + async processQueryArgs({ + authContext, + objectMetadataItemWithFieldMaps, + args, + }: { + authContext: WorkspaceAuthContext; + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + args: FindOneQueryArgs; + }): Promise { + const hookedArgs = + await this.workspaceQueryHookService.executePreQueryHooks( + authContext, + objectMetadataItemWithFieldMaps.nameSingular, + CommonQueryNames.findOne, + args, + ); + + return { + filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( + hookedArgs.filter, + objectMetadataItemWithFieldMaps, + ), + }; + } +} diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-query-runners.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-query-runners.ts new file mode 100644 index 000000000000..f946c9616ef5 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-query-runners.ts @@ -0,0 +1,3 @@ +import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service'; + +export const CommonQueryRunners = [CommonFindOneQueryRunnerService]; diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts new file mode 100644 index 000000000000..af4eaaefde06 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/errors/common-query-runner.exception.ts @@ -0,0 +1,9 @@ +import { CustomException } from 'src/utils/custom-exception'; + +export class CommonQueryRunnerException extends CustomException {} + +export enum CommonQueryRunnerExceptionCode { + RECORD_NOT_FOUND = 'RECORD_NOT_FOUND', + INVALID_QUERY_INPUT = 'INVALID_QUERY_INPUT', + INVALID_AUTH_CONTEXT = 'INVALID_AUTH_CONTEXT', +} diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-graphql-api-exception-handler.util.ts new file mode 100644 index 000000000000..f8b2adc008e1 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-graphql-api-exception-handler.util.ts @@ -0,0 +1,27 @@ +import { assertUnreachable } from 'twenty-shared/utils'; + +import { + CommonQueryRunnerExceptionCode, + type CommonQueryRunnerException, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { + AuthenticationError, + NotFoundError, + UserInputError, +} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; + +export const commonQueryRunnerToGraphqlApiExceptionHandler = ( + error: CommonQueryRunnerException, +) => { + switch (error.code) { + case CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND: + throw new NotFoundError(error); + case CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT: + throw new UserInputError(error); + case CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT: + throw new AuthenticationError(error); + default: { + return assertUnreachable(error.code); + } + } +}; diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util.ts new file mode 100644 index 000000000000..8225f598a5ee --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util.ts @@ -0,0 +1,24 @@ +import { BadRequestException, UnauthorizedException } from '@nestjs/common'; + +import { assertUnreachable } from 'twenty-shared/utils'; + +import { + CommonQueryRunnerExceptionCode, + type CommonQueryRunnerException, +} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; + +export const commonQueryRunnerToRestApiExceptionHandler = ( + error: CommonQueryRunnerException, +) => { + switch (error.code) { + case CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT: + throw new BadRequestException(error.message); + case CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND: + throw new BadRequestException('Record not found'); + case CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT: + throw new UnauthorizedException(error.message); + default: { + return assertUnreachable(error.code); + } + } +}; diff --git a/packages/twenty-server/src/engine/api/common/core-common-api.module.ts b/packages/twenty-server/src/engine/api/common/core-common-api.module.ts new file mode 100644 index 000000000000..8acc5b706ee5 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/core-common-api.module.ts @@ -0,0 +1,36 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { CommonArgsHandlers } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-arg-handlers'; +import { CommonQueryRunners } from 'src/engine/api/common/common-query-runners/common-query-runners'; +import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper'; +import { ProcessNestedRelationsV2Helper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper'; +import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; +import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module'; +import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module'; +import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module'; +import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; +import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; +import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; +import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; + +@Module({ + imports: [ + WorkspaceQueryHookModule, + WorkspaceQueryRunnerModule, + PermissionsModule, + TypeOrmModule.forFeature([RoleTargetsEntity]), + UserRoleModule, + ApiKeyModule, + WorkspacePermissionsCacheModule, + ], + providers: [ + ProcessNestedRelationsHelper, + ProcessNestedRelationsV2Helper, + ...CommonArgsHandlers, + ProcessAggregateHelper, + ...CommonQueryRunners, + ], + exports: [...CommonQueryRunners], +}) +export class CoreCommonApiModule {} diff --git a/packages/twenty-server/src/engine/api/common/interfaces/workspace-auth-context.interface.ts b/packages/twenty-server/src/engine/api/common/interfaces/workspace-auth-context.interface.ts new file mode 100644 index 000000000000..468b15fc9b0d --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/interfaces/workspace-auth-context.interface.ts @@ -0,0 +1,21 @@ +import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; + +interface AuthContextWithDefinedWorkspaceProperties { + user: AuthContext['user']; + workspace: NonNullable; + workspaceMetadataVersion?: string; + workspaceMemberId: AuthContext['workspaceMemberId']; +} + +interface ApiKeyAuthContext extends Request { + apiKey: NonNullable; + userWorkspaceId?: undefined; +} + +interface UserWorkspaceAuthContext extends Request { + apiKey?: undefined; + userWorkspaceId: NonNullable; +} + +export type WorkspaceAuthContext = AuthContextWithDefinedWorkspaceProperties & + (ApiKeyAuthContext | UserWorkspaceAuthContext); diff --git a/packages/twenty-server/src/engine/api/common/types/common-query-args.type.ts b/packages/twenty-server/src/engine/api/common/types/common-query-args.type.ts new file mode 100644 index 000000000000..90a2d5819ae1 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/types/common-query-args.type.ts @@ -0,0 +1,18 @@ +import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; + +import { type Depth } from 'src/engine/api/rest/input-factories/depth-input.factory'; + +export enum CommonQueryNames { + findOne = 'findOne', +} + +export type RawSelectedFields = { + graphqlSelectedFields?: Record; + depth?: Depth; +}; + +export interface FindOneQueryArgs { + filter?: ObjectRecordFilter; +} + +export type CommonQueryArgs = FindOneQueryArgs; diff --git a/packages/twenty-server/src/engine/api/common/types/common-selected-fields-result.type.ts b/packages/twenty-server/src/engine/api/common/types/common-selected-fields-result.type.ts new file mode 100644 index 000000000000..baac2e6eab0f --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/types/common-selected-fields-result.type.ts @@ -0,0 +1,9 @@ +interface SelectedFields { + [key: string]: boolean | SelectedFields; +} + +export type CommonSelectedFieldsResult = { + select: SelectedFields; + relations: SelectedFields; + aggregate: SelectedFields; +}; diff --git a/packages/twenty-server/src/engine/api/common/utils/is-workspace-auth-context.util.ts b/packages/twenty-server/src/engine/api/common/utils/is-workspace-auth-context.util.ts new file mode 100644 index 000000000000..c4c5e1d45582 --- /dev/null +++ b/packages/twenty-server/src/engine/api/common/utils/is-workspace-auth-context.util.ts @@ -0,0 +1,14 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; + +import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; + +export const isWorkspaceAuthContext = ( + context: AuthContext, +): context is WorkspaceAuthContext => { + return ( + isDefined(context.workspace) && + (isDefined(context.userWorkspaceId) || isDefined(context.apiKey)) + ); +}; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts index 51332c2a63e6..ab73907d3e34 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts @@ -28,6 +28,7 @@ import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/g import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service'; import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsException, @@ -39,7 +40,6 @@ import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; export type GraphqlQueryResolverExecutionArgs = { args: Input; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts index a29dca361808..a6899aba736d 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts @@ -1,3 +1,4 @@ +//TODO : Refacto-common - To delete import { Injectable } from '@nestjs/common'; import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface.ts index e0a9a167e3fb..6604a8de65ea 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface.ts @@ -1,3 +1,4 @@ +//TODO : Refacto-common - Should be moved to common api layer export interface ObjectRecord { id: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts index 414e4e5515c9..7554f58ae4cc 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts @@ -7,7 +7,7 @@ import { type ObjectRecord, type ObjectRecordFilter, } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { type CreateManyResolverArgs, type CreateOneResolverArgs, @@ -23,9 +23,9 @@ import { import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; +import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; @Injectable() export class QueryRunnerArgsFactory { @@ -214,7 +214,7 @@ export class QueryRunnerArgsFactory { return allOverriddenRecords; } - private overrideFilterByFieldMetadata( + public overrideFilterByFieldMetadata( filter: ObjectRecordFilter | undefined, objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, ) { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface.ts index a3e664ced5d0..6ea833fc9a84 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface.ts @@ -6,7 +6,7 @@ import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/objec export interface WorkspaceQueryRunnerOptions { authContext: AuthContext; - info: GraphQLResolveInfo; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; objectMetadataMaps: ObjectMetadataMaps; + objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + info: GraphQLResolveInfo; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts index 1e07e8d394ee..f3ddbfe4a910 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts @@ -1,5 +1,7 @@ import { type QueryFailedError } from 'typeorm'; +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { commonQueryRunnerToGraphqlApiExceptionHandler } from 'src/engine/api/common/common-query-runners/utils/common-query-runner-to-graphql-api-exception-handler.util'; import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { graphqlQueryRunnerExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/graphql-query-runner-exception-handler.util'; import { workspaceExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-exception-handler.util'; @@ -18,6 +20,7 @@ import { twentyORMGraphqlApiExceptionHandler } from 'src/engine/twenty-orm/utils interface QueryFailedErrorWithCode extends QueryFailedError { code: string; } +//TODO : Refacto-common - Should be handle first in common api layer export const workspaceQueryRunnerGraphqlApiExceptionHandler = ( error: QueryFailedErrorWithCode, @@ -33,6 +36,8 @@ export const workspaceQueryRunnerGraphqlApiExceptionHandler = ( return graphqlQueryRunnerExceptionHandler(error); case error instanceof TwentyORMException: return twentyORMGraphqlApiExceptionHandler(error); + case error instanceof CommonQueryRunnerException: + return commonQueryRunnerToGraphqlApiExceptionHandler(error); case error instanceof AuthException: return authGraphqlApiExceptionHandler(error); case error instanceof ApiKeyException: diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service.ts index aec9dcd6f920..31810a49ec5e 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service.ts @@ -5,6 +5,7 @@ import merge from 'lodash.merge'; import { type QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value'; import { type WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; +import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type'; import { type WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage'; import { type WorkspacePreQueryHookPayload } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type'; @@ -18,8 +19,9 @@ export class WorkspaceQueryHookService { private readonly workspaceQueryHookExplorer: WorkspaceQueryHookExplorer, ) {} + //TODO : Refacto-common - Should be Common public async executePreQueryHooks< - T extends WorkspaceResolverBuilderMethodNames, + T extends WorkspaceResolverBuilderMethodNames | CommonQueryNames, >( authContext: AuthContext, // TODO: We should allow wildcard for object name diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts index c503bce302c8..ede72c6faf62 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts @@ -13,8 +13,8 @@ import { FileModule } from 'src/engine/core-modules/file/file.module'; import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module'; import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module'; import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module'; -import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module'; +import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listener'; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts index 9796e88468a0..f2d7dac6aa95 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts @@ -1,14 +1,16 @@ import { Injectable } from '@nestjs/common'; -import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; +import graphqlFields from 'graphql-fields'; + import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { type FindOneResolverArgs, type Resolver, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; +import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; -import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service'; +import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names'; @Injectable() @@ -18,7 +20,7 @@ export class FindOneResolverFactory public static methodName = RESOLVER_METHOD_NAMES.FIND_ONE; constructor( - private readonly graphqlQueryRunnerService: GraphqlQueryFindOneResolverService, + private readonly commonFindOneQueryRunnerService: CommonFindOneQueryRunnerService, ) {} create( @@ -27,19 +29,20 @@ export class FindOneResolverFactory const internalContext = context; return async (_source, args, _context, info) => { - const options: WorkspaceQueryRunnerOptions = { - authContext: internalContext.authContext, - info, - objectMetadataMaps: internalContext.objectMetadataMaps, - objectMetadataItemWithFieldMaps: - internalContext.objectMetadataItemWithFieldMaps, - }; - - return await this.graphqlQueryRunnerService.execute( - args, - options, - FindOneResolverFactory.methodName, - ); + const selectedFields = graphqlFields(info); + + try { + return await this.commonFindOneQueryRunnerService.run({ + rawSelectedFields: { graphqlSelectedFields: selectedFields }, + args, + authContext: internalContext.authContext, + objectMetadataMaps: internalContext.objectMetadataMaps, + objectMetadataItemWithFieldMaps: + internalContext.objectMetadataItemWithFieldMaps, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module.ts index 89b8b36899dd..cd2915d8d773 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; +import { CoreCommonApiModule } from 'src/engine/api/common/core-common-api.module'; import { GraphqlQueryRunnerModule } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module'; import { WorkspaceResolverBuilderService } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.service'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; @@ -9,7 +10,7 @@ import { WorkspaceResolverFactory } from './workspace-resolver.factory'; import { workspaceResolverBuilderFactories } from './factories/factories'; @Module({ - imports: [GraphqlQueryRunnerModule, FeatureFlagModule], + imports: [GraphqlQueryRunnerModule, FeatureFlagModule, CoreCommonApiModule], providers: [ ...workspaceResolverBuilderFactories, WorkspaceResolverFactory, diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-many.handler.ts index c5f42237bf25..f4d833d4d4ef 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-many.handler.ts @@ -4,7 +4,7 @@ import { InternalServerErrorException, } from '@nestjs/common'; -import { type Request } from 'express'; +import { Request } from 'express'; import isEmpty from 'lodash.isempty'; import { isDefined } from 'twenty-shared/utils'; diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-one.handler.ts index 47a1e6d90e4f..4ee398cd8f0c 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-one.handler.ts @@ -4,7 +4,7 @@ import { InternalServerErrorException, } from '@nestjs/common'; -import { type Request } from 'express'; +import { Request } from 'express'; import isEmpty from 'lodash.isempty'; import { isDefined } from 'twenty-shared/utils'; diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts index 028dc44be667..ed1e60c7867e 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts @@ -1,6 +1,6 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { type Request } from 'express'; +import { Request } from 'express'; import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-many.handler.ts index 13fb14a3ccc2..5a3f9aec8aaf 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-many.handler.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { type Request } from 'express'; +import { Request } from 'express'; import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-one.handler.ts index 71c04b89a9bd..ee2fce5e2031 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-one.handler.ts @@ -1,49 +1,82 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; -import { type Request } from 'express'; -import { isDefined } from 'twenty-shared/utils'; +import { Request } from 'express'; +import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; +import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service'; import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; +import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util'; @Injectable() export class RestApiFindOneHandler extends RestApiBaseHandler { + constructor( + private readonly commonFindOneQueryRunnerService: CommonFindOneQueryRunnerService, + ) { + super(); + } + async handle(request: Request) { - const { id: recordId } = parseCorePath(request); + try { + const { args, rawSelectedFields } = await this.parseCommonArgs(request); + const { + authContext, + objectMetadataItemWithFieldMaps, + objectMetadataMaps, + } = await this.buildCommonOptions(request); + + const record = await this.commonFindOneQueryRunnerService.run({ + rawSelectedFields, + args, + authContext, + objectMetadataMaps, + objectMetadataItemWithFieldMaps, + }); - if (!isDefined(recordId)) { - throw new BadRequestException( - 'No recordId provided in rest api get one query', + return this.formatRestResponse( + record, + objectMetadataItemWithFieldMaps.nameSingular, ); + } catch (error) { + workspaceQueryRunnerRestApiExceptionHandler(error); } + } - const { - repository, - objectMetadata, - objectMetadataItemWithFieldsMaps, - restrictedFields, - } = await this.getRepositoryAndMetadataOrFail(request); - - const { records } = await this.findRecords({ - request, - recordId, - repository, - objectMetadata, - objectMetadataItemWithFieldsMaps, - restrictedFields, - }); - - const record = records?.[0]; - - if (!isDefined(record)) { - throw new BadRequestException('Record not found'); - } + private formatRestResponse(record: ObjectRecord, objectNameSingular: string) { + return { data: { [objectNameSingular]: record } }; + } + + private async parseCommonArgs(request: Request) { + const { id: recordId } = parseCorePath(request); + const filter = { id: { eq: recordId } }; + const depth = this.depthInputFactory.create(request); + + return { + args: { + filter, + }, + rawSelectedFields: { + depth, + }, + }; + } + + private async buildCommonOptions(request: Request) { + const { object: parsedObject } = parseCorePath(request); + + const { objectMetadataMaps, objectMetadataMapItem } = + await this.coreQueryBuilderFactory.getObjectMetadata( + request, + parsedObject, + ); + + const authContext = this.getAuthContextFromRequest(request); - return this.formatResult({ - operation: 'findOne', - objectNameSingular: objectMetadata.objectMetadataMapItem.nameSingular, - data: record, - }); + return { + authContext: authContext, + objectMetadataItemWithFieldMaps: objectMetadataMapItem, + objectMetadataMaps: objectMetadataMaps, + }; } } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts index 43dcd495e199..2596c20ea1ee 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts @@ -4,7 +4,7 @@ import { InternalServerErrorException, } from '@nestjs/common'; -import { type Request } from 'express'; +import { Request } from 'express'; import isEmpty from 'lodash.isempty'; import { isDefined } from 'twenty-shared/utils'; diff --git a/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts b/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts index dd17c66ecfd5..20fccb564ac3 100644 --- a/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts @@ -22,9 +22,9 @@ import { GetVariablesFactory } from 'src/engine/api/rest/core/query-builder/fact import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; import { type QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type'; import { - type Depth, DepthInputFactory, MAX_DEPTH, + type Depth, } from 'src/engine/api/rest/input-factories/depth-input.factory'; import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils'; import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils'; @@ -41,12 +41,14 @@ import { import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; +import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { formatResult as formatGetManyData } from 'src/engine/twenty-orm/utils/format-result.util'; import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; export interface PageInfo { hasNextPage?: boolean; @@ -94,11 +96,15 @@ export abstract class RestApiBaseHandler { @Inject() protected readonly createdByFromAuthContextService: CreatedByFromAuthContextService; @Inject() + protected readonly workspaceCacheStorageService: WorkspaceCacheStorageService; + @Inject() + protected readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService; + @Inject() protected readonly apiKeyRoleService: ApiKeyRoleService; protected abstract handle( request: Request, - ): Promise; + ): Promise; public async getRepositoryAndMetadataOrFail(request: Request) { const { workspace, apiKey, userWorkspaceId } = request; @@ -109,14 +115,14 @@ export abstract class RestApiBaseHandler { parsedObject, ); - if (!objectMetadata) { - throw new BadRequestException('Object metadata not found'); - } - if (!workspace?.id) { throw new BadRequestException('Workspace not found'); } + if (!objectMetadata) { + throw new BadRequestException('Object metadata not found'); + } + const workspaceDataSource = await this.twentyORMManager.getDatasource(); const objectMetadataNameSingular = @@ -134,18 +140,22 @@ export abstract class RestApiBaseHandler { ); } - let roleId: string | undefined = undefined; - let shouldBypassPermissionChecks = false; + let roleId: string; if (isDefined(apiKey)) { roleId = await this.apiKeyRoleService.getRoleIdForApiKey( apiKey.id, workspace.id, ); - } - if (isDefined(userWorkspaceId)) { - roleId = + if (!isDefined(roleId)) { + throw new PermissionsException( + PermissionsExceptionMessage.API_KEY_ROLE_NOT_FOUND, + PermissionsExceptionCode.API_KEY_ROLE_NOT_FOUND, + ); + } + } else { + const userWorkspaceRoleId = await this.workspacePermissionsCacheService.getRoleIdFromUserWorkspaceId( { workspaceId: workspace.id, @@ -153,60 +163,53 @@ export abstract class RestApiBaseHandler { }, ); - if (!roleId) { + if (!isDefined(userWorkspaceRoleId)) { throw new PermissionsException( PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE, PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE, ); } - } - if (!isDefined(apiKey) && !isDefined(userWorkspaceId)) { - throw new PermissionsException( - PermissionsExceptionMessage.NO_AUTHENTICATION_CONTEXT, - PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT, - ); + roleId = userWorkspaceRoleId; } const repository = workspaceDataSource.getRepository( objectMetadataNameSingular, - shouldBypassPermissionChecks, + false, roleId, ); - let restrictedFields: RestrictedFieldsPermissions = {}; - - if (roleId) { - const objectMetadataPermissions = - await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles( - { - workspaceId: workspace.id, - roleIds: roleId ? [roleId] : undefined, - }, - ); - - if ( - !isDefined( - objectMetadataPermissions?.[roleId]?.[ - objectMetadata.objectMetadataMapItem.id - ]?.restrictedFields, - ) - ) { - throw new InternalServerError('Fields permissions not found for role'); - } + const objectMetadataPermissions = + await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles( + { + workspaceId: workspace.id, + roleIds: roleId ? [roleId] : undefined, + }, + ); - restrictedFields = - objectMetadataPermissions[roleId][ + if ( + !isDefined( + objectMetadataPermissions?.[roleId]?.[ objectMetadata.objectMetadataMapItem.id - ].restrictedFields; + ]?.restrictedFields, + ) + ) { + throw new InternalServerError('Fields permissions not found for role'); } + const restrictedFields = + objectMetadataPermissions[roleId][objectMetadata.objectMetadataMapItem.id] + .restrictedFields; + return { objectMetadata, repository, workspaceDataSource, objectMetadataItemWithFieldsMaps, restrictedFields, + isExecutedByApiKey: isDefined(apiKey), + authContext: this.getAuthContextFromRequest(request), + objectsPermissions: objectMetadataPermissions[roleId], }; } diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.module.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.module.ts index b927f1761ca0..72d7847bb234 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.module.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.module.ts @@ -2,19 +2,23 @@ import { Module } from '@nestjs/common'; import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory'; import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories'; +import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; +import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; @Module({ imports: [ AuthModule, + ApiKeyModule, DomainManagerModule, FeatureFlagModule, WorkspaceCacheStorageModule, WorkspaceMetadataCacheModule, + WorkspacePermissionsCacheModule, ], providers: [...coreQueryBuilderFactories, CoreQueryBuilderFactory], exports: [CoreQueryBuilderFactory], diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/factories.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/factories.ts index 16f34a21aedf..f0405a531523 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/factories.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/factories.ts @@ -1,8 +1,8 @@ import { CreateManyQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/create-many-query.factory'; +import { CreateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/create-variables.factory'; import { FindDuplicatesQueryFactory } from 'src/engine/api/rest/core/query-builder/factories/find-duplicates-query.factory'; import { FindDuplicatesVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/find-duplicates-variables.factory'; import { GetVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/get-variables.factory'; -import { CreateVariablesFactory } from 'src/engine/api/rest/core/query-builder/factories/create-variables.factory'; import { inputFactories } from 'src/engine/api/rest/input-factories/factories'; export const coreQueryBuilderFactories = [ diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/get-variables.factory.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/get-variables.factory.ts index cf3de74f33e3..1df2ec258757 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/get-variables.factory.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/factories/get-variables.factory.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { type Request } from 'express'; +import { isDefined } from 'twenty-shared/utils'; import { type QueryVariables } from 'src/engine/api/rest/core/types/query-variables.type'; import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/ending-before-input.factory'; @@ -29,7 +30,7 @@ export class GetVariablesFactory { objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; }, ): QueryVariables { - if (id) { + if (isDefined(id)) { return { filter: { id: { eq: id } } }; } diff --git a/packages/twenty-server/src/engine/api/rest/core/rest-api-core.module.ts b/packages/twenty-server/src/engine/api/rest/core/rest-api-core.module.ts index a71a8c0a6836..50c7562365e4 100644 --- a/packages/twenty-server/src/engine/api/rest/core/rest-api-core.module.ts +++ b/packages/twenty-server/src/engine/api/rest/core/rest-api-core.module.ts @@ -1,6 +1,7 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; +import { CoreCommonApiModule } from 'src/engine/api/common/core-common-api.module'; import { RestApiCoreController } from 'src/engine/api/rest/core/controllers/rest-api-core.controller'; import { RestApiCreateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-many.handler'; import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler'; @@ -16,8 +17,10 @@ import { RestApiService } from 'src/engine/api/rest/rest-api.service'; import { ActorModule } from 'src/engine/core-modules/actor/actor.module'; import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; +import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module'; +import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; @@ -42,8 +45,11 @@ const restApiCoreResolvers = [ TwentyORMModule, RecordTransformerModule, WorkspacePermissionsCacheModule, + WorkspaceMetadataCacheModule, ActorModule, FeatureFlagModule, + CoreCommonApiModule, + DomainManagerModule, ], controllers: [RestApiCoreController], providers: [ diff --git a/packages/twenty-server/src/engine/api/rest/core/services/rest-api-core.service.ts b/packages/twenty-server/src/engine/api/rest/core/services/rest-api-core.service.ts index e8ee3bfd5be5..4314f1ef4d39 100644 --- a/packages/twenty-server/src/engine/api/rest/core/services/rest-api-core.service.ts +++ b/packages/twenty-server/src/engine/api/rest/core/services/rest-api-core.service.ts @@ -1,16 +1,16 @@ import { Injectable } from '@nestjs/common'; -import { type Request } from 'express'; +import { Request } from 'express'; import { isDefined } from 'twenty-shared/utils'; -import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; -import { RestApiDeleteOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-delete-one.handler'; -import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler'; -import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler'; -import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-one.handler'; -import { RestApiFindManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-many.handler'; import { RestApiCreateManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-many.handler'; +import { RestApiCreateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-create-one.handler'; +import { RestApiDeleteOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-delete-one.handler'; import { RestApiFindDuplicatesHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler'; +import { RestApiFindManyHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-many.handler'; +import { RestApiFindOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-find-one.handler'; +import { RestApiUpdateOneHandler } from 'src/engine/api/rest/core/handlers/rest-api-update-one.handler'; +import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; @Injectable() export class RestApiCoreService { diff --git a/packages/twenty-server/src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util.ts b/packages/twenty-server/src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util.ts new file mode 100644 index 000000000000..843b795a90be --- /dev/null +++ b/packages/twenty-server/src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util.ts @@ -0,0 +1,19 @@ +import { type QueryFailedError } from 'typeorm'; + +import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception'; +import { commonQueryRunnerToRestApiExceptionHandler } from 'src/engine/api/common/common-query-runners/utils/common-query-runner-to-rest-api-exception-handler.util'; + +interface QueryFailedErrorWithCode extends QueryFailedError { + code: string; +} + +export const workspaceQueryRunnerRestApiExceptionHandler = ( + error: QueryFailedErrorWithCode, +) => { + switch (true) { + case error instanceof CommonQueryRunnerException: + return commonQueryRunnerToRestApiExceptionHandler(error); + default: + throw error; + } +}; diff --git a/packages/twenty-server/src/engine/guards/jwt-auth.guard.ts b/packages/twenty-server/src/engine/guards/jwt-auth.guard.ts index b35d3906d73b..1e5b2ee54ee6 100644 --- a/packages/twenty-server/src/engine/guards/jwt-auth.guard.ts +++ b/packages/twenty-server/src/engine/guards/jwt-auth.guard.ts @@ -4,7 +4,14 @@ import { Injectable, } from '@nestjs/common'; +import { isDefined } from 'twenty-shared/utils'; + import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; +import { + PermissionsException, + PermissionsExceptionCode, + PermissionsExceptionMessage, +} from 'src/engine/metadata-modules/permissions/permissions.exception'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; @Injectable() @@ -26,6 +33,13 @@ export class JwtAuthGuard implements CanActivate { ) : undefined; + if (!isDefined(data.apiKey) && !isDefined(data.userWorkspaceId)) { + throw new PermissionsException( + PermissionsExceptionMessage.NO_AUTHENTICATION_CONTEXT, + PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT, + ); + } + request.user = data.user; request.apiKey = data.apiKey; request.workspace = data.workspace;