fix blocklist (#18332)

- The schema generator marked both the FK scalar and connect relation
input as required for non-nullable `MANY_TO_ONE` relations, but the
resolver rejects when both are provided making create mutations
impossible
- Fixed by making the connect input always optional in create input
types (the FK scalar still enforces the constraint)
- Added `createOne` pre-query hook for blocklist with ownership
validation


https://github.com/user-attachments/assets/aaae83d4-4747-4d16-a87c-8d8cad79d25d

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
neo773
2026-03-06 00:56:04 +05:30
committed by GitHub
parent 62a634831d
commit ef003fb929
31 changed files with 340 additions and 204 deletions
@@ -1,5 +1,7 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { z } from 'zod';
import {
@@ -7,6 +9,10 @@ import {
type UpdateOneResolverArgs,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
@@ -68,13 +74,21 @@ export class BlocklistValidationService {
for (const handle of blocklist.map((item) => item.handle)) {
if (!handle) {
throw new BadRequestException('Blocklist handle is required');
throw new CommonQueryRunnerException(
'Blocklist handle is required',
CommonQueryRunnerExceptionCode.BAD_REQUEST,
{ userFriendlyMessage: msg`Blocklist handle is required.` },
);
}
const result = emailOrDomainSchema.safeParse(handle);
if (!result.success) {
throw new BadRequestException(result.error.issues[0].message);
throw new CommonQueryRunnerException(
result.error.issues[0].message,
CommonQueryRunnerExceptionCode.BAD_REQUEST,
{ userFriendlyMessage: msg`Invalid email or domain.` },
);
}
}
}
@@ -102,6 +116,22 @@ export class BlocklistValidationService {
authContext,
);
if (
payload.data.some(
(item) =>
isDefined(item.workspaceMemberId) &&
item.workspaceMemberId !== currentWorkspaceMember.id,
)
) {
throw new CommonQueryRunnerException(
'Cannot create blocklist entry for another workspace member',
CommonQueryRunnerExceptionCode.BAD_REQUEST,
{
userFriendlyMessage: msg`Cannot create blocklist entry for another workspace member.`,
},
);
}
const currentBlocklist =
await this.blocklistRepository.getByWorkspaceMemberId(
currentWorkspaceMember.id,
@@ -115,7 +145,11 @@ export class BlocklistValidationService {
if (
payload.data.some((item) => currentBlocklistHandles.includes(item.handle))
) {
throw new BadRequestException('Blocklist handle already exists');
throw new CommonQueryRunnerException(
'Blocklist handle already exists',
CommonQueryRunnerExceptionCode.BAD_REQUEST,
{ userFriendlyMessage: msg`Blocklist handle already exists.` },
);
}
}
@@ -130,11 +164,19 @@ export class BlocklistValidationService {
);
if (!existingRecord) {
throw new BadRequestException('Blocklist item not found');
throw new CommonQueryRunnerException(
'Blocklist item not found',
CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND,
{ userFriendlyMessage: msg`Blocklist item not found.` },
);
}
if (existingRecord.workspaceMemberId !== payload.data.workspaceMemberId) {
throw new BadRequestException('Workspace member cannot be updated');
throw new CommonQueryRunnerException(
'Workspace member cannot be updated',
CommonQueryRunnerExceptionCode.BAD_REQUEST,
{ userFriendlyMessage: msg`Workspace member cannot be updated.` },
);
}
if (existingRecord.handle === payload.data.handle) {
@@ -170,7 +212,11 @@ export class BlocklistValidationService {
.map((blocklist) => blocklist.handle);
if (currentBlocklistHandles.includes(payload.data.handle)) {
throw new BadRequestException('Blocklist handle already exists');
throw new CommonQueryRunnerException(
'Blocklist handle already exists',
CommonQueryRunnerExceptionCode.BAD_REQUEST,
{ userFriendlyMessage: msg`Blocklist handle already exists.` },
);
}
}
}
@@ -1,8 +1,12 @@
import { BadRequestException } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
@@ -25,7 +29,11 @@ export class BlocklistCreateManyPreQueryHook
payload: CreateManyResolverArgs<BlocklistItem>,
): Promise<CreateManyResolverArgs<BlocklistItem>> {
if (!isUserAuthContext(authContext)) {
throw new BadRequestException('User id is required');
throw new CommonQueryRunnerException(
'User id is required',
CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT,
{ userFriendlyMessage: msg`User id is required.` },
);
}
await this.blocklistValidationService.validateBlocklistForCreateMany(
@@ -0,0 +1,49 @@
import { msg } from '@lingui/core/macro';
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type CreateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import {
type BlocklistItem,
BlocklistValidationService,
} from 'src/modules/blocklist/blocklist-validation-manager/services/blocklist-validation.service';
@WorkspaceQueryHook(`blocklist.createOne`)
export class BlocklistCreateOnePreQueryHook
implements WorkspacePreQueryHookInstance
{
constructor(
private readonly blocklistValidationService: BlocklistValidationService,
) {}
async execute(
authContext: WorkspaceAuthContext,
_objectName: string,
payload: CreateOneResolverArgs<BlocklistItem>,
): Promise<CreateOneResolverArgs<BlocklistItem>> {
if (!isUserAuthContext(authContext)) {
throw new CommonQueryRunnerException(
'User id is required',
CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT,
{
userFriendlyMessage: msg`You must be authenticated to manage blocklist.`,
},
);
}
await this.blocklistValidationService.validateBlocklistForCreateMany(
{ data: [payload.data] },
authContext.user.id,
authContext.workspace.id,
);
return payload;
}
}
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { BlocklistValidationManagerModule } from 'src/modules/blocklist/blocklist-validation-manager/blocklist-validation-manager.module';
import { BlocklistCreateManyPreQueryHook } from 'src/modules/blocklist/query-hooks/blocklist-create-many.pre-query.hook';
import { BlocklistCreateOnePreQueryHook } from 'src/modules/blocklist/query-hooks/blocklist-create-one.pre-query.hook';
import { BlocklistUpdateManyPreQueryHook } from 'src/modules/blocklist/query-hooks/blocklist-update-many.pre-query.hook';
import { BlocklistUpdateOnePreQueryHook } from 'src/modules/blocklist/query-hooks/blocklist-update-one.pre-query.hook';
@@ -9,6 +10,7 @@ import { BlocklistUpdateOnePreQueryHook } from 'src/modules/blocklist/query-hook
imports: [BlocklistValidationManagerModule],
providers: [
BlocklistCreateManyPreQueryHook,
BlocklistCreateOnePreQueryHook,
BlocklistUpdateManyPreQueryHook,
BlocklistUpdateOnePreQueryHook,
],
@@ -1,8 +1,12 @@
import { MethodNotAllowedException } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type UpdateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { type BlocklistItem } from 'src/modules/blocklist/blocklist-validation-manager/services/blocklist-validation.service';
@@ -13,6 +17,12 @@ export class BlocklistUpdateManyPreQueryHook
constructor() {}
async execute(): Promise<UpdateManyResolverArgs<BlocklistItem>> {
throw new MethodNotAllowedException('Method not allowed.');
throw new CommonQueryRunnerException(
'Method not allowed.',
CommonQueryRunnerExceptionCode.BAD_REQUEST,
{
userFriendlyMessage: msg`Bulk update of blocklist entries is not allowed.`,
},
);
}
}
@@ -1,8 +1,12 @@
import { BadRequestException } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
@@ -25,7 +29,11 @@ export class BlocklistUpdateOnePreQueryHook
payload: UpdateOneResolverArgs<BlocklistItem>,
): Promise<UpdateOneResolverArgs<BlocklistItem>> {
if (!isUserAuthContext(authContext)) {
throw new BadRequestException('User id is required');
throw new CommonQueryRunnerException(
'User id is required',
CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT,
{ userFriendlyMessage: msg`User id is required.` },
);
}
const workspace = authContext.workspace;