Add serverless function in twenty-cli (#14819)

- add serverlessFunction schema in twenty-cli
- add trigger schema in twenty-cli
- update serverless function code save and get
- sync serverless function
This commit is contained in:
martmull
2025-10-01 22:11:03 +02:00
committed by GitHub
parent 64dc726f50
commit 6fdd7894a1
47 changed files with 853 additions and 159 deletions
@@ -3,11 +3,15 @@ import { Field, HideField, InputType } from '@nestjs/graphql';
import {
IsNotEmpty,
IsNumber,
IsObject,
IsOptional,
IsString,
Max,
Min,
} from 'class-validator';
import graphqlTypeJson from 'graphql-type-json';
import { ServerlessFunctionCode } from 'src/engine/metadata-modules/serverless-function/types/serverless-function-code.type';
@InputType()
export class CreateServerlessFunctionInput {
@@ -31,6 +35,14 @@ export class CreateServerlessFunctionInput {
@HideField()
applicationId?: string;
@HideField()
universalIdentifier?: string;
@HideField()
serverlessFunctionLayerId?: string;
@Field(() => graphqlTypeJson, { nullable: true })
@IsObject()
@IsOptional()
code?: ServerlessFunctionCode;
}
@@ -13,6 +13,7 @@ import {
import graphqlTypeJson from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ServerlessFunctionCode } from 'src/engine/metadata-modules/serverless-function/types/serverless-function-code.type';
@InputType()
export class UpdateServerlessFunctionInput {
@@ -41,5 +42,5 @@ export class UpdateServerlessFunctionInput {
@Field(() => graphqlTypeJson)
@IsObject()
code: JSON;
code: ServerlessFunctionCode;
}
@@ -39,6 +39,6 @@ import { ServerlessFunctionLayerModule } from 'src/engine/metadata-modules/serve
ServerlessFunctionResolver,
WorkspaceFlatServerlessFunctionMapCacheService,
],
exports: [ServerlessFunctionService],
exports: [ServerlessFunctionService, ServerlessFunctionV2Service],
})
export class ServerlessFunctionModule {}
@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { basename, dirname, join } from 'path';
import { join } from 'path';
import deepEqual from 'deep-equal';
import { isDefined } from 'twenty-shared/utils';
@@ -13,9 +13,6 @@ import { type ServerlessExecuteResult } from 'src/engine/core-modules/serverless
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { SERVERLESS_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/serverless-function/serverless-function-executed';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content';
import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name';
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files';
import { getLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-layer-dependencies';
import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service';
@@ -34,6 +31,7 @@ import {
WorkflowVersionStepExceptionCode,
} from 'src/modules/workflow/common/exceptions/workflow-version-step.exception';
import { ServerlessFunctionLayerService } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service';
import { Sources } from 'src/engine/core-modules/file-storage/types/source.type';
@Injectable()
export class ServerlessFunctionService {
@@ -66,7 +64,7 @@ export class ServerlessFunctionService {
workspaceId: string,
id: string,
version: string,
): Promise<{ [filePath: string]: string } | undefined> {
): Promise<Sources | undefined> {
const serverlessFunction =
await this.serverlessFunctionRepository.findOneOrFail({
where: {
@@ -81,20 +79,7 @@ export class ServerlessFunctionService {
version,
});
const indexFileStream = await this.fileStorageService.read({
folderPath: join(folderPath, 'src'),
filename: INDEX_FILE_NAME,
});
const envFileStream = await this.fileStorageService.read({
folderPath: folderPath,
filename: ENV_FILE_NAME,
});
return {
'.env': await readFileContent(envFileStream),
'src/index.ts': await readFileContent(indexFileStream),
};
return await this.fileStorageService.readFolder(folderPath);
} catch (error) {
if (error.code === FileStorageExceptionCode.FILE_NOT_FOUND) {
return;
@@ -289,15 +274,10 @@ export class ServerlessFunctionService {
version: 'draft',
});
for (const key of Object.keys(serverlessFunctionInput.code)) {
await this.fileStorageService.write({
// @ts-expect-error legacy noImplicitAny
file: serverlessFunctionInput.code[key],
name: basename(key),
mimeType: undefined,
folder: join(fileFolder, dirname(key)),
});
}
await this.fileStorageService.writeFolder(
serverlessFunctionInput.code,
fileFolder,
);
return this.serverlessFunctionRepository.findOneBy({
id: existingServerlessFunction.id,
@@ -168,9 +168,11 @@ export class ServerlessFunctionV2Service {
async deleteOne({
deleteServerlessFunctionInput,
workspaceId,
isSystemBuild = false,
}: {
deleteServerlessFunctionInput: ServerlessFunctionIdInput;
workspaceId: string;
isSystemBuild?: boolean;
}): Promise<FlatServerlessFunction> {
const { flatServerlessFunctionMaps: existingFlatServerlessFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
@@ -211,8 +213,8 @@ export class ServerlessFunctionV2Service {
},
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
isSystemBuild,
},
workspaceId,
},
@@ -244,9 +246,11 @@ export class ServerlessFunctionV2Service {
async destroyOne({
destroyServerlessFunctionInput,
workspaceId,
isSystemBuild = false,
}: {
destroyServerlessFunctionInput: ServerlessFunctionIdInput;
workspaceId: string;
isSystemBuild?: boolean;
}): Promise<FlatServerlessFunction> {
const { flatServerlessFunctionMaps: existingFlatServerlessFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
@@ -288,7 +292,7 @@ export class ServerlessFunctionV2Service {
},
},
buildOptions: {
isSystemBuild: false,
isSystemBuild,
inferDeletionFromMissingEntities: true,
},
workspaceId,
@@ -6,6 +6,7 @@ import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serve
import { type ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
import { type ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { type ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
import { type ServerlessFunctionCode } from 'src/engine/metadata-modules/serverless-function/types/serverless-function-code.type';
export type ServerlessFunctionEntityRelationProperties =
ExtractRecordTypeOrmRelationProperties<
@@ -23,5 +24,5 @@ export type FlatServerlessFunction = Omit<
ServerlessFunctionEntityRelationProperties
> & {
universalIdentifier: string;
code?: JSON;
code?: ServerlessFunctionCode;
};
@@ -0,0 +1,8 @@
import { type Sources } from 'src/engine/core-modules/file-storage/types/source.type';
export type ServerlessFunctionCode = {
src: {
'index.ts': string;
} & Sources;
'.env'?: string;
};
@@ -4,6 +4,7 @@ import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/l
import { type CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
import { ServerlessFunctionRuntime } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils';
export const fromCreateServerlessFunctionInputToFlatServerlessFunction = ({
createServerlessFunctionInput,
@@ -13,14 +14,14 @@ export const fromCreateServerlessFunctionInputToFlatServerlessFunction = ({
workspaceId: string;
}): FlatServerlessFunction => {
const id = v4();
const universalIdentifier = v4();
const currentDate = new Date();
return {
id,
name: createServerlessFunctionInput.name,
description: createServerlessFunctionInput.description ?? null,
universalIdentifier,
universalIdentifier:
createServerlessFunctionInput.universalIdentifier ?? v4(),
createdAt: currentDate,
updatedAt: currentDate,
deletedAt: null,
@@ -34,6 +35,11 @@ export const fromCreateServerlessFunctionInputToFlatServerlessFunction = ({
serverlessFunctionLayerId:
createServerlessFunctionInput.serverlessFunctionLayerId ?? null,
workspaceId,
checksum: null,
code: createServerlessFunctionInput?.code,
checksum: createServerlessFunctionInput?.code
? serverlessFunctionCreateHash(
JSON.stringify(createServerlessFunctionInput.code),
)
: null,
};
};
@@ -13,8 +13,8 @@ import {
ServerlessFunctionExceptionCode,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
import { serverlessFunctionCreateCodeChecksum } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-code-checksum.utils';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils';
export const fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOrThrow =
({
@@ -42,8 +42,8 @@ export const fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOr
const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields(
{
...rawUpdateServerlessFunctionInput,
checksum: serverlessFunctionCreateCodeChecksum(
rawUpdateServerlessFunctionInput.code,
checksum: serverlessFunctionCreateHash(
JSON.stringify(rawUpdateServerlessFunctionInput.code),
),
},
FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES,
@@ -1,8 +1,11 @@
import { isDefined } from 'twenty-shared/utils';
import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils';
import { type ServerlessFunctionCode } from 'src/engine/metadata-modules/serverless-function/types/serverless-function-code.type';
export const serverlessFunctionCreateCodeChecksum = (code: JSON): string => {
export const serverlessFunctionCreateCodeChecksum = (
code: ServerlessFunctionCode,
): string => {
if (!isDefined(code) || typeof code !== 'object') {
return serverlessFunctionCreateHash('');
}