Add ServerlessFunction to migration v2 (#14698)
This commit is contained in:
+7
@@ -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)[];
|
||||
+6
@@ -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)[];
|
||||
+27
-12
@@ -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;
|
||||
}
|
||||
|
||||
+9
-1
@@ -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],
|
||||
})
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+369
@@ -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;
|
||||
}
|
||||
}
|
||||
+71
@@ -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;
|
||||
}
|
||||
}
|
||||
+4
@@ -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];
|
||||
+19
@@ -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;
|
||||
};
|
||||
+59
@@ -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 [];
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
+36
@@ -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,
|
||||
};
|
||||
};
|
||||
+51
@@ -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,
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user