diff --git a/packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx b/packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx index 2562f335bd..2d0799f07d 100644 --- a/packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx +++ b/packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx @@ -262,6 +262,10 @@ POST https://your-twenty-server.com/webhooks/server/:resolverLogicFunctionUniver The identifier is the resolver's `universalIdentifier` from your manifest. Register that URL with the provider. + +**The application must be claimed and installed on its owner workspace.** Because the resolver runs in the **owner workspace** (the workspace that owns the application registration), a server route trigger only works once the application has been *claimed* — i.e. it has an owner workspace — **and** that application is **installed on the owner workspace**. Until both are true the resolver has nowhere to run, so the route cannot be dispatched. An application that exposes a `serverRouteTriggerSettings` logic function therefore cannot be listed in the marketplace until it is claimed and installed on its owner workspace. + + **Resolver contract.** The SDK's `LogicFunctionConfig` type enforces this at compile time: as soon as you set `serverRouteTriggerSettings`, your handler is constrained to return `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (or a `Promise` of it). The `workspaceId` must be a workspace where the target function is installed, otherwise the request is rejected with `404`. | Field | Type | Notes | diff --git a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationRegistrationGeneralTab.tsx b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationRegistrationGeneralTab.tsx index 6f7e9540a6..03021db0b8 100644 --- a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationRegistrationGeneralTab.tsx +++ b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationRegistrationGeneralTab.tsx @@ -24,7 +24,7 @@ export const SettingsApplicationRegistrationGeneralTab = ({ {!registration.isConfigured && fromAdmin && ( navigate('#config'), diff --git a/packages/twenty-server/src/engine/core-modules/application/application-registration-variable/application-registration-variable.module.ts b/packages/twenty-server/src/engine/core-modules/application/application-registration-variable/application-registration-variable.module.ts index 04e1540159..a15aa7bfe1 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-registration-variable/application-registration-variable.module.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-registration-variable/application-registration-variable.module.ts @@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ApplicationRegistrationVariableEntity } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.entity'; import { ApplicationRegistrationVariableService } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.service'; import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module'; @Module({ @@ -11,6 +12,7 @@ import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryptio TypeOrmModule.forFeature([ ApplicationRegistrationVariableEntity, ApplicationRegistrationEntity, + ApplicationEntity, ]), SecretEncryptionModule, ], diff --git a/packages/twenty-server/src/engine/core-modules/application/application-registration-variable/application-registration-variable.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-registration-variable/application-registration-variable.service.ts index 3703799095..9f662b5880 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-registration-variable/application-registration-variable.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-registration-variable/application-registration-variable.service.ts @@ -7,6 +7,7 @@ import { In, Not, type Repository } from 'typeorm'; import { ApplicationRegistrationVariableEntity } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.entity'; import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { ApplicationRegistrationException, ApplicationRegistrationExceptionCode, @@ -23,6 +24,8 @@ export class ApplicationRegistrationVariableService { private readonly variableRepository: Repository, @InjectRepository(ApplicationRegistrationEntity) private readonly applicationRegistrationRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, private readonly encryptionService: SecretEncryptionService, ) {} @@ -161,43 +164,71 @@ export class ApplicationRegistrationVariableService { async isConfiguredBatch( applicationRegistrationIds: string[], ): Promise> { - const variables = await this.variableRepository.find({ - where: { applicationRegistrationId: In(applicationRegistrationIds) }, - }); - - const variablesByRegistrationId = new Map< - string, - ApplicationRegistrationVariableEntity[] - >(); - - for (const variable of variables) { - const existing = - variablesByRegistrationId.get(variable.applicationRegistrationId) ?? []; - - existing.push(variable); - variablesByRegistrationId.set( - variable.applicationRegistrationId, - existing, - ); - } + const [variables, registrations, installedApps] = await Promise.all([ + this.variableRepository.find({ + where: { applicationRegistrationId: In(applicationRegistrationIds) }, + }), + this.applicationRegistrationRepository.find({ + where: { id: In(applicationRegistrationIds) }, + select: { id: true, manifest: true, ownerWorkspaceId: true }, + }), + this.applicationRepository.find({ + where: { applicationRegistrationId: In(applicationRegistrationIds) }, + select: { applicationRegistrationId: true, workspaceId: true }, + }), + ]); const result = new Map(); for (const id of applicationRegistrationIds) { - const registrationVariables = variablesByRegistrationId.get(id) ?? []; - const requiredVariables = registrationVariables.filter( - (v) => v.isRequired, + const registration = registrations.find( + (registration) => registration.id === id, + ); + + const areVariablesConfigured = variables + .filter( + (variable) => + variable.applicationRegistrationId === id && variable.isRequired, + ) + .every((variable) => variable.isFilled); + + const isInstalledOnOwnerWorkspace = installedApps.some( + (app) => + app.applicationRegistrationId === id && + app.workspaceId === registration?.ownerWorkspaceId, ); result.set( id, - requiredVariables.every((v) => v.isFilled), + areVariablesConfigured && + this.isServerRouteConfigured( + registration, + isInstalledOnOwnerWorkspace, + ), ); } return result; } + private isServerRouteConfigured( + registration: ApplicationRegistrationEntity | undefined, + isInstalledOnOwnerWorkspace: boolean, + ): boolean { + const hasServerRouteFunction = + registration?.manifest?.logicFunctions?.some((logicFunction) => + isDefined(logicFunction.serverRouteTriggerSettings), + ) ?? false; + + if (!hasServerRouteFunction) { + return true; + } + + return ( + isDefined(registration?.ownerWorkspaceId) && isInstalledOnOwnerWorkspace + ); + } + private async findVariableOrThrow( id: string, ): Promise { diff --git a/packages/twenty-server/src/engine/core-modules/server-route-trigger/__tests__/server-route-trigger.service.spec.ts b/packages/twenty-server/src/engine/core-modules/server-route-trigger/__tests__/server-route-trigger.service.spec.ts index d8b5cb6f09..b61ef4479e 100644 --- a/packages/twenty-server/src/engine/core-modules/server-route-trigger/__tests__/server-route-trigger.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/server-route-trigger/__tests__/server-route-trigger.service.spec.ts @@ -9,7 +9,6 @@ import { } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service'; import { ServerRouteTriggerExceptionCode } from 'src/engine/core-modules/server-route-trigger/exceptions/server-route-trigger.exception'; import { ServerRouteTriggerService } from 'src/engine/core-modules/server-route-trigger/server-route-trigger.service'; -import { type TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { type LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity'; import { LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto'; @@ -55,7 +54,6 @@ describe('ServerRouteTriggerService', () => { let logicFunctionExecutorService: jest.Mocked< Pick >; - let twentyConfigService: jest.Mocked>; const handle = () => service.handle({ @@ -98,12 +96,10 @@ describe('ServerRouteTriggerService', () => { // target returns the final response body .mockResolvedValueOnce(buildExecuteResult({ ok: true })), }; - twentyConfigService = { get: jest.fn().mockReturnValue(true) }; service = new ServerRouteTriggerService( logicFunctionRepository as unknown as Repository, logicFunctionExecutorService as unknown as LogicFunctionExecutorService, - twentyConfigService as unknown as TwentyConfigService, ); }); @@ -133,14 +129,6 @@ describe('ServerRouteTriggerService', () => { ); }); - it('refuses when the feature is disabled', async () => { - twentyConfigService.get.mockReturnValue(false); - - await expect(handle()).rejects.toMatchObject({ - code: ServerRouteTriggerExceptionCode.FEATURE_DISABLED, - }); - }); - it('throws LOGIC_FUNCTION_NOT_FOUND when no row matches the universalIdentifier', async () => { logicFunctionRepository.find.mockResolvedValue([]); diff --git a/packages/twenty-server/src/engine/core-modules/server-route-trigger/exceptions/server-route-trigger-rest-api-exception-filter.ts b/packages/twenty-server/src/engine/core-modules/server-route-trigger/exceptions/server-route-trigger-rest-api-exception-filter.ts index 794d7bc319..6c128abdbf 100644 --- a/packages/twenty-server/src/engine/core-modules/server-route-trigger/exceptions/server-route-trigger-rest-api-exception-filter.ts +++ b/packages/twenty-server/src/engine/core-modules/server-route-trigger/exceptions/server-route-trigger-rest-api-exception-filter.ts @@ -30,12 +30,6 @@ export class ServerRouteTriggerRestApiExceptionFilter implements ExceptionFilter response, 404, ); - case ServerRouteTriggerExceptionCode.FEATURE_DISABLED: - return this.httpExceptionHandlerService.handleError( - exception as CustomException, - response, - 503, - ); case ServerRouteTriggerExceptionCode.RATE_LIMIT_EXCEEDED: return this.httpExceptionHandlerService.handleError( exception as CustomException, diff --git a/packages/twenty-server/src/engine/core-modules/server-route-trigger/exceptions/server-route-trigger.exception.ts b/packages/twenty-server/src/engine/core-modules/server-route-trigger/exceptions/server-route-trigger.exception.ts index e821dd8cb8..5016f5ee26 100644 --- a/packages/twenty-server/src/engine/core-modules/server-route-trigger/exceptions/server-route-trigger.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/server-route-trigger/exceptions/server-route-trigger.exception.ts @@ -5,7 +5,6 @@ import { assertUnreachable } from 'twenty-shared/utils'; import { CustomException } from 'src/utils/custom-exception'; export enum ServerRouteTriggerExceptionCode { - FEATURE_DISABLED = 'FEATURE_DISABLED', LOGIC_FUNCTION_NOT_FOUND = 'LOGIC_FUNCTION_NOT_FOUND', RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED', SERVER_ROUTE_USER_UNCAUGHT_ERROR = 'SERVER_ROUTE_USER_UNCAUGHT_ERROR', @@ -17,8 +16,6 @@ const getServerRouteTriggerExceptionUserFriendlyMessage = ( code: ServerRouteTriggerExceptionCode, ) => { switch (code) { - case ServerRouteTriggerExceptionCode.FEATURE_DISABLED: - return msg`Server logic functions are disabled on this instance.`; case ServerRouteTriggerExceptionCode.LOGIC_FUNCTION_NOT_FOUND: return msg`Server logic function not found.`; case ServerRouteTriggerExceptionCode.RATE_LIMIT_EXCEEDED: diff --git a/packages/twenty-server/src/engine/core-modules/server-route-trigger/server-route-trigger.service.ts b/packages/twenty-server/src/engine/core-modules/server-route-trigger/server-route-trigger.service.ts index 60d8e23ee7..82c8b8ecf8 100644 --- a/packages/twenty-server/src/engine/core-modules/server-route-trigger/server-route-trigger.service.ts +++ b/packages/twenty-server/src/engine/core-modules/server-route-trigger/server-route-trigger.service.ts @@ -20,7 +20,6 @@ import { ServerRouteTriggerException, ServerRouteTriggerExceptionCode, } from 'src/engine/core-modules/server-route-trigger/exceptions/server-route-trigger.exception'; -import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity'; type ResolverResult = { @@ -37,7 +36,6 @@ export class ServerRouteTriggerService { @InjectRepository(LogicFunctionEntity) private readonly logicFunctionRepository: Repository, private readonly logicFunctionExecutorService: LogicFunctionExecutorService, - private readonly twentyConfigService: TwentyConfigService, ) {} async handle({ @@ -47,13 +45,6 @@ export class ServerRouteTriggerService { request: Request; resolverLogicFunctionUniversalIdentifier: string; }): Promise { - if (!this.twentyConfigService.get('IS_SERVER_LOGIC_FUNCTION_ENABLED')) { - throw new ServerRouteTriggerException( - 'Server logic functions are disabled on this instance', - ServerRouteTriggerExceptionCode.FEATURE_DISABLED, - ); - } - const resolver = await this.findResolver({ logicFunctionUniversalIdentifier: resolverLogicFunctionUniversalIdentifier, diff --git a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts index 1b58b790cf..027a6707a2 100644 --- a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts @@ -695,14 +695,6 @@ export class ConfigVariables { @IsAWSRegion() LOGIC_FUNCTION_LAMBDA_LAYER_BUCKET_REGION?: AwsRegion; - @ConfigVariablesMetadata({ - group: ConfigVariablesGroup.LOGIC_FUNCTION_CONFIG, - description: 'Enable instance-level (server) logic functions', - type: ConfigVariableType.BOOLEAN, - }) - @IsOptional() - IS_SERVER_LOGIC_FUNCTION_ENABLED = false; - @ConfigVariablesMetadata({ group: ConfigVariablesGroup.CODE_INTERPRETER_CONFIG, description: