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,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;
}
}