8723 workflow add editor in serverless function code step (#8805)

- create a serverless function when creating a new workflow code step
- add code editor in workflow code step
- move workflowVersion steps management from frontend to backend
  - add a custom resolver for workflow-version management
  - fix optimistic rendering on frontend
- fix css
- delete serverless function when deleting workflow code step

TODO
- Don't update serverlessFunction if no code change
- Factorize what can be between crud trigger and crud step
- Publish serverless version when activating workflow
- delete serverless functions when deleting workflow or workflowVersion
- fix optimistic rendering for code updates
- Unify CRUD types

<img width="1279" alt="image"
src="https://github.com/user-attachments/assets/3d97ee9f-4b96-4abc-9d36-5c0280058be4">
This commit is contained in:
martmull
2024-12-03 09:41:13 +01:00
committed by GitHub
parent 9d7632cb4f
commit d0ff1ffd5f
75 changed files with 2192 additions and 1527 deletions

View File

@ -0,0 +1,33 @@
import { Scope } from '@nestjs/common';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
export type DeleteServerlessFunctionBatchEvent = {
ids: string[];
workspaceId: string;
};
@Processor({
queueName: MessageQueue.serverlessFunctionQueue,
scope: Scope.REQUEST,
})
export class DeleteServerlessFunctionJob {
constructor(
private readonly serverlessFunctionService: ServerlessFunctionService,
) {}
@Process(DeleteServerlessFunctionJob.name)
async handle(batchEvent: DeleteServerlessFunctionBatchEvent): Promise<void> {
await Promise.all(
batchEvent.ids.map((id) =>
this.serverlessFunctionService.deleteOneServerlessFunction(
id,
batchEvent.workspaceId,
),
),
);
}
}

View File

@ -1,59 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { join } from 'path';
import { Repository } from 'typeorm';
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
import { SERVERLESS_FUNCTION_PUBLISHED } from 'src/engine/metadata-modules/serverless-function/constants/serverless-function-published';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service';
import { OnCustomBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-custom-batch-event.decorator';
@Injectable()
export class ServerlessFunctionPublicationListener {
constructor(
private readonly serverlessFunctionService: ServerlessFunctionService,
private readonly codeIntrospectionService: CodeIntrospectionService,
@InjectRepository(ServerlessFunctionEntity, 'metadata')
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
) {}
@OnCustomBatchEvent(SERVERLESS_FUNCTION_PUBLISHED)
async handle(
batchEvent: WorkspaceEventBatch<{
serverlessFunctionId: string;
serverlessFunctionVersion: string;
}>,
): Promise<void> {
for (const event of batchEvent.events) {
const sourceCode =
await this.serverlessFunctionService.getServerlessFunctionSourceCode(
batchEvent.workspaceId,
event.serverlessFunctionId,
event.serverlessFunctionVersion,
);
if (!sourceCode) {
return;
}
const indexCode = sourceCode[join('src', INDEX_FILE_NAME)];
if (!indexCode) {
return;
}
const latestVersionInputSchema =
this.codeIntrospectionService.getFunctionInputSchema(indexCode);
await this.serverlessFunctionRepository.update(
{ id: event.serverlessFunctionId },
{ latestVersionInputSchema },
);
}
}
}

View File

@ -8,11 +8,9 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-
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 { ServerlessFunctionPublicationListener } from 'src/engine/metadata-modules/serverless-function/listeners/serverless-function-publication.listener';
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 { CodeIntrospectionModule } from 'src/modules/code-introspection/code-introspection.module';
@Module({
imports: [
@ -22,13 +20,8 @@ import { CodeIntrospectionModule } from 'src/modules/code-introspection/code-int
FileModule,
ThrottlerModule,
AnalyticsModule,
CodeIntrospectionModule,
],
providers: [
ServerlessFunctionService,
ServerlessFunctionResolver,
ServerlessFunctionPublicationListener,
],
providers: [ServerlessFunctionService, ServerlessFunctionResolver],
exports: [ServerlessFunctionService],
})
export class ServerlessFunctionModule {}

View File

@ -84,11 +84,14 @@ export class ServerlessFunctionResolver {
}
@Query(() => graphqlTypeJson)
async getAvailablePackages(@AuthWorkspace() { id: workspaceId }: Workspace) {
async getAvailablePackages(
@Args('input') { id }: ServerlessFunctionIdInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
try {
await this.checkFeatureFlag(workspaceId);
return await this.serverlessFunctionService.getAvailablePackages();
return await this.serverlessFunctionService.getAvailablePackages(id);
} catch (error) {
serverlessFunctionGraphQLApiExceptionHandler(error);
}

View File

@ -17,11 +17,9 @@ import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/consta
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version';
import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files';
import { getLastLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-layer-dependencies';
import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service';
import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
import { SERVERLESS_FUNCTION_PUBLISHED } from 'src/engine/metadata-modules/serverless-function/constants/serverless-function-published';
import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
import {
@ -32,8 +30,8 @@ import {
ServerlessFunctionException,
ServerlessFunctionExceptionCode,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import { isDefined } from 'src/utils/is-defined';
import { getLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-layer-dependencies';
@Injectable()
export class ServerlessFunctionService {
@ -45,7 +43,6 @@ export class ServerlessFunctionService {
private readonly throttlerService: ThrottlerService,
private readonly environmentService: EnvironmentService,
private readonly analyticsService: AnalyticsService,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
) {}
async findManyServerlessFunctions(where) {
@ -194,17 +191,6 @@ export class ServerlessFunctionService {
},
);
this.workspaceEventEmitter.emitCustomBatchEvent(
SERVERLESS_FUNCTION_PUBLISHED,
[
{
serverlessFunctionId: existingServerlessFunction.id,
serverlessFunctionVersion: newVersion,
},
],
workspaceId,
);
return this.serverlessFunctionRepository.findOneBy({
id: existingServerlessFunction.id,
});
@ -290,8 +276,14 @@ export class ServerlessFunctionService {
});
}
async getAvailablePackages() {
const { packageJson, yarnLock } = await getLastLayerDependencies();
async getAvailablePackages(serverlessFunctionId: string) {
const serverlessFunction =
await this.serverlessFunctionRepository.findOneBy({
id: serverlessFunctionId,
});
const { packageJson, yarnLock } = await getLayerDependencies(
serverlessFunction?.layerVersion || 'latest',
);
const packageVersionRegex = /^"([^@]+)@.*?":\n\s+version: (.+)$/gm;
const versions: Record<string, string> = {};