Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ export abstract class CommonBaseQueryRunnerService<
workspaceId: string,
): Promise<string> {
if (isDefined(authContext.apiKey)) {
return this.apiKeyRoleService.getRoleIdForApiKey(
return this.apiKeyRoleService.getRoleIdForApiKeyId(
authContext.apiKey.id,
workspaceId,
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Test, type TestingModule } from '@nestjs/testing';

import { McpProtocolService } from 'src/engine/api/mcp/services/mcp-protocol.service';
import { type JsonRpc } from 'src/engine/api/mcp/dtos/json-rpc';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { MCP_SERVER_METADATA } from 'src/engine/api/mcp/constants/mcp.const';
import { McpCoreController } from 'src/engine/api/mcp/controllers/mcp-core.controller';
import { type JsonRpc } from 'src/engine/api/mcp/dtos/json-rpc';
import { McpProtocolService } from 'src/engine/api/mcp/services/mcp-protocol.service';
import { type ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service';
import { McpCoreController } from 'src/engine/api/mcp/controllers/mcp-core.controller';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';

describe('McpCoreController', () => {
let controller: McpCoreController;
Expand Down Expand Up @@ -53,7 +54,7 @@ describe('McpCoreController', () => {
describe('handleMcpCore', () => {
const mockWorkspace = { id: 'workspace-1' } as WorkspaceEntity;
const mockUserWorkspaceId = 'user-workspace-1';
const mockApiKey = 'api-key-1';
const mockApiKey = { id: 'api-key-1' } as ApiKeyEntity;

it('should call mcpProtocolService.handleMCPCoreQuery with correct parameters', async () => {
const mockRequest: JsonRpc = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { JsonRpc } from 'src/engine/api/mcp/dtos/json-rpc';
import { McpProtocolService } from 'src/engine/api/mcp/services/mcp-protocol.service';
import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter';
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthApiKey } from 'src/engine/decorators/auth/auth-api-key.decorator';
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
Expand All @@ -36,7 +37,7 @@ export class McpCoreController {
async handleMcpCore(
@Body() body: JsonRpc,
@AuthWorkspace() workspace: WorkspaceEntity,
@AuthApiKey() apiKey: string | undefined,
@AuthApiKey() apiKey: ApiKeyEntity | undefined,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is going to break any current MCP users right ? are we ok with that ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only a typing change, this won't break the actual logic behind it. Also, request object already types it correctly but this was not typed properly in controllers/resolvers.
The typing was wrong originally already, code expected things to be strings but they were objects. They were rarely accessed as values but simply checked if they were defined but I'd prefer to correct the typing right now to avoid any issue in the future. We had a real bug that I've fixed in this PR if you want an example #16540

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, I did not read 16540. Then it's 100% logic

@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
) {
return await this.mcpProtocolService.handleMCPCoreQuery(body, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { MCP_SERVER_METADATA } from 'src/engine/api/mcp/constants/mcp.const';
import { type JsonRpc } from 'src/engine/api/mcp/dtos/json-rpc';
import { McpProtocolService } from 'src/engine/api/mcp/services/mcp-protocol.service';
import { McpToolExecutorService } from 'src/engine/api/mcp/services/mcp-tool-executor.service';
import { type ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { ToolProviderService } from 'src/engine/core-modules/tool-provider/services/tool-provider.service';
Expand All @@ -27,7 +28,10 @@ describe('McpProtocolService', () => {
const mockUserWorkspaceId = 'user-workspace-1';
const mockRoleId = 'role-1';
const mockAdminRoleId = 'admin-role-1';
const mockApiKey = 'api-key-1';
const mockApiKey = {
id: 'api-key-1',
workspaceId: mockWorkspace.id,
} as ApiKeyEntity;

beforeEach(async () => {
const mockFeatureFlagService = {
Expand Down Expand Up @@ -191,6 +195,7 @@ describe('McpProtocolService', () => {
const result = await service.handleMCPCoreQuery(mockRequest, {
workspace: mockWorkspace,
userWorkspaceId: mockUserWorkspaceId,
apiKey: undefined,
});

expect(result).toMatchObject({
Expand Down Expand Up @@ -252,6 +257,7 @@ describe('McpProtocolService', () => {
const result = await service.handleMCPCoreQuery(mockRequest, {
workspace: mockWorkspace,
userWorkspaceId: mockUserWorkspaceId,
apiKey: undefined,
});

expect(result).toEqual(mockToolCallResponse);
Expand Down Expand Up @@ -356,6 +362,7 @@ describe('McpProtocolService', () => {
const result = await service.handleMCPCoreQuery(mockRequest, {
workspace: mockWorkspace,
userWorkspaceId: mockUserWorkspaceId,
apiKey: undefined,
});

expect(result).toMatchObject(mockToolsListingResponse);
Expand All @@ -373,6 +380,7 @@ describe('McpProtocolService', () => {
const result = await service.handleMCPCoreQuery(mockRequest, {
workspace: mockWorkspace,
userWorkspaceId: mockUserWorkspaceId,
apiKey: undefined,
});

expect(result).toEqual({
Expand Down Expand Up @@ -408,6 +416,7 @@ describe('McpProtocolService', () => {
const result = await service.handleMCPCoreQuery(mockRequest, {
workspace: mockWorkspace,
userWorkspaceId: mockUserWorkspaceId,
apiKey: undefined,
});

expect(result).toEqual({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';

import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';

import { type JsonRpc } from 'src/engine/api/mcp/dtos/json-rpc';
import { McpToolExecutorService } from 'src/engine/api/mcp/services/mcp-tool-executor.service';
import { wrapJsonRpcResponse } from 'src/engine/api/mcp/utils/wrap-jsonrpc-response.util';
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
Expand Down Expand Up @@ -58,21 +60,21 @@ export class McpProtocolService {
async getRoleId(
workspaceId: string,
userWorkspaceId?: string,
apiKey?: string,
apiKey?: ApiKeyEntity,
) {
if (apiKey) {
const roles = await this.roleRepository.find({
if (isDefined(apiKey)) {
const [role] = await this.roleRepository.find({
where: {
workspaceId,
standardId: ADMIN_ROLE.standardId,
},
});

if (roles.length === 0) {
if (!isDefined(role)) {
throw new HttpException('Admin role not found', HttpStatus.FORBIDDEN);
}

return roles[0].id;
return role.id;
}

if (!userWorkspaceId) {
Expand Down Expand Up @@ -103,7 +105,7 @@ export class McpProtocolService {
}: {
workspace: WorkspaceEntity;
userWorkspaceId?: string;
apiKey?: string;
apiKey: ApiKeyEntity | undefined;
},
): Promise<Record<string, unknown>> {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export abstract class RestApiBaseHandler {
let roleId: string;

if (isDefined(authContext.apiKey)) {
roleId = await this.apiKeyRoleService.getRoleIdForApiKey(
roleId = await this.apiKeyRoleService.getRoleIdForApiKeyId(
authContext.apiKey.id,
authContext.workspace.id,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class ApiKeyRoleService {
});
}

async getRoleIdForApiKey(
async getRoleIdForApiKeyId(
apiKeyId: string,
workspaceId: string,
): Promise<string> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';

import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { PermissionFlagType } from 'twenty-shared/constants';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';

import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
import { BillingCheckoutSessionInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-checkout-session.input';
import { BillingSessionInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-session.input';
import { BillingUpdateSubscriptionItemPriceInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-update-subscription-item-price.input';
Expand Down Expand Up @@ -87,12 +88,12 @@ export class BillingResolver {
plan,
requirePaymentMethod,
}: BillingCheckoutSessionInput,
@AuthApiKey() apiKey?: string,
@AuthApiKey() apiKey?: ApiKeyEntity,
) {
await this.validateCanCheckoutSessionPermissionOrThrow({
workspaceId: workspace.id,
userWorkspaceId,
apiKeyId: apiKey,
apiKeyId: apiKey?.id,
workspaceActivationStatus: workspace.activationStatus,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export class SearchResolver {
let rolePermissionConfig: RolePermissionConfig | undefined;

if (isDefined(apiKey)) {
const roleId = await this.apiKeyRoleService.getRoleIdForApiKey(
const roleId = await this.apiKeyRoleService.getRoleIdForApiKeyId(
apiKey.id,
workspace.id,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/

import type { FileUpload } from 'graphql-upload/processRequest.mjs';

import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
import {
AuthException,
AuthExceptionCode,
Expand Down Expand Up @@ -417,7 +418,7 @@ export class UserResolver {
@AuthUserWorkspaceId() userWorkspaceId: string,
@AuthWorkspace()
workspace: WorkspaceEntity,
@AuthApiKey() apiKey?: string,
@AuthApiKey() apiKey: ApiKeyEntity | undefined,
) {
if (!workspace) {
throw new AuthException(
Expand Down Expand Up @@ -462,7 +463,7 @@ export class UserResolver {
userWorkspaceId,
workspaceId: workspace.id,
setting: PermissionFlagType.WORKSPACE_MEMBERS,
apiKeyId: apiKey ?? undefined,
apiKeyId: apiKey?.id,
}));

if (!canDeleteUserFromWorkspace) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import assert from 'assert';

import { msg } from '@lingui/core/macro';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { PermissionFlagType } from 'twenty-shared/constants';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { Repository } from 'typeorm';
import { PermissionFlagType } from 'twenty-shared/constants';

import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service';
Expand Down Expand Up @@ -107,7 +108,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
}: {
payload: Partial<WorkspaceEntity> & { id: string };
userWorkspaceId?: string;
apiKey?: string;
apiKey: ApiKeyEntity | undefined;
}) {
const workspace = await this.workspaceRepository.findOneBy({
id: payload.id,
Expand Down Expand Up @@ -389,7 +390,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
payload: Partial<WorkspaceEntity>;
userWorkspaceId?: string;
workspaceId: string;
apiKey?: string;
apiKey: ApiKeyEntity | undefined;
workspaceActivationStatus: WorkspaceActivationStatus;
}) {
if (
Expand Down Expand Up @@ -439,7 +440,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
userWorkspaceId,
workspaceId,
setting: permission,
apiKeyId: apiKey,
apiKeyId: apiKey?.id,
});

if (!hasPermission) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ import {
import assert from 'assert';

import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { PermissionFlagType } from 'twenty-shared/constants';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';

import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';

import type { FileUpload } from 'graphql-upload/processRequest.mjs';

import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
import { fromFlatApplicationToApplicationDto } from 'src/engine/core-modules/application/utils/from-flat-application-to-application-dto.util';
Expand Down Expand Up @@ -136,7 +137,7 @@ export class WorkspaceResolver {
@Args('data') data: UpdateWorkspaceInput,
@AuthWorkspace() workspace: WorkspaceEntity,
@AuthUserWorkspaceId() userWorkspaceId: string,
@AuthApiKey() apiKey?: string,
@AuthApiKey() apiKey: ApiKeyEntity | undefined,
) {
try {
return await this.workspaceService.updateWorkspaceById({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';

import { msg } from '@lingui/core/macro';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
import { PermissionFlagType } from 'twenty-shared/constants';

import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
import { TOOL_PERMISSION_FLAGS } from 'src/engine/metadata-modules/permissions/constants/tool-permission-flags';
Expand Down Expand Up @@ -131,8 +131,8 @@ export class PermissionsService {
setting: PermissionFlagType;
apiKeyId?: string;
}): Promise<boolean> {
if (apiKeyId) {
const roleId = await this.apiKeyRoleService.getRoleIdForApiKey(
if (isDefined(apiKeyId)) {
const roleId = await this.apiKeyRoleService.getRoleIdForApiKeyId(
apiKeyId,
workspaceId,
);
Expand Down
Loading