Add ServerlessFunction to migration v2 (#14698)

This commit is contained in:
Weiko
2025-09-25 14:05:01 +02:00
committed by GitHub
parent edb331d68b
commit 188d66909d
45 changed files with 1671 additions and 112 deletions
@@ -0,0 +1,7 @@
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
export const FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES = [
'name',
'description',
'timeoutSeconds',
] as const satisfies (keyof FlatServerlessFunction)[];
@@ -0,0 +1,6 @@
import { FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-editable-properties.constant';
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
export const FLAT_SERVERLESS_FUNCTION_PROPERTIES_TO_COMPARE = [
...FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES,
] as const satisfies (keyof FlatServerlessFunction)[];
@@ -5,15 +5,18 @@ import {
DeleteDateColumn,
Entity,
Index,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
OneToMany,
} from 'typeorm';
import { InputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/input-schema.type';
import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/interfaces/syncable-entity.interface';
import { Route } from 'src/engine/metadata-modules/route/route.entity';
import { ServerlessFunctionEntityRelationProperties } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
import { CronTrigger } from 'src/engine/metadata-modules/trigger/entities/cron-trigger.entity';
import { DatabaseEventTrigger } from 'src/engine/metadata-modules/trigger/entities/database-event-trigger.entity';
import { Route } from 'src/engine/metadata-modules/route/route.entity';
import { InputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/input-schema.type';
const DEFAULT_SERVERLESS_TIMEOUT_SECONDS = 300; // 5 minutes
@@ -22,26 +25,35 @@ export enum ServerlessFunctionRuntime {
NODE22 = 'nodejs22.x',
}
export const SERVERLESS_FUNCTION_ENTITY_RELATION_PROPERTIES = [
'cronTriggers',
'databaseEventTriggers',
'routes',
] as const satisfies readonly ServerlessFunctionEntityRelationProperties[];
@Entity('serverlessFunction')
@Index('IDX_SERVERLESS_FUNCTION_ID_DELETED_AT', ['id', 'deletedAt'])
export class ServerlessFunctionEntity {
export class ServerlessFunctionEntity
extends SyncableEntity
implements Required<ServerlessFunctionEntity>
{
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false })
name: string;
@Column({ nullable: true })
description: string;
@Column({ nullable: true, type: 'varchar' })
description: string | null;
@Column({ nullable: true })
latestVersion: string;
@Column({ nullable: true, type: 'varchar' })
latestVersion: string | null;
@Column({ nullable: false, type: 'jsonb', default: [] })
publishedVersions: string[];
@Column({ nullable: true, type: 'jsonb' })
latestVersionInputSchema: InputSchema;
latestVersionInputSchema: InputSchema | null;
@Column({ nullable: false, default: ServerlessFunctionRuntime.NODE22 })
runtime: ServerlessFunctionRuntime;
@@ -50,12 +62,15 @@ export class ServerlessFunctionEntity {
@Check(`"timeoutSeconds" >= 1 AND "timeoutSeconds" <= 900`)
timeoutSeconds: number;
@Column({ nullable: true })
layerVersion: number;
@Column({ nullable: true, type: 'integer' })
layerVersion: number | null;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@Column({ nullable: true, type: 'uuid' })
applicationId: string | null;
@OneToMany(
() => CronTrigger,
(cronTrigger) => cronTrigger.serverlessFunction,
@@ -86,5 +101,5 @@ export class ServerlessFunctionEntity {
updatedAt: Date;
@DeleteDateColumn({ type: 'timestamptz' })
deletedAt?: Date;
deletedAt: Date | null;
}
@@ -4,15 +4,19 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/core-modules/common/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
import { ServerlessFunctionTriggerJob } from 'src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { ServerlessFunctionResolver } from 'src/engine/metadata-modules/serverless-function/serverless-function.resolver';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { ServerlessFunctionTriggerJob } from 'src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job';
import { ServerlessFunctionV2Service } from 'src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service';
import { WorkspaceFlatServerlessFunctionMapCacheService } from 'src/engine/metadata-modules/serverless-function/services/workspace-flat-serverless-function-map-cache.service';
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
@Module({
imports: [
@@ -23,11 +27,15 @@ import { ServerlessFunctionTriggerJob } from 'src/engine/metadata-modules/server
ThrottlerModule,
AuditModule,
FeatureFlagModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationV2Module,
],
providers: [
ServerlessFunctionService,
ServerlessFunctionV2Service,
ServerlessFunctionTriggerJob,
ServerlessFunctionResolver,
WorkspaceFlatServerlessFunctionMapCacheService,
],
exports: [ServerlessFunctionService],
})
@@ -420,7 +420,7 @@ export class ServerlessFunctionService {
const newServerlessFunction = await this.createOneServerlessFunction(
{
name: serverlessFunctionToDuplicate.name,
description: serverlessFunctionToDuplicate.description,
description: serverlessFunctionToDuplicate.description ?? undefined,
timeoutSeconds: serverlessFunctionToDuplicate.timeoutSeconds,
},
workspaceId,
@@ -0,0 +1,369 @@
import { Injectable } from '@nestjs/common';
import { basename, dirname, join } from 'path';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/core-modules/common/services/workspace-many-or-all-flat-entity-maps-cache.service.';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { getSubFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/get-sub-flat-entity-maps-or-throw.util';
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files';
import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service';
import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
import { ServerlessFunctionIdInput } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-id.input';
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import {
ServerlessFunctionException,
ServerlessFunctionExceptionCode,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
import { fromCreateServerlessFunctionInputToFlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util';
import { fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOrThrow } from 'src/engine/metadata-modules/serverless-function/utils/from-update-serverless-function-input-to-flat-serverless-function-to-update-or-throw.util';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class ServerlessFunctionV2Service {
constructor(
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly fileStorageService: FileStorageService,
private readonly serverlessService: ServerlessService,
) {}
async createOne(
serverlessFunctionInput: CreateServerlessFunctionInput,
workspaceId: string,
) {
const flatEntityMaps =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatServerlessFunctionMaps'],
},
);
const existingFlatServerlessFunctionMaps =
flatEntityMaps.flatServerlessFunctionMaps;
const flatServerlessFunctionToCreate =
fromCreateServerlessFunctionInputToFlatServerlessFunction({
createServerlessFunctionInput: serverlessFunctionInput,
workspaceId,
});
const toFlatServerlessFunctionMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: flatServerlessFunctionToCreate,
flatEntityMaps: existingFlatServerlessFunctionMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
workspaceId,
fromToAllFlatEntityMaps: {
flatServerlessFunctionMaps: {
from: existingFlatServerlessFunctionMaps,
to: toFlatServerlessFunctionMaps,
},
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
},
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while creating serverless function',
);
}
const updatedFlatEntityMaps =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatServerlessFunctionMaps'],
},
);
const createdServerlessFunction =
updatedFlatEntityMaps.flatServerlessFunctionMaps.byId[
flatServerlessFunctionToCreate.id
];
if (!isDefined(createdServerlessFunction)) {
throw new ServerlessFunctionException(
'Created serverless function not found in recomputed cache',
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
);
}
const draftFileFolder = getServerlessFolder({
serverlessFunction: createdServerlessFunction,
version: 'draft',
});
for (const file of await getBaseTypescriptProjectFiles) {
await this.fileStorageService.write({
file: file.content,
name: file.name,
mimeType: undefined,
folder: join(draftFileFolder, file.path),
});
}
return createdServerlessFunction;
}
async updateOne(
serverlessFunctionInput: UpdateServerlessFunctionInput,
workspaceId: string,
) {
const flatEntityMaps =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatServerlessFunctionMaps'],
},
);
const existingFlatServerlessFunctionMaps =
flatEntityMaps.flatServerlessFunctionMaps;
const optimisticallyUpdatedFlatServerlessFunction =
fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOrThrow({
flatServerlessFunctionMaps: existingFlatServerlessFunctionMaps,
updateServerlessFunctionInput: serverlessFunctionInput,
});
const fromFlatServerlessFunctionMaps = getSubFlatEntityMapsOrThrow({
flatEntityIds: [optimisticallyUpdatedFlatServerlessFunction.id],
flatEntityMaps: existingFlatServerlessFunctionMaps,
});
const toFlatServerlessFunctionMaps =
replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: optimisticallyUpdatedFlatServerlessFunction,
flatEntityMaps: fromFlatServerlessFunctionMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
workspaceId,
fromToAllFlatEntityMaps: {
flatServerlessFunctionMaps: {
from: existingFlatServerlessFunctionMaps,
to: toFlatServerlessFunctionMaps,
},
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
},
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while updating serverless function',
);
}
const updatedFlatEntityMaps =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatServerlessFunctionMaps'],
},
);
const updatedFlatServerlessFunction =
updatedFlatEntityMaps.flatServerlessFunctionMaps.byId[
optimisticallyUpdatedFlatServerlessFunction.id
];
if (!isDefined(updatedFlatServerlessFunction)) {
throw new ServerlessFunctionException(
'Updated serverless function not found in recomputed cache',
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
);
}
const fileFolder = getServerlessFolder({
serverlessFunction: updatedFlatServerlessFunction,
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)),
});
}
return updatedFlatServerlessFunction;
}
async deleteOne({
deleteServerlessFunctionInput,
workspaceId,
}: {
deleteServerlessFunctionInput: ServerlessFunctionIdInput;
workspaceId: string;
}): Promise<FlatServerlessFunction> {
const { flatServerlessFunctionMaps: existingFlatServerlessFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatServerlessFunctionMaps'],
},
);
const existingFlatServerlessFunction =
existingFlatServerlessFunctionMaps.byId[deleteServerlessFunctionInput.id];
if (!isDefined(existingFlatServerlessFunction)) {
throw new ServerlessFunctionException(
'Serverless function to delete not found',
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
);
}
const optimisticallyUpdatedFlatServerlessFunctionWithDeletedAt = {
...existingFlatServerlessFunction,
deletedAt: new Date(),
};
const toFlatServerlessFunctionMaps =
replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: optimisticallyUpdatedFlatServerlessFunctionWithDeletedAt,
flatEntityMaps: existingFlatServerlessFunctionMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromToAllFlatEntityMaps: {
flatServerlessFunctionMaps: {
from: existingFlatServerlessFunctionMaps,
to: toFlatServerlessFunctionMaps,
},
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
},
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while deleting serverless function',
);
}
const {
flatServerlessFunctionMaps: recomputedExistingFlatServerlessFunctionMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatServerlessFunctionMaps'],
},
);
this.serverlessService.delete(
existingFlatServerlessFunction as ServerlessFunctionEntity,
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: optimisticallyUpdatedFlatServerlessFunctionWithDeletedAt.id,
flatEntityMaps: recomputedExistingFlatServerlessFunctionMaps,
});
}
async destroyOne({
destroyServerlessFunctionInput,
workspaceId,
}: {
destroyServerlessFunctionInput: ServerlessFunctionIdInput;
workspaceId: string;
}): Promise<FlatServerlessFunction> {
const { flatServerlessFunctionMaps: existingFlatServerlessFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatServerlessFunctionMaps'],
},
);
const existingFlatServerlessFunction =
existingFlatServerlessFunctionMaps.byId[
destroyServerlessFunctionInput.id
];
if (!isDefined(existingFlatServerlessFunction)) {
throw new ServerlessFunctionException(
'Serverless function to destroy not found',
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
);
}
const fromFlatServerlessFunctionMaps = getSubFlatEntityMapsOrThrow({
flatEntityIds: [existingFlatServerlessFunction.id],
flatEntityMaps: existingFlatServerlessFunctionMaps,
});
const toFlatServerlessFunctionMaps =
deleteFlatEntityFromFlatEntityMapsOrThrow({
flatEntityMaps: fromFlatServerlessFunctionMaps,
entityToDeleteId: existingFlatServerlessFunction.id,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromToAllFlatEntityMaps: {
flatServerlessFunctionMaps: {
from: fromFlatServerlessFunctionMaps,
to: toFlatServerlessFunctionMaps,
},
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: true,
},
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while destroying serverless function',
);
}
this.fileStorageService.delete({
folderPath: getServerlessFolder({
serverlessFunction: existingFlatServerlessFunction,
}),
});
return existingFlatServerlessFunction;
}
}
@@ -0,0 +1,71 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/core-modules/common/constant/empty-flat-entity-maps.constant';
import { FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import {
SERVERLESS_FUNCTION_ENTITY_RELATION_PROPERTIES,
ServerlessFunctionEntity,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator';
import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service';
@Injectable()
@WorkspaceFlatMapCache('flatServerlessFunctionMaps')
export class WorkspaceFlatServerlessFunctionMapCacheService extends WorkspaceFlatMapCacheService<
FlatEntityMaps<FlatServerlessFunction>
> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(ServerlessFunctionEntity)
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
) {
super(cacheStorageService);
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatEntityMaps<FlatServerlessFunction>> {
const serverlessFunctions = await this.serverlessFunctionRepository.find({
where: {
workspaceId,
},
withDeleted: true,
});
const flatServerlessFunctionMaps = serverlessFunctions.reduce<
FlatEntityMaps<FlatServerlessFunction>
>((flatEntityMaps, serverlessFunction) => {
const flatServerlessFunction = {
...removePropertiesFromRecord(serverlessFunction, [
...SERVERLESS_FUNCTION_ENTITY_RELATION_PROPERTIES,
]),
universalIdentifier: serverlessFunction.universalIdentifier ?? '',
} satisfies FlatServerlessFunction;
return {
byId: {
...flatEntityMaps.byId,
[flatServerlessFunction.id]: flatServerlessFunction,
},
idByUniversalIdentifier: {
...flatEntityMaps.idByUniversalIdentifier,
[flatServerlessFunction.universalIdentifier]:
flatServerlessFunction.id,
},
};
}, EMPTY_FLAT_ENTITY_MAPS);
return flatServerlessFunctionMaps;
}
}
@@ -0,0 +1,4 @@
import { type FLAT_SERVERLESS_FUNCTION_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-properties-to-compare.constant';
export type FlatServerlessFunctionPropertiesToCompare =
(typeof FLAT_SERVERLESS_FUNCTION_PROPERTIES_TO_COMPARE)[number];
@@ -0,0 +1,19 @@
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { type Route } from 'src/engine/metadata-modules/route/route.entity';
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { type CronTrigger } from 'src/engine/metadata-modules/trigger/entities/cron-trigger.entity';
import { type DatabaseEventTrigger } from 'src/engine/metadata-modules/trigger/entities/database-event-trigger.entity';
import { type ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
export type ServerlessFunctionEntityRelationProperties =
ExtractRecordTypeOrmRelationProperties<
ServerlessFunctionEntity,
CronTrigger | DatabaseEventTrigger | Route | Workspace
>;
export type FlatServerlessFunction = Omit<
ServerlessFunctionEntity,
ServerlessFunctionEntityRelationProperties
> & {
universalIdentifier: string;
};
@@ -0,0 +1,59 @@
import diff from 'microdiff';
import { type FromTo } from 'twenty-shared/types';
import { FLAT_SERVERLESS_FUNCTION_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-properties-to-compare.constant';
import { type FlatServerlessFunctionPropertiesToCompare } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function-properties-to-compare.type';
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
import { type UpdateServerlessFunctionAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-serverless-function-action-v2.type';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
type GetWorkspaceMigrationUpdateServerlessFunctionActionArgs = FromTo<
FlatServerlessFunction,
'FlatServerlessFunction'
>;
export const compareTwoFlatServerlessFunction = ({
fromFlatServerlessFunction,
toFlatServerlessFunction,
}: GetWorkspaceMigrationUpdateServerlessFunctionActionArgs) => {
const transformMetadataForComparisonParameters = {
shouldIgnoreProperty: (property: string) =>
!FLAT_SERVERLESS_FUNCTION_PROPERTIES_TO_COMPARE.includes(
property as FlatServerlessFunctionPropertiesToCompare,
),
propertiesToStringify: [],
};
const fromCompare = transformMetadataForComparison(
fromFlatServerlessFunction,
transformMetadataForComparisonParameters,
);
const toCompare = transformMetadataForComparison(
toFlatServerlessFunction,
transformMetadataForComparisonParameters,
);
const flatServerlessFunctionDifferences = diff(fromCompare, toCompare);
return flatServerlessFunctionDifferences.flatMap<
UpdateServerlessFunctionAction['updates'][number]
>((difference) => {
switch (difference.type) {
case 'CHANGE': {
const { oldValue, path, value } = difference;
const property = path[0] as FlatServerlessFunctionPropertiesToCompare;
return {
from: oldValue,
to: value,
property,
};
}
case 'CREATE':
case 'REMOVE':
default: {
// Should never occurs, we should only provide null never undefined and so on
return [];
}
}
});
};
@@ -0,0 +1,36 @@
import { v4 } from 'uuid';
import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version';
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';
export const fromCreateServerlessFunctionInputToFlatServerlessFunction = ({
createServerlessFunctionInput,
workspaceId,
}: {
createServerlessFunctionInput: CreateServerlessFunctionInput;
workspaceId: string;
}): FlatServerlessFunction => {
const id = v4();
const universalIdentifier = v4();
const currentDate = new Date();
return {
id,
name: createServerlessFunctionInput.name,
description: createServerlessFunctionInput.description ?? null,
universalIdentifier,
createdAt: currentDate,
updatedAt: currentDate,
deletedAt: null,
latestVersion: null,
publishedVersions: [],
applicationId: null,
latestVersionInputSchema: null,
runtime: ServerlessFunctionRuntime.NODE22,
timeoutSeconds: createServerlessFunctionInput.timeoutSeconds ?? 300,
layerVersion: LAST_LAYER_VERSION,
workspaceId,
};
};
@@ -0,0 +1,51 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
} from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-editable-properties.constant';
import { type UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
import {
ServerlessFunctionException,
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 { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
export const fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOrThrow =
({
updateServerlessFunctionInput: rawUpdateServerlessFunctionInput,
flatServerlessFunctionMaps,
}: {
updateServerlessFunctionInput: UpdateServerlessFunctionInput;
flatServerlessFunctionMaps: FlatEntityMaps<FlatServerlessFunction>;
}): FlatServerlessFunction => {
const { id: serverlessFunctionToUpdateId } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawUpdateServerlessFunctionInput,
['id'],
);
const existingFlatServerlessFunctionToUpdate =
flatServerlessFunctionMaps.byId[serverlessFunctionToUpdateId];
if (!isDefined(existingFlatServerlessFunctionToUpdate)) {
throw new ServerlessFunctionException(
t`Serverless function to update not found`,
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
);
}
const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields(
rawUpdateServerlessFunctionInput,
FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES,
);
return mergeUpdateInExistingRecord({
existing: existingFlatServerlessFunctionToUpdate,
properties: FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES,
update: updatedEditableFieldProperties,
});
};