da6f1bbef3
## Summary Rename "Serverless Function" to "Logic Function" across the codebase for clearer naming. ### Environment Variable Changes | Old | New | |-----|-----| | `SERVERLESS_TYPE` | `LOGIC_FUNCTION_TYPE` | | `SERVERLESS_LAMBDA_REGION` | `LOGIC_FUNCTION_LAMBDA_REGION` | | `SERVERLESS_LAMBDA_ROLE` | `LOGIC_FUNCTION_LAMBDA_ROLE` | | `SERVERLESS_LAMBDA_SUBHOSTING_URL` | `LOGIC_FUNCTION_LAMBDA_SUBHOSTING_URL` | | `SERVERLESS_LAMBDA_ACCESS_KEY_ID` | `LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID` | | `SERVERLESS_LAMBDA_SECRET_ACCESS_KEY` | `LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY` | ### Breaking Changes - Environment variables must be updated in production deployments - Database migration renames `serverlessFunction` → `logicFunction` tables
170 lines
4.8 KiB
TypeScript
170 lines
4.8 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
|
|
import { Request } from 'express';
|
|
import { match } from 'path-to-regexp';
|
|
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
|
import { IsNull, Not, Repository } from 'typeorm';
|
|
import { HTTPMethod } from 'twenty-shared/types';
|
|
|
|
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
|
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
|
import {
|
|
RouteTriggerException,
|
|
RouteTriggerExceptionCode,
|
|
} from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception';
|
|
import { buildLogicFunctionEvent } from 'src/engine/metadata-modules/route-trigger/utils/build-logic-function-event.util';
|
|
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
|
|
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/logic-function.service';
|
|
|
|
@Injectable()
|
|
export class RouteTriggerService {
|
|
constructor(
|
|
private readonly accessTokenService: AccessTokenService,
|
|
private readonly logicFunctionService: LogicFunctionService,
|
|
private readonly workspaceDomainsService: WorkspaceDomainsService,
|
|
@InjectRepository(LogicFunctionEntity)
|
|
private readonly logicFunctionRepository: Repository<LogicFunctionEntity>,
|
|
) {}
|
|
|
|
private async getLogicFunctionWithPathParamsOrFail({
|
|
request,
|
|
httpMethod,
|
|
}: {
|
|
request: Request;
|
|
httpMethod: HTTPMethod;
|
|
}): Promise<{
|
|
logicFunction: LogicFunctionEntity;
|
|
pathParams: Partial<Record<string, string | string[]>>;
|
|
}> {
|
|
const host = `${request.protocol}://${request.get('host')}`;
|
|
|
|
const workspace =
|
|
await this.workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace(
|
|
host,
|
|
);
|
|
|
|
assertIsDefinedOrThrow(
|
|
workspace,
|
|
new RouteTriggerException(
|
|
'Workspace not found',
|
|
RouteTriggerExceptionCode.WORKSPACE_NOT_FOUND,
|
|
),
|
|
);
|
|
|
|
const logicFunctionsWithHttpRouteTrigger =
|
|
await this.logicFunctionRepository.find({
|
|
where: {
|
|
workspaceId: workspace.id,
|
|
httpRouteTriggerSettings: Not(IsNull()),
|
|
},
|
|
});
|
|
|
|
const requestPath = request.path.replace(/^\/s\//, '/');
|
|
|
|
for (const logicFunction of logicFunctionsWithHttpRouteTrigger) {
|
|
const httpRouteSettings = logicFunction.httpRouteTriggerSettings;
|
|
|
|
if (
|
|
!isDefined(httpRouteSettings) ||
|
|
httpRouteSettings.httpMethod !== httpMethod
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
const routeMatcher = match(httpRouteSettings.path, {
|
|
decode: decodeURIComponent,
|
|
});
|
|
const routeMatched = routeMatcher(requestPath);
|
|
|
|
if (routeMatched) {
|
|
return {
|
|
logicFunction,
|
|
pathParams: routeMatched.params,
|
|
};
|
|
}
|
|
}
|
|
|
|
throw new RouteTriggerException(
|
|
'No Route trigger found',
|
|
RouteTriggerExceptionCode.TRIGGER_NOT_FOUND,
|
|
);
|
|
}
|
|
|
|
private async validateWorkspaceFromRequest({
|
|
request,
|
|
workspaceId,
|
|
}: {
|
|
request: Request;
|
|
workspaceId: string;
|
|
}) {
|
|
const authContext =
|
|
await this.accessTokenService.validateTokenByRequest(request);
|
|
|
|
if (!isDefined(authContext.workspace)) {
|
|
throw new RouteTriggerException(
|
|
'Workspace not found',
|
|
RouteTriggerExceptionCode.WORKSPACE_NOT_FOUND,
|
|
);
|
|
}
|
|
|
|
if (authContext.workspace.id !== workspaceId) {
|
|
throw new RouteTriggerException(
|
|
'You are not authorized',
|
|
RouteTriggerExceptionCode.FORBIDDEN_EXCEPTION,
|
|
);
|
|
}
|
|
|
|
return authContext;
|
|
}
|
|
|
|
async handle({
|
|
request,
|
|
httpMethod,
|
|
}: {
|
|
request: Request;
|
|
httpMethod: HTTPMethod;
|
|
}) {
|
|
const { logicFunction, pathParams } =
|
|
await this.getLogicFunctionWithPathParamsOrFail({
|
|
request,
|
|
httpMethod,
|
|
});
|
|
|
|
const httpRouteSettings = logicFunction.httpRouteTriggerSettings;
|
|
|
|
if (httpRouteSettings?.isAuthRequired) {
|
|
await this.validateWorkspaceFromRequest({
|
|
request,
|
|
workspaceId: logicFunction.workspaceId,
|
|
});
|
|
}
|
|
|
|
const event = buildLogicFunctionEvent({
|
|
request,
|
|
pathParameters: pathParams,
|
|
forwardedRequestHeaders: httpRouteSettings?.forwardedRequestHeaders ?? [],
|
|
});
|
|
|
|
const result = await this.logicFunctionService.executeOneLogicFunction({
|
|
id: logicFunction.id,
|
|
workspaceId: logicFunction.workspaceId,
|
|
payload: event,
|
|
version: 'draft',
|
|
});
|
|
|
|
if (!isDefined(result)) {
|
|
return result;
|
|
}
|
|
|
|
if (result.error) {
|
|
throw new RouteTriggerException(
|
|
result.error.errorMessage,
|
|
RouteTriggerExceptionCode.LOGIC_FUNCTION_EXECUTION_ERROR,
|
|
);
|
|
}
|
|
|
|
return result.data;
|
|
}
|
|
}
|