From 47ddc7be83c8ad1c2f8a6863eabd27b19c95c785 Mon Sep 17 00:00:00 2001 From: martmull Date: Wed, 17 Jul 2024 17:53:01 +0200 Subject: [PATCH] 6181 workflows create a custom code executor (#6235) Closes #6181 ## Testing - download Altair graphql dev tool https://altairgraphql.dev/#download - create a file locally `test.ts` containing: ``` export const handler = async (event: object, context: object) => { return { test: 'toto', data: event['data'] }; } ``` - play those requests in Altair: mutation UpsertFunction($file: Upload!) { upsertFunction(name: "toto", file: $file) } mutation ExecFunction { executeFunction(name:"toto", payload: {data: "titi"}) } - it will run the local driver, add those env variable to test with lambda driver ``` CUSTOM_CODE_ENGINE_DRIVER_TYPE=lambda LAMBDA_REGION=eu-west-2 LAMBDA_ROLE= ``` --- package.json | 4 + .../src/generated-metadata/graphql.ts | 95 ++ .../twenty-front/src/generated/graphql.tsx | 38 + packages/twenty-server/.env.example | 3 +- ...210534680-createServerlessFunctionTable.ts | 23 + .../engine/core-modules/core-engine.module.ts | 4 +- .../file/interfaces/file-folder.interface.ts | 1 + .../environment/environment-variables.ts | 25 + .../file-storage/utils/read-file-content.ts | 11 + .../integrations/integrations.module.ts | 12 + .../drivers/base-serverless.driver.ts | 33 + .../drivers/constants/build-file-name.ts | 1 + .../drivers/constants/source-file-name.ts | 1 + .../interfaces/serverless-driver.interface.ts | 9 + .../serverless/drivers/lambda.driver.ts | 100 ++ .../serverless/drivers/local.driver.ts | 94 ++ .../build-directory-manager.service.ts | 48 + .../drivers/utils/compile-typescript.ts | 19 + .../drivers/utils/create-zip-file.ts | 21 + .../serverless/serverless-module.factory.ts | 59 + .../serverless/serverless.constants.ts | 1 + .../serverless/serverless.interface.ts | 30 + .../serverless/serverless.module.ts | 35 + .../serverless/serverless.service.ts | 22 + .../metadata-engine.module.ts | 3 + .../dtos/execute-serverless-function.input.ts | 20 + ...verless-function-execution-result-d-t.o.ts | 13 + .../dtos/serverless-function.dto.ts | 70 + .../serverless-function.entity.ts | 43 + .../serverless-function.exception.ts | 14 + .../serverless-function.module.ts | 50 + .../serverless-function.resolver.ts | 59 + .../serverless-function.service.ts | 112 ++ .../serverless-function-create-hash.utils.ts | 8 + ...ion-graphql-api-exception-handler.utils.ts | 26 + .../self-hosting/self-hosting-var.mdx | 10 + yarn.lock | 1281 ++++++++++++++++- 37 files changed, 2382 insertions(+), 16 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/metadata/migrations/1721210534680-createServerlessFunctionTable.ts create mode 100644 packages/twenty-server/src/engine/integrations/file-storage/utils/read-file-content.ts create mode 100644 packages/twenty-server/src/engine/integrations/serverless/drivers/base-serverless.driver.ts create mode 100644 packages/twenty-server/src/engine/integrations/serverless/drivers/constants/build-file-name.ts create mode 100644 packages/twenty-server/src/engine/integrations/serverless/drivers/constants/source-file-name.ts create mode 100644 packages/twenty-server/src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface.ts create mode 100644 packages/twenty-server/src/engine/integrations/serverless/drivers/lambda.driver.ts create mode 100644 packages/twenty-server/src/engine/integrations/serverless/drivers/local.driver.ts create mode 100644 packages/twenty-server/src/engine/integrations/serverless/drivers/services/build-directory-manager.service.ts create mode 100644 packages/twenty-server/src/engine/integrations/serverless/drivers/utils/compile-typescript.ts create mode 100644 packages/twenty-server/src/engine/integrations/serverless/drivers/utils/create-zip-file.ts create mode 100644 packages/twenty-server/src/engine/integrations/serverless/serverless-module.factory.ts create mode 100644 packages/twenty-server/src/engine/integrations/serverless/serverless.constants.ts create mode 100644 packages/twenty-server/src/engine/integrations/serverless/serverless.interface.ts create mode 100644 packages/twenty-server/src/engine/integrations/serverless/serverless.module.ts create mode 100644 packages/twenty-server/src/engine/integrations/serverless/serverless.service.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/execute-serverless-function.input.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result-d-t.o.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.exception.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.resolver.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils.ts diff --git a/package.json b/package.json index 42d37ebab..301fc7e82 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "@air/react-drag-to-select": "^5.0.8", "@apollo/client": "^3.7.17", "@apollo/server": "^4.7.3", + "@aws-sdk/client-lambda": "^3.614.0", "@aws-sdk/client-s3": "^3.363.0", "@aws-sdk/credential-providers": "^3.363.0", "@blocknote/core": "^0.12.1", @@ -70,6 +71,8 @@ "afterframe": "^1.0.2", "apollo-server-express": "^3.12.0", "apollo-upload-client": "^17.0.0", + "archiver": "^7.0.1", + "aws-sdk": "^2.1658.0", "axios": "^1.6.2", "bcrypt": "^5.1.1", "better-sqlite3": "^9.2.2", @@ -93,6 +96,7 @@ "facepaint": "^1.2.1", "file-type": "16.5.4", "framer-motion": "^10.12.17", + "fs-extra": "^11.2.0", "googleapis": "105", "graphiql": "^3.1.1", "graphql": "16.8.0", diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 958328038..45e025a09 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -26,6 +26,13 @@ export type Scalars = { Upload: { input: any; output: any; } }; +export type AisqlQueryResult = { + __typename?: 'AISQLQueryResult'; + queryFailedErrorMessage?: Maybe; + sqlQuery: Scalars['String']['output']; + sqlQueryResult?: Maybe; +}; + export type ActivateWorkspaceInput = { displayName?: InputMaybe; }; @@ -345,6 +352,7 @@ export enum FileFolder { Attachment = 'Attachment', PersonPicture = 'PersonPicture', ProfilePicture = 'ProfilePicture', + ServerlessFunction = 'ServerlessFunction', WorkspaceLogo = 'WorkspaceLogo' } @@ -397,6 +405,7 @@ export type Mutation = { createOneObject: Object; createOneRelation: Relation; createOneRemoteServer: RemoteServer; + createOneServerlessFunction: ServerlessFunction; deleteCurrentWorkspace: Workspace; deleteOneField: Field; deleteOneObject: Object; @@ -407,6 +416,7 @@ export type Mutation = { emailPasswordResetLink: EmailPasswordResetLink; enablePostgresProxy: PostgresCredentials; exchangeAuthorizationCode: ExchangeAuthCode; + executeOneServerlessFunction: ServerlessFunctionExecutionResult; generateApiKeyToken: ApiKeyToken; generateJWT: AuthTokens; generateTransientToken: TransientToken; @@ -488,6 +498,12 @@ export type MutationCreateOneRemoteServerArgs = { }; +export type MutationCreateOneServerlessFunctionArgs = { + file: Scalars['Upload']['input']; + name: Scalars['String']['input']; +}; + + export type MutationDeleteOneFieldArgs = { input: DeleteOneFieldInput; }; @@ -520,6 +536,12 @@ export type MutationExchangeAuthorizationCodeArgs = { }; +export type MutationExecuteOneServerlessFunctionArgs = { + name: Scalars['String']['input']; + payload?: InputMaybe; +}; + + export type MutationGenerateApiKeyTokenArgs = { apiKeyId: Scalars['String']['input']; expiresAt: Scalars['String']['input']; @@ -707,6 +729,7 @@ export type Query = { findManyRemoteServersByType: Array; findOneRemoteServerById: RemoteServer; findWorkspaceFromInviteHash: Workspace; + getAISQLQuery: AisqlQueryResult; getPostgresCredentials?: Maybe; getProductPrices: ProductPricesEntity; getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal; @@ -717,6 +740,8 @@ export type Query = { objects: ObjectConnection; relation: Relation; relations: RelationConnection; + serverlessFunction: ServerlessFunction; + serverlessFunctions: ServerlessFunctionConnection; validatePasswordResetToken: ValidatePasswordResetToken; }; @@ -768,6 +793,11 @@ export type QueryFindWorkspaceFromInviteHashArgs = { }; +export type QueryGetAisqlQueryArgs = { + text: Scalars['String']['input']; +}; + + export type QueryGetProductPricesArgs = { product: Scalars['String']['input']; }; @@ -822,6 +852,18 @@ export type QueryRelationsArgs = { }; +export type QueryServerlessFunctionArgs = { + id: Scalars['UUID']['input']; +}; + + +export type QueryServerlessFunctionsArgs = { + filter?: ServerlessFunctionFilter; + paging?: CursorPaging; + sorting?: Array; +}; + + export type QueryValidatePasswordResetTokenArgs = { passwordResetToken: Scalars['String']['input']; }; @@ -915,6 +957,26 @@ export type Sentry = { release?: Maybe; }; +export type ServerlessFunctionConnection = { + __typename?: 'ServerlessFunctionConnection'; + /** Array of edges. */ + edges: Array; + /** Paging information */ + pageInfo: PageInfo; +}; + +export type ServerlessFunctionExecutionResult = { + __typename?: 'ServerlessFunctionExecutionResult'; + /** Execution result in JSON format */ + result: Scalars['JSON']['output']; +}; + +/** SyncStatus of the serverlessFunction */ +export enum ServerlessFunctionSyncStatus { + NotReady = 'NOT_READY', + Ready = 'READY' +} + export type SessionEntity = { __typename?: 'SessionEntity'; url?: Maybe; @@ -1345,6 +1407,39 @@ export type RelationEdge = { node: Relation; }; +export type ServerlessFunction = { + __typename?: 'serverlessFunction'; + createdAt: Scalars['DateTime']['output']; + id: Scalars['UUID']['output']; + name: Scalars['String']['output']; + syncStatus: ServerlessFunctionSyncStatus; + updatedAt: Scalars['DateTime']['output']; +}; + +export type ServerlessFunctionEdge = { + __typename?: 'serverlessFunctionEdge'; + /** Cursor for this node. */ + cursor: Scalars['ConnectionCursor']['output']; + /** The node containing the serverlessFunction */ + node: ServerlessFunction; +}; + +export type ServerlessFunctionFilter = { + and?: InputMaybe>; + id?: InputMaybe; + or?: InputMaybe>; +}; + +export type ServerlessFunctionSort = { + direction: SortDirection; + field: ServerlessFunctionSortFields; + nulls?: InputMaybe; +}; + +export enum ServerlessFunctionSortFields { + Id = 'id' +} + export type RemoteServerFieldsFragment = { __typename?: 'RemoteServer', id: string, createdAt: any, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string, updatedAt: any, schema?: string | null, label: string, userMappingOptions?: { __typename?: 'UserMappingOptionsUser', user?: string | null } | null }; export type RemoteTableFieldsFragment = { __typename?: 'RemoteTable', id?: any | null, name: string, schema?: string | null, status: RemoteTableStatus, schemaPendingUpdates?: Array | null }; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 4b1a828b4..4cfb626e9 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -258,6 +258,7 @@ export enum FileFolder { Attachment = 'Attachment', PersonPicture = 'PersonPicture', ProfilePicture = 'ProfilePicture', + ServerlessFunction = 'ServerlessFunction', WorkspaceLogo = 'WorkspaceLogo' } @@ -683,6 +684,26 @@ export type Sentry = { release?: Maybe; }; +export type ServerlessFunctionConnection = { + __typename?: 'ServerlessFunctionConnection'; + /** Array of edges. */ + edges: Array; + /** Paging information */ + pageInfo: PageInfo; +}; + +export type ServerlessFunctionExecutionResult = { + __typename?: 'ServerlessFunctionExecutionResult'; + /** Execution result in JSON format */ + result: Scalars['JSON']; +}; + +/** SyncStatus of the serverlessFunction */ +export enum ServerlessFunctionSyncStatus { + NotReady = 'NOT_READY', + Ready = 'READY' +} + export type SessionEntity = { __typename?: 'SessionEntity'; url?: Maybe; @@ -1064,6 +1085,23 @@ export type RelationEdge = { node: Relation; }; +export type ServerlessFunction = { + __typename?: 'serverlessFunction'; + createdAt: Scalars['DateTime']; + id: Scalars['UUID']; + name: Scalars['String']; + syncStatus: ServerlessFunctionSyncStatus; + updatedAt: Scalars['DateTime']; +}; + +export type ServerlessFunctionEdge = { + __typename?: 'serverlessFunctionEdge'; + /** Cursor for this node. */ + cursor: Scalars['ConnectionCursor']; + /** The node containing the serverlessFunction */ + node: ServerlessFunction; +}; + export type TimelineCalendarEventFragmentFragment = { __typename?: 'TimelineCalendarEvent', id: any, title: string, description: string, location: string, startsAt: string, endsAt: string, isFullDay: boolean, visibility: CalendarChannelVisibility, participants: Array<{ __typename?: 'TimelineCalendarEventParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }; export type TimelineCalendarEventParticipantFragmentFragment = { __typename?: 'TimelineCalendarEventParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }; diff --git a/packages/twenty-server/.env.example b/packages/twenty-server/.env.example index 8b2c6cac2..2724fb7c4 100644 --- a/packages/twenty-server/.env.example +++ b/packages/twenty-server/.env.example @@ -35,6 +35,7 @@ SIGN_IN_PREFILLED=true # AUTH_GOOGLE_CLIENT_SECRET=replace_me_with_google_client_secret # AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/redirect # AUTH_GOOGLE_APIS_CALLBACK_URL=http://localhost:3000/auth/google-apis/get-access-token +# SERVERLESS_TYPE=local # STORAGE_TYPE=local # STORAGE_LOCAL_PATH=.local-storage # SUPPORT_DRIVER=front @@ -72,4 +73,4 @@ SIGN_IN_PREFILLED=true # API_RATE_LIMITING_LIMIT= # MUTATION_MAXIMUM_AFFECTED_RECORDS=100 # CHROME_EXTENSION_ID=bggmipldbceihilonnbpgoeclgbkblkp -# PG_SSL_ALLOW_SELF_SIGNED=true \ No newline at end of file +# PG_SSL_ALLOW_SELF_SIGNED=true diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1721210534680-createServerlessFunctionTable.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1721210534680-createServerlessFunctionTable.ts new file mode 100644 index 000000000..83f2ddc00 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/metadata/migrations/1721210534680-createServerlessFunctionTable.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateServerlessFunctionTable1721210534680 + implements MigrationInterface +{ + name = 'CreateServerlessFunctionTable1721210534680'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "metadata"."serverlessFunction_syncstatus_enum" AS ENUM('NOT_READY', 'READY')`, + ); + await queryRunner.query( + `CREATE TABLE "metadata"."serverlessFunction" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "sourceCodeHash" character varying NOT NULL, "syncStatus" "metadata"."serverlessFunction_syncstatus_enum" NOT NULL DEFAULT 'NOT_READY', "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "IndexOnNameAndWorkspaceIdUnique" UNIQUE ("name", "workspaceId"), CONSTRAINT "PK_49bfacee064bee9d0d486483b60" PRIMARY KEY ("id"))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "metadata"."serverlessFunction"`); + await queryRunner.query( + `DROP TYPE "metadata"."serverlessFunction_syncstatus_enum"`, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts index cb63a112c..a8c98201c 100644 --- a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts +++ b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts @@ -13,9 +13,9 @@ import { HealthModule } from 'src/engine/core-modules/health/health.module'; import { AISQLQueryModule } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.module'; import { PostgresCredentialsModule } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.module'; -import { AnalyticsModule } from './analytics/analytics.module'; -import { FileModule } from './file/file.module'; import { ClientConfigModule } from './client-config/client-config.module'; +import { FileModule } from './file/file.module'; +import { AnalyticsModule } from './analytics/analytics.module'; @Module({ imports: [ diff --git a/packages/twenty-server/src/engine/core-modules/file/interfaces/file-folder.interface.ts b/packages/twenty-server/src/engine/core-modules/file/interfaces/file-folder.interface.ts index 2e4e99bca..57afbd72c 100644 --- a/packages/twenty-server/src/engine/core-modules/file/interfaces/file-folder.interface.ts +++ b/packages/twenty-server/src/engine/core-modules/file/interfaces/file-folder.interface.ts @@ -5,6 +5,7 @@ export enum FileFolder { WorkspaceLogo = 'workspace-logo', Attachment = 'attachment', PersonPicture = 'person-picture', + ServerlessFunction = 'serverless-function', } registerEnumType(FileFolder, { diff --git a/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts b/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts index 2be0f2c75..c2e67a200 100644 --- a/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts +++ b/packages/twenty-server/src/engine/integrations/environment/environment-variables.ts @@ -20,6 +20,7 @@ import { NodeEnvironment } from 'src/engine/integrations/environment/interfaces/ import { LLMChatModelDriver } from 'src/engine/integrations/llm-chat-model/interfaces/llm-chat-model.interface'; import { LLMTracingDriver } from 'src/engine/integrations/llm-tracing/interfaces/llm-tracing.interface'; +import { ServerlessDriverType } from 'src/engine/integrations/serverless/serverless.interface'; import { assert } from 'src/utils/assert'; import { CastToStringArray } from 'src/engine/integrations/environment/decorators/cast-to-string-array.decorator'; import { ExceptionHandlerDriver } from 'src/engine/integrations/exception-handler/interfaces'; @@ -204,6 +205,30 @@ export class EnvironmentVariables { @ValidateIf((env) => env.AUTH_GOOGLE_ENABLED) AUTH_GOOGLE_CALLBACK_URL: string; + // Custom Code Engine + @IsEnum(ServerlessDriverType) + @IsOptional() + SERVERLESS_TYPE: ServerlessDriverType = ServerlessDriverType.Local; + + @ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda) + @IsAWSRegion() + SERVERLESS_LAMBDA_REGION: AwsRegion; + + @ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda) + @IsString() + @IsOptional() + SERVERLESS_LAMBDA_ROLE: string; + + @ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda) + @IsString() + @IsOptional() + SERVERLESS_LAMBDA_ACCESS_KEY_ID: string; + + @ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda) + @IsString() + @IsOptional() + SERVERLESS_LAMBDA_SECRET_ACCESS_KEY: string; + // Storage @IsEnum(StorageDriverType) @IsOptional() diff --git a/packages/twenty-server/src/engine/integrations/file-storage/utils/read-file-content.ts b/packages/twenty-server/src/engine/integrations/file-storage/utils/read-file-content.ts new file mode 100644 index 000000000..629743faa --- /dev/null +++ b/packages/twenty-server/src/engine/integrations/file-storage/utils/read-file-content.ts @@ -0,0 +1,11 @@ +import { Readable } from 'stream'; + +export const readFileContent = async (stream: Readable): Promise => { + const chunks: Buffer[] = []; + + for await (const chunk of stream) { + chunks.push(Buffer.from(chunk)); + } + + return Buffer.concat(chunks).toString('utf8'); +}; diff --git a/packages/twenty-server/src/engine/integrations/integrations.module.ts b/packages/twenty-server/src/engine/integrations/integrations.module.ts index fb2f40051..e3f8ab5fe 100644 --- a/packages/twenty-server/src/engine/integrations/integrations.module.ts +++ b/packages/twenty-server/src/engine/integrations/integrations.module.ts @@ -16,6 +16,10 @@ import { LLMChatModelModule } from 'src/engine/integrations/llm-chat-model/llm-c import { llmChatModelModuleFactory } from 'src/engine/integrations/llm-chat-model/llm-chat-model.module-factory'; import { LLMTracingModule } from 'src/engine/integrations/llm-tracing/llm-tracing.module'; import { llmTracingModuleFactory } from 'src/engine/integrations/llm-tracing/llm-tracing.module-factory'; +import { ServerlessModule } from 'src/engine/integrations/serverless/serverless.module'; +import { serverlessModuleFactory } from 'src/engine/integrations/serverless/serverless-module.factory'; +import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service'; +import { BuildDirectoryManagerService } from 'src/engine/integrations/serverless/drivers/services/build-directory-manager.service'; import { EnvironmentModule } from './environment/environment.module'; import { EnvironmentService } from './environment/environment.service'; @@ -62,6 +66,14 @@ import { MessageQueueModule } from './message-queue/message-queue.module'; useFactory: llmTracingModuleFactory, inject: [EnvironmentService], }), + ServerlessModule.forRootAsync({ + useFactory: serverlessModuleFactory, + inject: [ + EnvironmentService, + FileStorageService, + BuildDirectoryManagerService, + ], + }), ], exports: [], providers: [], diff --git a/packages/twenty-server/src/engine/integrations/serverless/drivers/base-serverless.driver.ts b/packages/twenty-server/src/engine/integrations/serverless/drivers/base-serverless.driver.ts new file mode 100644 index 000000000..cfae10d6b --- /dev/null +++ b/packages/twenty-server/src/engine/integrations/serverless/drivers/base-serverless.driver.ts @@ -0,0 +1,33 @@ +import { join } from 'path'; + +import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface'; + +import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { SOURCE_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/source-file-name'; +import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content'; +import { compileTypescript } from 'src/engine/integrations/serverless/drivers/utils/compile-typescript'; +import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service'; + +export class BaseServerlessDriver { + getFolderPath(serverlessFunction: ServerlessFunctionEntity) { + return join( + FileFolder.ServerlessFunction, + serverlessFunction.workspaceId, + serverlessFunction.id, + ); + } + + async getCompiledCode( + serverlessFunction: ServerlessFunctionEntity, + fileStorageService: FileStorageService, + ) { + const folderPath = this.getFolderPath(serverlessFunction); + const fileStream = await fileStorageService.read({ + folderPath, + filename: SOURCE_FILE_NAME, + }); + const typescriptCode = await readFileContent(fileStream); + + return compileTypescript(typescriptCode); + } +} diff --git a/packages/twenty-server/src/engine/integrations/serverless/drivers/constants/build-file-name.ts b/packages/twenty-server/src/engine/integrations/serverless/drivers/constants/build-file-name.ts new file mode 100644 index 000000000..6f60d9054 --- /dev/null +++ b/packages/twenty-server/src/engine/integrations/serverless/drivers/constants/build-file-name.ts @@ -0,0 +1 @@ +export const BUILD_FILE_NAME = 'build.js'; diff --git a/packages/twenty-server/src/engine/integrations/serverless/drivers/constants/source-file-name.ts b/packages/twenty-server/src/engine/integrations/serverless/drivers/constants/source-file-name.ts new file mode 100644 index 000000000..9126720f9 --- /dev/null +++ b/packages/twenty-server/src/engine/integrations/serverless/drivers/constants/source-file-name.ts @@ -0,0 +1 @@ +export const SOURCE_FILE_NAME = 'source.ts'; diff --git a/packages/twenty-server/src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface.ts b/packages/twenty-server/src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface.ts new file mode 100644 index 000000000..4d1489bbe --- /dev/null +++ b/packages/twenty-server/src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface.ts @@ -0,0 +1,9 @@ +import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; + +export interface ServerlessDriver { + build(serverlessFunction: ServerlessFunctionEntity): Promise; + execute( + serverlessFunction: ServerlessFunctionEntity, + payload: object | undefined, + ): Promise; +} diff --git a/packages/twenty-server/src/engine/integrations/serverless/drivers/lambda.driver.ts b/packages/twenty-server/src/engine/integrations/serverless/drivers/lambda.driver.ts new file mode 100644 index 000000000..d115cd9a4 --- /dev/null +++ b/packages/twenty-server/src/engine/integrations/serverless/drivers/lambda.driver.ts @@ -0,0 +1,100 @@ +import fs from 'fs'; + +import { + CreateFunctionCommand, + Lambda, + LambdaClientConfig, + InvokeCommand, +} from '@aws-sdk/client-lambda'; +import { CreateFunctionCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/CreateFunctionCommand'; + +import { ServerlessDriver } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface'; + +import { createZipFile } from 'src/engine/integrations/serverless/drivers/utils/create-zip-file'; +import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service'; +import { BaseServerlessDriver } from 'src/engine/integrations/serverless/drivers/base-serverless.driver'; +import { BuildDirectoryManagerService } from 'src/engine/integrations/serverless/drivers/services/build-directory-manager.service'; + +export interface LambdaDriverOptions extends LambdaClientConfig { + fileStorageService: FileStorageService; + buildDirectoryManagerService: BuildDirectoryManagerService; + region: string; + role: string; +} + +export class LambdaDriver + extends BaseServerlessDriver + implements ServerlessDriver +{ + private readonly lambdaClient: Lambda; + private readonly lambdaRole: string; + private readonly fileStorageService: FileStorageService; + private readonly buildDirectoryManagerService: BuildDirectoryManagerService; + + constructor(options: LambdaDriverOptions) { + super(); + const { region, role, ...lambdaOptions } = options; + + this.lambdaClient = new Lambda({ ...lambdaOptions, region }); + this.lambdaRole = role; + this.fileStorageService = options.fileStorageService; + this.buildDirectoryManagerService = options.buildDirectoryManagerService; + } + + async build(serverlessFunction: ServerlessFunctionEntity) { + const javascriptCode = await this.getCompiledCode( + serverlessFunction, + this.fileStorageService, + ); + + const { + sourceTemporaryDir, + lambdaZipPath, + javascriptFilePath, + lambdaHandler, + } = await this.buildDirectoryManagerService.init(); + + await fs.promises.writeFile(javascriptFilePath, javascriptCode); + + await createZipFile(sourceTemporaryDir, lambdaZipPath); + + const params: CreateFunctionCommandInput = { + Code: { + ZipFile: await fs.promises.readFile(lambdaZipPath), + }, + FunctionName: serverlessFunction.id, + Handler: lambdaHandler, + Role: this.lambdaRole, + Runtime: 'nodejs18.x', + Description: 'Lambda function to run user script', + Timeout: 900, + }; + + const command = new CreateFunctionCommand(params); + + await this.lambdaClient.send(command); + + await this.buildDirectoryManagerService.clean(); + } + + async execute( + functionToExecute: ServerlessFunctionEntity, + payload: object | undefined = undefined, + ): Promise { + const params = { + FunctionName: functionToExecute.id, + Payload: JSON.stringify(payload), + }; + + const command = new InvokeCommand(params); + + const result = await this.lambdaClient.send(command); + + if (!result.Payload) { + return {}; + } + + return JSON.parse(result.Payload.transformToString()); + } +} diff --git a/packages/twenty-server/src/engine/integrations/serverless/drivers/local.driver.ts b/packages/twenty-server/src/engine/integrations/serverless/drivers/local.driver.ts new file mode 100644 index 000000000..15e5701e5 --- /dev/null +++ b/packages/twenty-server/src/engine/integrations/serverless/drivers/local.driver.ts @@ -0,0 +1,94 @@ +import { join } from 'path'; +import { tmpdir } from 'os'; +import { promises as fs } from 'fs'; +import { fork } from 'child_process'; + +import { v4 } from 'uuid'; + +import { ServerlessDriver } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface'; + +import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service'; +import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content'; +import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { BUILD_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/build-file-name'; +import { BaseServerlessDriver } from 'src/engine/integrations/serverless/drivers/base-serverless.driver'; + +export interface LocalDriverOptions { + fileStorageService: FileStorageService; +} + +export class LocalDriver + extends BaseServerlessDriver + implements ServerlessDriver +{ + private readonly fileStorageService: FileStorageService; + + constructor(options: LocalDriverOptions) { + super(); + this.fileStorageService = options.fileStorageService; + } + + async build(serverlessFunction: ServerlessFunctionEntity) { + const javascriptCode = await this.getCompiledCode( + serverlessFunction, + this.fileStorageService, + ); + + await this.fileStorageService.write({ + file: javascriptCode, + name: BUILD_FILE_NAME, + mimeType: undefined, + folder: this.getFolderPath(serverlessFunction), + }); + } + + async execute( + serverlessFunction: ServerlessFunctionEntity, + payload: object | undefined = undefined, + ): Promise { + const fileStream = await this.fileStorageService.read({ + folderPath: this.getFolderPath(serverlessFunction), + filename: BUILD_FILE_NAME, + }); + const fileContent = await readFileContent(fileStream); + + const tmpFilePath = join(tmpdir(), `${v4()}.js`); + + const modifiedContent = ` + process.on('message', async (message) => { + const { event, context } = message; + const result = await handler(event, context); + process.send(result); + }); + + ${fileContent} + `; + + await fs.writeFile(tmpFilePath, modifiedContent); + + return await new Promise((resolve, reject) => { + const child = fork(tmpFilePath); + + child.on('message', (message: object) => { + resolve(message); + child.kill(); + fs.unlink(tmpFilePath); + }); + + child.on('error', (error) => { + reject(error); + child.kill(); + fs.unlink(tmpFilePath); + }); + + child.on('exit', (code) => { + if (code && code !== 0) { + reject(new Error(`Child process exited with code ${code}`)); + fs.unlink(tmpFilePath); + } + }); + + child.send({ event: payload }); + }); + } +} diff --git a/packages/twenty-server/src/engine/integrations/serverless/drivers/services/build-directory-manager.service.ts b/packages/twenty-server/src/engine/integrations/serverless/drivers/services/build-directory-manager.service.ts new file mode 100644 index 000000000..9f0d6b6a7 --- /dev/null +++ b/packages/twenty-server/src/engine/integrations/serverless/drivers/services/build-directory-manager.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@nestjs/common'; + +import { join } from 'path'; +import { tmpdir } from 'os'; +import fs from 'fs'; + +import fsExtra from 'fs-extra'; +import { v4 } from 'uuid'; + +const TEMPORARY_LAMBDA_FOLDER = 'twenty-build-lambda-temp-folder'; +const TEMPORARY_LAMBDA_SOURCE_FOLDER = 'src'; +const LAMBDA_ZIP_FILE_NAME = 'lambda.zip'; +const LAMBDA_ENTRY_FILE_NAME = 'index.js'; + +@Injectable() +export class BuildDirectoryManagerService { + private temporaryDir = join(tmpdir(), `${TEMPORARY_LAMBDA_FOLDER}_${v4()}`); + private lambdaHandler = `${LAMBDA_ENTRY_FILE_NAME.split('.')[0]}.handler`; + + async init() { + const sourceTemporaryDir = join( + this.temporaryDir, + TEMPORARY_LAMBDA_SOURCE_FOLDER, + ); + const lambdaZipPath = join(this.temporaryDir, LAMBDA_ZIP_FILE_NAME); + const javascriptFilePath = join(sourceTemporaryDir, LAMBDA_ENTRY_FILE_NAME); + + if (!fs.existsSync(this.temporaryDir)) { + await fs.promises.mkdir(this.temporaryDir); + await fs.promises.mkdir(sourceTemporaryDir); + } else { + await fsExtra.emptyDir(this.temporaryDir); + await fs.promises.mkdir(sourceTemporaryDir); + } + + return { + sourceTemporaryDir, + lambdaZipPath, + javascriptFilePath, + lambdaHandler: this.lambdaHandler, + }; + } + + async clean() { + await fsExtra.emptyDir(this.temporaryDir); + await fs.promises.rmdir(this.temporaryDir); + } +} diff --git a/packages/twenty-server/src/engine/integrations/serverless/drivers/utils/compile-typescript.ts b/packages/twenty-server/src/engine/integrations/serverless/drivers/utils/compile-typescript.ts new file mode 100644 index 000000000..7db82434a --- /dev/null +++ b/packages/twenty-server/src/engine/integrations/serverless/drivers/utils/compile-typescript.ts @@ -0,0 +1,19 @@ +import ts from 'typescript'; + +export const compileTypescript = (typescriptCode: string): string => { + const options: ts.CompilerOptions = { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2017, + moduleResolution: ts.ModuleResolutionKind.Node10, + esModuleInterop: true, + resolveJsonModule: true, + allowSyntheticDefaultImports: true, + types: ['node'], + }; + + const result = ts.transpileModule(typescriptCode, { + compilerOptions: options, + }); + + return result.outputText; +}; diff --git a/packages/twenty-server/src/engine/integrations/serverless/drivers/utils/create-zip-file.ts b/packages/twenty-server/src/engine/integrations/serverless/drivers/utils/create-zip-file.ts new file mode 100644 index 000000000..994f546a4 --- /dev/null +++ b/packages/twenty-server/src/engine/integrations/serverless/drivers/utils/create-zip-file.ts @@ -0,0 +1,21 @@ +import fs from 'fs'; +import { pipeline } from 'stream/promises'; + +import archiver from 'archiver'; + +export const createZipFile = async ( + sourceDir: string, + outPath: string, +): Promise => { + const output = fs.createWriteStream(outPath); + const archive = archiver('zip', { + zlib: { level: 9 }, // Compression level + }); + + const p = pipeline(archive, output); + + archive.directory(sourceDir, false); + archive.finalize(); + + return p; +}; diff --git a/packages/twenty-server/src/engine/integrations/serverless/serverless-module.factory.ts b/packages/twenty-server/src/engine/integrations/serverless/serverless-module.factory.ts new file mode 100644 index 000000000..a8309073a --- /dev/null +++ b/packages/twenty-server/src/engine/integrations/serverless/serverless-module.factory.ts @@ -0,0 +1,59 @@ +import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; + +import { + ServerlessModuleOptions, + ServerlessDriverType, +} from 'src/engine/integrations/serverless/serverless.interface'; +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; +import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service'; +import { BuildDirectoryManagerService } from 'src/engine/integrations/serverless/drivers/services/build-directory-manager.service'; + +export const serverlessModuleFactory = async ( + environmentService: EnvironmentService, + fileStorageService: FileStorageService, + buildDirectoryManagerService: BuildDirectoryManagerService, +): Promise => { + const driverType = environmentService.get('SERVERLESS_TYPE'); + const options = { fileStorageService }; + + switch (driverType) { + case ServerlessDriverType.Local: { + return { + type: ServerlessDriverType.Local, + options, + }; + } + case ServerlessDriverType.Lambda: { + const region = environmentService.get('SERVERLESS_LAMBDA_REGION'); + const accessKeyId = environmentService.get( + 'SERVERLESS_LAMBDA_ACCESS_KEY_ID', + ); + const secretAccessKey = environmentService.get( + 'SERVERLESS_LAMBDA_SECRET_ACCESS_KEY', + ); + const role = environmentService.get('SERVERLESS_LAMBDA_ROLE'); + + return { + type: ServerlessDriverType.Lambda, + options: { + ...options, + buildDirectoryManagerService, + credentials: accessKeyId + ? { + accessKeyId, + secretAccessKey, + } + : fromNodeProviderChain({ + clientConfig: { region }, + }), + region: region ?? '', + role: role ?? '', + }, + }; + } + default: + throw new Error( + `Invalid serverless driver type (${driverType}), check your .env file`, + ); + } +}; diff --git a/packages/twenty-server/src/engine/integrations/serverless/serverless.constants.ts b/packages/twenty-server/src/engine/integrations/serverless/serverless.constants.ts new file mode 100644 index 000000000..f9df58e51 --- /dev/null +++ b/packages/twenty-server/src/engine/integrations/serverless/serverless.constants.ts @@ -0,0 +1 @@ +export const SERVERLESS_DRIVER = Symbol('SERVERLESS_DRIVER'); diff --git a/packages/twenty-server/src/engine/integrations/serverless/serverless.interface.ts b/packages/twenty-server/src/engine/integrations/serverless/serverless.interface.ts new file mode 100644 index 000000000..c1c4fb661 --- /dev/null +++ b/packages/twenty-server/src/engine/integrations/serverless/serverless.interface.ts @@ -0,0 +1,30 @@ +import { FactoryProvider, ModuleMetadata } from '@nestjs/common'; + +import { LocalDriverOptions } from 'src/engine/integrations/serverless/drivers/local.driver'; +import { LambdaDriverOptions } from 'src/engine/integrations/serverless/drivers/lambda.driver'; + +export enum ServerlessDriverType { + Lambda = 'lambda', + Local = 'local', +} + +export interface LocalDriverFactoryOptions { + type: ServerlessDriverType.Local; + options: LocalDriverOptions; +} + +export interface LambdaDriverFactoryOptions { + type: ServerlessDriverType.Lambda; + options: LambdaDriverOptions; +} + +export type ServerlessModuleOptions = + | LocalDriverFactoryOptions + | LambdaDriverFactoryOptions; + +export type ServerlessModuleAsyncOptions = { + useFactory: ( + ...args: any[] + ) => ServerlessModuleOptions | Promise; +} & Pick & + Pick; diff --git a/packages/twenty-server/src/engine/integrations/serverless/serverless.module.ts b/packages/twenty-server/src/engine/integrations/serverless/serverless.module.ts new file mode 100644 index 000000000..ea87c0f01 --- /dev/null +++ b/packages/twenty-server/src/engine/integrations/serverless/serverless.module.ts @@ -0,0 +1,35 @@ +import { DynamicModule, Global } from '@nestjs/common'; + +import { + ServerlessDriverType, + ServerlessModuleAsyncOptions, +} from 'src/engine/integrations/serverless/serverless.interface'; +import { ServerlessService } from 'src/engine/integrations/serverless/serverless.service'; +import { SERVERLESS_DRIVER } from 'src/engine/integrations/serverless/serverless.constants'; +import { LocalDriver } from 'src/engine/integrations/serverless/drivers/local.driver'; +import { LambdaDriver } from 'src/engine/integrations/serverless/drivers/lambda.driver'; +import { BuildDirectoryManagerService } from 'src/engine/integrations/serverless/drivers/services/build-directory-manager.service'; + +@Global() +export class ServerlessModule { + static forRootAsync(options: ServerlessModuleAsyncOptions): DynamicModule { + const provider = { + provide: SERVERLESS_DRIVER, + useFactory: async (...args: any[]) => { + const config = await options.useFactory(...args); + + return config?.type === ServerlessDriverType.Local + ? new LocalDriver(config.options) + : new LambdaDriver(config.options); + }, + inject: options.inject || [], + }; + + return { + module: ServerlessModule, + imports: options.imports || [], + providers: [ServerlessService, BuildDirectoryManagerService, provider], + exports: [ServerlessService], + }; + } +} diff --git a/packages/twenty-server/src/engine/integrations/serverless/serverless.service.ts b/packages/twenty-server/src/engine/integrations/serverless/serverless.service.ts new file mode 100644 index 000000000..72f003913 --- /dev/null +++ b/packages/twenty-server/src/engine/integrations/serverless/serverless.service.ts @@ -0,0 +1,22 @@ +import { Inject, Injectable } from '@nestjs/common'; + +import { ServerlessDriver } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface'; + +import { SERVERLESS_DRIVER } from 'src/engine/integrations/serverless/serverless.constants'; +import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; + +@Injectable() +export class ServerlessService implements ServerlessDriver { + constructor(@Inject(SERVERLESS_DRIVER) private driver: ServerlessDriver) {} + + async build(serverlessFunction: ServerlessFunctionEntity): Promise { + return this.driver.build(serverlessFunction); + } + + async execute( + serverlessFunction: ServerlessFunctionEntity, + payload: object | undefined = undefined, + ) { + return this.driver.execute(serverlessFunction, payload); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts b/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts index 9e096c8e0..4db423a60 100644 --- a/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts @@ -7,6 +7,7 @@ import { RelationMetadataModule } from 'src/engine/metadata-modules/relation-met import { RemoteServerModule } from 'src/engine/metadata-modules/remote-server/remote-server.module'; import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module'; import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; +import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module'; @Module({ imports: [ @@ -14,6 +15,7 @@ import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace- FieldMetadataModule, ObjectMetadataModule, RelationMetadataModule, + ServerlessFunctionModule, WorkspaceCacheVersionModule, WorkspaceMigrationModule, RemoteServerModule, @@ -24,6 +26,7 @@ import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace- FieldMetadataModule, ObjectMetadataModule, RelationMetadataModule, + ServerlessFunctionModule, RemoteServerModule, ], }) diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/execute-serverless-function.input.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/execute-serverless-function.input.ts new file mode 100644 index 000000000..836748314 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/execute-serverless-function.input.ts @@ -0,0 +1,20 @@ +import { ArgsType, Field } from '@nestjs/graphql'; + +import { IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator'; +import graphqlTypeJson from 'graphql-type-json'; + +@ArgsType() +export class ExecuteServerlessFunctionInput { + @Field({ description: 'Name of the serverless function to execute' }) + @IsNotEmpty() + @IsString() + name: string; + + @Field(() => graphqlTypeJson, { + description: 'Payload in JSON format', + nullable: true, + }) + @IsObject() + @IsOptional() + payload?: JSON; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result-d-t.o.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result-d-t.o.ts new file mode 100644 index 000000000..4560c4371 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result-d-t.o.ts @@ -0,0 +1,13 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { IsObject } from 'class-validator'; +import graphqlTypeJson from 'graphql-type-json'; + +@ObjectType('ServerlessFunctionExecutionResult') +export class ServerlessFunctionExecutionResultDTO { + @IsObject() + @Field(() => graphqlTypeJson, { + description: 'Execution result in JSON format', + }) + result: JSON; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto.ts new file mode 100644 index 000000000..cabebe263 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto.ts @@ -0,0 +1,70 @@ +import { + Field, + HideField, + ObjectType, + registerEnumType, +} from '@nestjs/graphql'; + +import { + Authorize, + IDField, + QueryOptions, +} from '@ptc-org/nestjs-query-graphql'; +import { + IsDateString, + IsEnum, + IsNotEmpty, + IsString, + IsUUID, +} from 'class-validator'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { ServerlessFunctionSyncStatus } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; + +registerEnumType(ServerlessFunctionSyncStatus, { + name: 'ServerlessFunctionSyncStatus', + description: 'SyncStatus of the serverlessFunction', +}); + +@ObjectType('serverlessFunction') +@Authorize({ + authorize: (context: any) => ({ + workspaceId: { eq: context?.req?.user?.workspace?.id }, + }), +}) +@QueryOptions({ + defaultResultSize: 10, + maxResultsSize: 1000, +}) +export class ServerlessFunctionDto { + @IsUUID() + @IsNotEmpty() + @IDField(() => UUIDScalarType) + id: string; + + @IsString() + @IsNotEmpty() + @Field() + name: string; + + @IsString() + @IsNotEmpty() + @Field() + sourceCodeHash: string; + + @IsEnum(ServerlessFunctionSyncStatus) + @IsNotEmpty() + @Field(() => ServerlessFunctionSyncStatus) + syncStatus: ServerlessFunctionSyncStatus; + + @HideField() + workspaceId: string; + + @IsDateString() + @Field() + createdAt: Date; + + @IsDateString() + @Field() + updatedAt: Date; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts new file mode 100644 index 000000000..5ed76fefd --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts @@ -0,0 +1,43 @@ +import { + Column, + CreateDateColumn, + Entity, + PrimaryGeneratedColumn, + Unique, + UpdateDateColumn, +} from 'typeorm'; + +export enum ServerlessFunctionSyncStatus { + NOT_READY = 'NOT_READY', + READY = 'READY', +} + +@Entity('serverlessFunction') +@Unique('IndexOnNameAndWorkspaceIdUnique', ['name', 'workspaceId']) +export class ServerlessFunctionEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ nullable: false }) + name: string; + + @Column({ nullable: false }) + sourceCodeHash: string; + + @Column({ + nullable: false, + default: ServerlessFunctionSyncStatus.NOT_READY, + type: 'enum', + enum: ServerlessFunctionSyncStatus, + }) + syncStatus: ServerlessFunctionSyncStatus; + + @Column({ nullable: false, type: 'uuid' }) + workspaceId: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.exception.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.exception.ts new file mode 100644 index 000000000..bc4266f08 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.exception.ts @@ -0,0 +1,14 @@ +import { CustomException } from 'src/utils/custom-exception'; + +export class ServerlessFunctionException extends CustomException { + code: ServerlessFunctionExceptionCode; + constructor(message: string, code: ServerlessFunctionExceptionCode) { + super(message, code); + } +} + +export enum ServerlessFunctionExceptionCode { + SERVERLESS_FUNCTION_NOT_FOUND = 'SERVERLESS_FUNCTION_NOT_FOUND', + SERVERLESS_FUNCTION_ALREADY_EXIST = 'SERVERLESS_FUNCTION_ALREADY_EXIST', + SERVERLESS_FUNCTION_NOT_READY = 'SERVERLESS_FUNCTION_NOT_READY', +} diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts new file mode 100644 index 000000000..94ca8bede --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts @@ -0,0 +1,50 @@ +import { Module } from '@nestjs/common'; + +import { + NestjsQueryGraphQLModule, + PagingStrategies, +} from '@ptc-org/nestjs-query-graphql'; +import { SortDirection } from '@ptc-org/nestjs-query-core'; +import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; + +import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { ServerlessModule } from 'src/engine/integrations/serverless/serverless.module'; +import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; +import { ServerlessFunctionResolver } from 'src/engine/metadata-modules/serverless-function/serverless-function.resolver'; +import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; +import { ServerlessFunctionDto } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto'; +import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; + +@Module({ + imports: [ + NestjsQueryGraphQLModule.forFeature({ + imports: [ + FileUploadModule, + NestjsQueryTypeOrmModule.forFeature( + [ServerlessFunctionEntity], + 'metadata', + ), + ], + services: [ServerlessFunctionService], + resolvers: [ + { + EntityClass: ServerlessFunctionEntity, + DTOClass: ServerlessFunctionDto, + ServiceClass: ServerlessFunctionService, + pagingStrategy: PagingStrategies.CURSOR, + read: { + defaultSort: [{ field: 'id', direction: SortDirection.DESC }], + }, + create: { disabled: true }, + update: { disabled: true }, + delete: { disabled: true }, + guards: [JwtAuthGuard], + }, + ], + }), + ServerlessModule, + ], + providers: [ServerlessFunctionService, ServerlessFunctionResolver], + exports: [ServerlessFunctionService], +}) +export class ServerlessFunctionModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.resolver.ts new file mode 100644 index 000000000..286ffc192 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.resolver.ts @@ -0,0 +1,59 @@ +import { UseGuards } from '@nestjs/common'; +import { Args, Mutation, Resolver } from '@nestjs/graphql'; + +import { FileUpload, GraphQLUpload } from 'graphql-upload'; + +import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; +import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; +import { ExecuteServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/execute-serverless-function.input'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { ServerlessFunctionDto } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto'; +import { ServerlessFunctionExecutionResultDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result-d-t.o'; +import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils'; + +@UseGuards(JwtAuthGuard) +@Resolver() +export class ServerlessFunctionResolver { + constructor( + private readonly serverlessFunctionService: ServerlessFunctionService, + ) {} + + @Mutation(() => ServerlessFunctionDto) + async createOneServerlessFunction( + @Args({ name: 'file', type: () => GraphQLUpload }) + file: FileUpload, + @Args('name', { type: () => String }) name: string, + @AuthWorkspace() { id: workspaceId }: Workspace, + ) { + try { + return await this.serverlessFunctionService.createOne( + name, + workspaceId, + file, + ); + } catch (error) { + serverlessFunctionGraphQLApiExceptionHandler(error); + } + } + + @Mutation(() => ServerlessFunctionExecutionResultDTO) + async executeOneServerlessFunction( + @Args() executeServerlessFunctionInput: ExecuteServerlessFunctionInput, + @AuthWorkspace() { id: workspaceId }: Workspace, + ) { + try { + const { name, payload } = executeServerlessFunctionInput; + + return { + result: await this.serverlessFunctionService.executeOne( + name, + workspaceId, + payload, + ), + }; + } catch (error) { + serverlessFunctionGraphQLApiExceptionHandler(error); + } + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts new file mode 100644 index 000000000..5d9c22a91 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts @@ -0,0 +1,112 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { join } from 'path'; + +import { FileUpload } from 'graphql-upload'; +import { Repository } from 'typeorm'; + +import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface'; + +import { ServerlessService } from 'src/engine/integrations/serverless/serverless.service'; +import { + ServerlessFunctionEntity, + ServerlessFunctionSyncStatus, +} from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { + ServerlessFunctionException, + ServerlessFunctionExceptionCode, +} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception'; +import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content'; +import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service'; +import { SOURCE_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/source-file-name'; +import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils'; + +@Injectable() +export class ServerlessFunctionService { + constructor( + private readonly fileStorageService: FileStorageService, + private readonly serverlessService: ServerlessService, + @InjectRepository(ServerlessFunctionEntity, 'metadata') + private readonly serverlessFunctionRepository: Repository, + ) {} + + async executeOne( + name: string, + workspaceId: string, + payload: object | undefined = undefined, + ) { + const functionToExecute = await this.serverlessFunctionRepository.findOne({ + where: { + name, + workspaceId, + }, + }); + + if (!functionToExecute) { + throw new ServerlessFunctionException( + `Function does not exist`, + ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND, + ); + } + + if ( + functionToExecute.syncStatus === ServerlessFunctionSyncStatus.NOT_READY + ) { + throw new ServerlessFunctionException( + `Function is not ready to be executed`, + ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND, + ); + } + + return this.serverlessService.execute(functionToExecute, payload); + } + + async createOne( + name: string, + workspaceId: string, + { createReadStream, mimetype }: FileUpload, + ) { + const existingServerlessFunction = + await this.serverlessFunctionRepository.findOne({ + where: { name, workspaceId }, + }); + + if (existingServerlessFunction) { + throw new ServerlessFunctionException( + `Function already exists`, + ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_ALREADY_EXIST, + ); + } + + const typescriptCode = await readFileContent(createReadStream()); + + const serverlessFunction = await this.serverlessFunctionRepository.save({ + name, + workspaceId, + sourceCodeHash: serverlessFunctionCreateHash(typescriptCode), + }); + + const fileFolder = join( + FileFolder.ServerlessFunction, + workspaceId, + serverlessFunction.id, + ); + + await this.fileStorageService.write({ + file: typescriptCode, + name: SOURCE_FILE_NAME, + mimeType: mimetype, + folder: fileFolder, + }); + + await this.serverlessService.build(serverlessFunction); + await this.serverlessFunctionRepository.update(serverlessFunction.id, { + syncStatus: ServerlessFunctionSyncStatus.READY, + }); + + return await this.serverlessFunctionRepository.findOneByOrFail({ + id: serverlessFunction.id, + }); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils.ts new file mode 100644 index 000000000..e33cec6f9 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils.ts @@ -0,0 +1,8 @@ +import { createHash } from 'crypto'; + +export const serverlessFunctionCreateHash = (fileContent: string) => { + return createHash('sha512') + .update(fileContent) + .digest('hex') + .substring(0, 32); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils.ts new file mode 100644 index 000000000..0e00aa103 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils.ts @@ -0,0 +1,26 @@ +import { + ServerlessFunctionException, + ServerlessFunctionExceptionCode, +} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception'; +import { + ConflictError, + ForbiddenError, + InternalServerError, + NotFoundError, +} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; + +export const serverlessFunctionGraphQLApiExceptionHandler = (error: any) => { + if (error instanceof ServerlessFunctionException) { + switch (error.code) { + case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND: + throw new NotFoundError(error.message); + case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_ALREADY_EXIST: + throw new ConflictError(error.message); + case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_READY: + throw new ForbiddenError(error.message); + default: + throw new InternalServerError(error.message); + } + } + throw error; +}; diff --git a/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx b/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx index f133fde18..377783ba7 100644 --- a/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx +++ b/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx @@ -147,6 +147,16 @@ yarn command:prod cron:calendar:calendar-event-list-fetch ['STORAGE_LOCAL_PATH', '.local-storage', 'data path (local storage)'], ]}> +### Custom Code Execution + + + ### Message Queue =1.0.0" + peerDependenciesMeta: + aws-crt: + optional: true + checksum: 1e7b4d572a2915d921db814efbf771603b605aea114399aa357208433746f4b2990c927bdedd8616a6e50c98588032449b8994ce9ffae1cce7976986dc40adc1 + languageName: node + linkType: hard + "@aws-sdk/util-utf8-browser@npm:^3.0.0": version: 3.259.0 resolution: "@aws-sdk/util-utf8-browser@npm:3.259.0" @@ -12666,6 +13175,16 @@ __metadata: languageName: node linkType: hard +"@smithy/abort-controller@npm:^3.1.1": + version: 3.1.1 + resolution: "@smithy/abort-controller@npm:3.1.1" + dependencies: + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: 914933d961b3b29db41a10b9040396968a738340d2bfd7f0b553521a91624ff86ee4ce7d97c15e3d94ca5e2b924da9dbefaf91e6cbd34db25d493690e4889f93 + languageName: node + linkType: hard + "@smithy/chunked-blob-reader-native@npm:^2.0.1": version: 2.0.1 resolution: "@smithy/chunked-blob-reader-native@npm:2.0.1" @@ -12698,6 +13217,19 @@ __metadata: languageName: node linkType: hard +"@smithy/config-resolver@npm:^3.0.5": + version: 3.0.5 + resolution: "@smithy/config-resolver@npm:3.0.5" + dependencies: + "@smithy/node-config-provider": "npm:^3.1.4" + "@smithy/types": "npm:^3.3.0" + "@smithy/util-config-provider": "npm:^3.0.0" + "@smithy/util-middleware": "npm:^3.0.3" + tslib: "npm:^2.6.2" + checksum: 2346a0430a157660a759aee24fd20f18a9c4a3796938b1c792019a898afcdbb0af91af687b84f976a9f1e05eaba6946736e076f6b0ceb5f84b9063c67d2db8ae + languageName: node + linkType: hard + "@smithy/core@npm:^1.2.2": version: 1.2.2 resolution: "@smithy/core@npm:1.2.2" @@ -12714,6 +13246,22 @@ __metadata: languageName: node linkType: hard +"@smithy/core@npm:^2.2.6": + version: 2.2.6 + resolution: "@smithy/core@npm:2.2.6" + dependencies: + "@smithy/middleware-endpoint": "npm:^3.0.5" + "@smithy/middleware-retry": "npm:^3.0.9" + "@smithy/middleware-serde": "npm:^3.0.3" + "@smithy/protocol-http": "npm:^4.0.3" + "@smithy/smithy-client": "npm:^3.1.7" + "@smithy/types": "npm:^3.3.0" + "@smithy/util-middleware": "npm:^3.0.3" + tslib: "npm:^2.6.2" + checksum: 2af8466fbd4bb2fd381af0ac704a81e1e17b515a625e1b12ce9e62dc30a6389bb663341fdb6d643e583ae2c3785108e0c86e6a169d2a9ea0dd171d932960a9f6 + languageName: node + linkType: hard + "@smithy/credential-provider-imds@npm:^2.0.0, @smithy/credential-provider-imds@npm:^2.1.5": version: 2.1.5 resolution: "@smithy/credential-provider-imds@npm:2.1.5" @@ -12727,6 +13275,19 @@ __metadata: languageName: node linkType: hard +"@smithy/credential-provider-imds@npm:^3.1.4": + version: 3.1.4 + resolution: "@smithy/credential-provider-imds@npm:3.1.4" + dependencies: + "@smithy/node-config-provider": "npm:^3.1.4" + "@smithy/property-provider": "npm:^3.1.3" + "@smithy/types": "npm:^3.3.0" + "@smithy/url-parser": "npm:^3.0.3" + tslib: "npm:^2.6.2" + checksum: c05bb394ede243a165c51b717aaa050e7249a335bdccb3c413484eb2ce840f117eb74eb55a11ff6ecf81caf8b94b750b305afb2367c03a5c793d62da4124a7a0 + languageName: node + linkType: hard + "@smithy/eventstream-codec@npm:^2.0.16": version: 2.0.16 resolution: "@smithy/eventstream-codec@npm:2.0.16" @@ -12739,6 +13300,18 @@ __metadata: languageName: node linkType: hard +"@smithy/eventstream-codec@npm:^3.1.2": + version: 3.1.2 + resolution: "@smithy/eventstream-codec@npm:3.1.2" + dependencies: + "@aws-crypto/crc32": "npm:5.2.0" + "@smithy/types": "npm:^3.3.0" + "@smithy/util-hex-encoding": "npm:^3.0.0" + tslib: "npm:^2.6.2" + checksum: fc8db95d9625524b2832cf9cea203b4c1062197d04eef6f676b6eea06cc0007d45acb5270937c1b6b76f98638acaf0c2b822278226c25841ab45488df786e332 + languageName: node + linkType: hard + "@smithy/eventstream-serde-browser@npm:^2.0.16": version: 2.0.16 resolution: "@smithy/eventstream-serde-browser@npm:2.0.16" @@ -12750,6 +13323,17 @@ __metadata: languageName: node linkType: hard +"@smithy/eventstream-serde-browser@npm:^3.0.4": + version: 3.0.4 + resolution: "@smithy/eventstream-serde-browser@npm:3.0.4" + dependencies: + "@smithy/eventstream-serde-universal": "npm:^3.0.4" + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: 4a2b559934202daac2853e0e4c351973a29a50db8535096223d07ed55514c6438a575c642e1f3719342e96908035c884cd35f2a2a5785702ea58566e70d24528 + languageName: node + linkType: hard + "@smithy/eventstream-serde-config-resolver@npm:^2.0.16": version: 2.0.16 resolution: "@smithy/eventstream-serde-config-resolver@npm:2.0.16" @@ -12760,6 +13344,16 @@ __metadata: languageName: node linkType: hard +"@smithy/eventstream-serde-config-resolver@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/eventstream-serde-config-resolver@npm:3.0.3" + dependencies: + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: ef3360c0a0e4ad20f6e6da84b63e5071e3158af726bf291c610e2d42b5e042008cd9fe41ce2183f491422f23c36437987c0d1139e68b3c127d48c01b442dab82 + languageName: node + linkType: hard + "@smithy/eventstream-serde-node@npm:^2.0.16": version: 2.0.16 resolution: "@smithy/eventstream-serde-node@npm:2.0.16" @@ -12771,6 +13365,17 @@ __metadata: languageName: node linkType: hard +"@smithy/eventstream-serde-node@npm:^3.0.4": + version: 3.0.4 + resolution: "@smithy/eventstream-serde-node@npm:3.0.4" + dependencies: + "@smithy/eventstream-serde-universal": "npm:^3.0.4" + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: 11ff38048b1176625d4beb9ca245118aacaf867c90a94747e8cf0bb99e48c68aeedeab56c48a0238a27e35920c7074f3b6f71f8a8246a0d115962d728063a1f5 + languageName: node + linkType: hard + "@smithy/eventstream-serde-universal@npm:^2.0.16": version: 2.0.16 resolution: "@smithy/eventstream-serde-universal@npm:2.0.16" @@ -12782,6 +13387,17 @@ __metadata: languageName: node linkType: hard +"@smithy/eventstream-serde-universal@npm:^3.0.4": + version: 3.0.4 + resolution: "@smithy/eventstream-serde-universal@npm:3.0.4" + dependencies: + "@smithy/eventstream-codec": "npm:^3.1.2" + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: 99ab5f708fa4ebccea96b373395efc76b49c34ae8eb97aa33622ba82e93441a72010bb03693ec18d1517d9bb0a4a7e5c254179c22f38f411a6fecf8b3291c77f + languageName: node + linkType: hard + "@smithy/fetch-http-handler@npm:^2.3.2": version: 2.3.2 resolution: "@smithy/fetch-http-handler@npm:2.3.2" @@ -12795,6 +13411,19 @@ __metadata: languageName: node linkType: hard +"@smithy/fetch-http-handler@npm:^3.2.1": + version: 3.2.1 + resolution: "@smithy/fetch-http-handler@npm:3.2.1" + dependencies: + "@smithy/protocol-http": "npm:^4.0.3" + "@smithy/querystring-builder": "npm:^3.0.3" + "@smithy/types": "npm:^3.3.0" + "@smithy/util-base64": "npm:^3.0.0" + tslib: "npm:^2.6.2" + checksum: 0f815a0c380f0ea3878db5af94c17c569ae37ffad3e361ed49dc4beb0aef87a762e3cf659ebdafc81497b9528e844a14beef4c7c237f8b1dd18b426f8e3d05d2 + languageName: node + linkType: hard + "@smithy/hash-blob-browser@npm:^2.0.17": version: 2.0.17 resolution: "@smithy/hash-blob-browser@npm:2.0.17" @@ -12819,6 +13448,18 @@ __metadata: languageName: node linkType: hard +"@smithy/hash-node@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/hash-node@npm:3.0.3" + dependencies: + "@smithy/types": "npm:^3.3.0" + "@smithy/util-buffer-from": "npm:^3.0.0" + "@smithy/util-utf8": "npm:^3.0.0" + tslib: "npm:^2.6.2" + checksum: d0ba0f069cb047a8a040733b9b119a194c130d287e8a68b8e79cf9cac5abe683df84ea28dd918e85a46031155e0d561f3c5854de3d280c3d501977a986550c8b + languageName: node + linkType: hard + "@smithy/hash-stream-node@npm:^2.0.18": version: 2.0.18 resolution: "@smithy/hash-stream-node@npm:2.0.18" @@ -12840,6 +13481,16 @@ __metadata: languageName: node linkType: hard +"@smithy/invalid-dependency@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/invalid-dependency@npm:3.0.3" + dependencies: + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: c52e909fa0cd8630e1e850da78af20abb11091b134ca107108e4f8336eee4b1b8cde60ba5946eff4bfe3d7bddc74e80a59fa0f448a7b45bf69df1e247aeee607 + languageName: node + linkType: hard + "@smithy/is-array-buffer@npm:^2.0.0": version: 2.0.0 resolution: "@smithy/is-array-buffer@npm:2.0.0" @@ -12849,6 +13500,24 @@ __metadata: languageName: node linkType: hard +"@smithy/is-array-buffer@npm:^2.2.0": + version: 2.2.0 + resolution: "@smithy/is-array-buffer@npm:2.2.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 2f2523cd8cc4538131e408eb31664983fecb0c8724956788b015aaf3ab85a0c976b50f4f09b176f1ed7bbe79f3edf80743be7a80a11f22cd9ce1285d77161aaf + languageName: node + linkType: hard + +"@smithy/is-array-buffer@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/is-array-buffer@npm:3.0.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 44710d94b9e6655ebc02169c149ea2bc5d5b9e509b6b39511cfe61bac571412290f4b9c743d61e395822f014021fcb709dbb533f2f717c1ac2d5a356696c22fd + languageName: node + linkType: hard + "@smithy/md5-js@npm:^2.0.18": version: 2.0.18 resolution: "@smithy/md5-js@npm:2.0.18" @@ -12871,6 +13540,17 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-content-length@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/middleware-content-length@npm:3.0.3" + dependencies: + "@smithy/protocol-http": "npm:^4.0.3" + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: ff76e160e416c40d1fbf462232c1e188addae884dc76ed8b4259fc639312b758332eb56a266e9a022d2ccb5b3bc1dd67e680b74d39e527096f906cc6ea27cac2 + languageName: node + linkType: hard + "@smithy/middleware-endpoint@npm:^2.3.0": version: 2.3.0 resolution: "@smithy/middleware-endpoint@npm:2.3.0" @@ -12886,6 +13566,21 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-endpoint@npm:^3.0.5": + version: 3.0.5 + resolution: "@smithy/middleware-endpoint@npm:3.0.5" + dependencies: + "@smithy/middleware-serde": "npm:^3.0.3" + "@smithy/node-config-provider": "npm:^3.1.4" + "@smithy/shared-ini-file-loader": "npm:^3.1.4" + "@smithy/types": "npm:^3.3.0" + "@smithy/url-parser": "npm:^3.0.3" + "@smithy/util-middleware": "npm:^3.0.3" + tslib: "npm:^2.6.2" + checksum: 1820e52115a3312d4d9b915e7337c113590f12a41967d6b8f24bd5a033c1e16ca3b9419ff2ca9b8acfd106d210119b2ca5b8316b1150cbbf1827c4cb334d4551 + languageName: node + linkType: hard + "@smithy/middleware-retry@npm:^2.0.26": version: 2.0.26 resolution: "@smithy/middleware-retry@npm:2.0.26" @@ -12903,6 +13598,23 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-retry@npm:^3.0.9": + version: 3.0.9 + resolution: "@smithy/middleware-retry@npm:3.0.9" + dependencies: + "@smithy/node-config-provider": "npm:^3.1.4" + "@smithy/protocol-http": "npm:^4.0.3" + "@smithy/service-error-classification": "npm:^3.0.3" + "@smithy/smithy-client": "npm:^3.1.7" + "@smithy/types": "npm:^3.3.0" + "@smithy/util-middleware": "npm:^3.0.3" + "@smithy/util-retry": "npm:^3.0.3" + tslib: "npm:^2.6.2" + uuid: "npm:^9.0.1" + checksum: a0fe7b668be461de74373cf32c0be463c87089f5f5d4e01bbe60ba487282c9fb63244ace527fca7316be8df879d811cd666b9472562ad1ec06ce5e7c7e2fc0cd + languageName: node + linkType: hard + "@smithy/middleware-serde@npm:^2.0.16": version: 2.0.16 resolution: "@smithy/middleware-serde@npm:2.0.16" @@ -12913,6 +13625,16 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-serde@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/middleware-serde@npm:3.0.3" + dependencies: + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: 5b2ad50dea8af9a7a98816c0746c14af4267d053adcade9586a260cff968c41d768220b2987e5b751dbee7cd8c9538ff9839fbc7698dd09bf9b9ca4f5c8001ab + languageName: node + linkType: hard + "@smithy/middleware-stack@npm:^2.0.10": version: 2.0.10 resolution: "@smithy/middleware-stack@npm:2.0.10" @@ -12923,6 +13645,16 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-stack@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/middleware-stack@npm:3.0.3" + dependencies: + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: c886d367ce02f6ae7bc70c4060e79ddfa46c3b35851921364836d64efb76f2fc71b0c1c09401c47d289dc93527a7699085a3feb0778e0337862aa8e6473cb54b + languageName: node + linkType: hard + "@smithy/node-config-provider@npm:^2.1.9": version: 2.1.9 resolution: "@smithy/node-config-provider@npm:2.1.9" @@ -12935,6 +13667,18 @@ __metadata: languageName: node linkType: hard +"@smithy/node-config-provider@npm:^3.1.4": + version: 3.1.4 + resolution: "@smithy/node-config-provider@npm:3.1.4" + dependencies: + "@smithy/property-provider": "npm:^3.1.3" + "@smithy/shared-ini-file-loader": "npm:^3.1.4" + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: 1d69cb8f83292df9e15523a727d55f6b812ff0ca30d615439cc6e7a5fe0d59c9524875745939bba611ca818757790f37509bb843b95f1e6d6b1ccd6d6c546077 + languageName: node + linkType: hard + "@smithy/node-http-handler@npm:^2.2.2": version: 2.2.2 resolution: "@smithy/node-http-handler@npm:2.2.2" @@ -12948,6 +13692,19 @@ __metadata: languageName: node linkType: hard +"@smithy/node-http-handler@npm:^3.1.2": + version: 3.1.2 + resolution: "@smithy/node-http-handler@npm:3.1.2" + dependencies: + "@smithy/abort-controller": "npm:^3.1.1" + "@smithy/protocol-http": "npm:^4.0.3" + "@smithy/querystring-builder": "npm:^3.0.3" + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: e0e887eee24cdcbf2964032915e7d3557ddcf8f6fbf43ed2612f85f78a486f417b8df8a7067ae357ff4f5afdb653d21a103fafebbf387810323df65b4204883c + languageName: node + linkType: hard + "@smithy/property-provider@npm:^2.0.0, @smithy/property-provider@npm:^2.0.17": version: 2.0.17 resolution: "@smithy/property-provider@npm:2.0.17" @@ -12958,6 +13715,16 @@ __metadata: languageName: node linkType: hard +"@smithy/property-provider@npm:^3.1.3": + version: 3.1.3 + resolution: "@smithy/property-provider@npm:3.1.3" + dependencies: + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: e1414e01f6efc298728ff79c1513f9606b44c00b98eb92d003e332ae7312ac9c0e1b7ef08ce426c99545100531fdc33efc0d769b6f75a953df015a8479e73f90 + languageName: node + linkType: hard + "@smithy/protocol-http@npm:^3.0.12": version: 3.0.12 resolution: "@smithy/protocol-http@npm:3.0.12" @@ -12968,6 +13735,16 @@ __metadata: languageName: node linkType: hard +"@smithy/protocol-http@npm:^4.0.3": + version: 4.0.3 + resolution: "@smithy/protocol-http@npm:4.0.3" + dependencies: + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: 3b6e9d587910a25879dcfde2bb25dd5ed3ce45f1fba9aa5956ec47c841eb2c2b08ed724af536fb2b04a792c9c38c03abca36590be5e9d164a029a011df05d97d + languageName: node + linkType: hard + "@smithy/querystring-builder@npm:^2.0.16": version: 2.0.16 resolution: "@smithy/querystring-builder@npm:2.0.16" @@ -12979,6 +13756,17 @@ __metadata: languageName: node linkType: hard +"@smithy/querystring-builder@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/querystring-builder@npm:3.0.3" + dependencies: + "@smithy/types": "npm:^3.3.0" + "@smithy/util-uri-escape": "npm:^3.0.0" + tslib: "npm:^2.6.2" + checksum: 0fd88fb2f3b494981e286b840b7eeb90896d8cc2f47ce3964f65ae95eb74c82691af205bdc17abc39fd483e1952359459204686bb1741c9f425cd5a9a1503f65 + languageName: node + linkType: hard + "@smithy/querystring-parser@npm:^2.0.16": version: 2.0.16 resolution: "@smithy/querystring-parser@npm:2.0.16" @@ -12989,6 +13777,16 @@ __metadata: languageName: node linkType: hard +"@smithy/querystring-parser@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/querystring-parser@npm:3.0.3" + dependencies: + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: a7bcbce8342ca520ca0dbbe420e93547c4eebf7193df4467bae5be6f0493492486a8dad6e20477c5f37f40b9903df91cb8bfb41ee1d21b63b5512f77291ffe6e + languageName: node + linkType: hard + "@smithy/service-error-classification@npm:^2.0.9": version: 2.0.9 resolution: "@smithy/service-error-classification@npm:2.0.9" @@ -12998,6 +13796,15 @@ __metadata: languageName: node linkType: hard +"@smithy/service-error-classification@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/service-error-classification@npm:3.0.3" + dependencies: + "@smithy/types": "npm:^3.3.0" + checksum: 8ba7b655668fff01eb5de1d504711d6304d3e8a8dbbcb0620921bfdaafa5abca7621c0278d21367782d6c53277cddb8bbb6f9373013f64aac0c855520696bbd1 + languageName: node + linkType: hard + "@smithy/shared-ini-file-loader@npm:^2.0.6, @smithy/shared-ini-file-loader@npm:^2.2.8": version: 2.2.8 resolution: "@smithy/shared-ini-file-loader@npm:2.2.8" @@ -13008,6 +13815,16 @@ __metadata: languageName: node linkType: hard +"@smithy/shared-ini-file-loader@npm:^3.1.4": + version: 3.1.4 + resolution: "@smithy/shared-ini-file-loader@npm:3.1.4" + dependencies: + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: e90e5e375fc5afb4dda335e1d0a9d3496cec731511c35351330a210dc22d22b398c45e49d3a4142e55ce7d0e1b280d1b3d46cecdd97b9527f2d9e89ced74f63b + languageName: node + linkType: hard + "@smithy/signature-v4@npm:^2.0.0": version: 2.0.19 resolution: "@smithy/signature-v4@npm:2.0.19" @@ -13024,6 +13841,21 @@ __metadata: languageName: node linkType: hard +"@smithy/signature-v4@npm:^3.1.2": + version: 3.1.2 + resolution: "@smithy/signature-v4@npm:3.1.2" + dependencies: + "@smithy/is-array-buffer": "npm:^3.0.0" + "@smithy/types": "npm:^3.3.0" + "@smithy/util-hex-encoding": "npm:^3.0.0" + "@smithy/util-middleware": "npm:^3.0.3" + "@smithy/util-uri-escape": "npm:^3.0.0" + "@smithy/util-utf8": "npm:^3.0.0" + tslib: "npm:^2.6.2" + checksum: 252807b2c8a400e0eddf34c75fcaaf3d99b7bc0b31d4c79c0d48ee4572687279717d8b19fdd2acf597ade0d07c7355e6e93b74e9651786cf24317c2fcd1c0a06 + languageName: node + linkType: hard + "@smithy/smithy-client@npm:^2.2.1": version: 2.2.1 resolution: "@smithy/smithy-client@npm:2.2.1" @@ -13038,6 +13870,20 @@ __metadata: languageName: node linkType: hard +"@smithy/smithy-client@npm:^3.1.7": + version: 3.1.7 + resolution: "@smithy/smithy-client@npm:3.1.7" + dependencies: + "@smithy/middleware-endpoint": "npm:^3.0.5" + "@smithy/middleware-stack": "npm:^3.0.3" + "@smithy/protocol-http": "npm:^4.0.3" + "@smithy/types": "npm:^3.3.0" + "@smithy/util-stream": "npm:^3.0.6" + tslib: "npm:^2.6.2" + checksum: 4e2ce8c6c8d6398030aa75fe1069839d65aeb541394a20c7cabc4bc00b4a44195b653381a046d75c366f96f8fbf7c081ce761ff7e9fbe62ae876ab72a0a38acd + languageName: node + linkType: hard + "@smithy/types@npm:^2.8.0": version: 2.8.0 resolution: "@smithy/types@npm:2.8.0" @@ -13047,6 +13893,15 @@ __metadata: languageName: node linkType: hard +"@smithy/types@npm:^3.3.0": + version: 3.3.0 + resolution: "@smithy/types@npm:3.3.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: ab2c2d621384a2bbdd31d5c90809395cb5c2a726afd69758895d5a630f932f6ae9a53ca7a9cd5d8c195df9278869b2420a2fb4fada47dee9e8c9d4e3c80a349e + languageName: node + linkType: hard + "@smithy/url-parser@npm:^2.0.16": version: 2.0.16 resolution: "@smithy/url-parser@npm:2.0.16" @@ -13058,6 +13913,17 @@ __metadata: languageName: node linkType: hard +"@smithy/url-parser@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/url-parser@npm:3.0.3" + dependencies: + "@smithy/querystring-parser": "npm:^3.0.3" + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: 9ed0ab14034369fd823587c22d22e257203638a327954853c9bb92c3571a94fa7dc56211f9340b0ac3af5c37dfa206fd99dcde4ee9164a300994314a83e0b042 + languageName: node + linkType: hard + "@smithy/util-base64@npm:^2.0.1": version: 2.0.1 resolution: "@smithy/util-base64@npm:2.0.1" @@ -13068,6 +13934,17 @@ __metadata: languageName: node linkType: hard +"@smithy/util-base64@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-base64@npm:3.0.0" + dependencies: + "@smithy/util-buffer-from": "npm:^3.0.0" + "@smithy/util-utf8": "npm:^3.0.0" + tslib: "npm:^2.6.2" + checksum: 5c05c3505bd1ac4c1e04ec0e22ad1c9e0c61756945735861614f9e46146369a1a112dd0895602475822c18b8f1fe0cc3fb9e45c99a4e7fb03308969c673cf043 + languageName: node + linkType: hard + "@smithy/util-body-length-browser@npm:^2.0.1": version: 2.0.1 resolution: "@smithy/util-body-length-browser@npm:2.0.1" @@ -13077,6 +13954,15 @@ __metadata: languageName: node linkType: hard +"@smithy/util-body-length-browser@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-body-length-browser@npm:3.0.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: cfb595e814334fe7bb78e8381141cc7364f66bff0c1d672680f4abb99361ef66fbdb9468fa1dbabcd5753254b2b05c59c907fa9d600b36e6e4b8423eccf412f7 + languageName: node + linkType: hard + "@smithy/util-body-length-node@npm:^2.1.0": version: 2.1.0 resolution: "@smithy/util-body-length-node@npm:2.1.0" @@ -13086,6 +13972,15 @@ __metadata: languageName: node linkType: hard +"@smithy/util-body-length-node@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-body-length-node@npm:3.0.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 6f779848e7c81051364cf6e40ed61034a06fa8df3480398528baae54d9b69622abc7d068869e33dbe51fef2bbc6fda3f548ac59644a0f10545a54c87bc3a4391 + languageName: node + linkType: hard + "@smithy/util-buffer-from@npm:^2.0.0": version: 2.0.0 resolution: "@smithy/util-buffer-from@npm:2.0.0" @@ -13096,6 +13991,26 @@ __metadata: languageName: node linkType: hard +"@smithy/util-buffer-from@npm:^2.2.0": + version: 2.2.0 + resolution: "@smithy/util-buffer-from@npm:2.2.0" + dependencies: + "@smithy/is-array-buffer": "npm:^2.2.0" + tslib: "npm:^2.6.2" + checksum: 223d6a508b52ff236eea01cddc062b7652d859dd01d457a4e50365af3de1e24a05f756e19433f6ccf1538544076b4215469e21a4ea83dc1d58d829725b0dbc5a + languageName: node + linkType: hard + +"@smithy/util-buffer-from@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-buffer-from@npm:3.0.0" + dependencies: + "@smithy/is-array-buffer": "npm:^3.0.0" + tslib: "npm:^2.6.2" + checksum: b10fb81ef34f95418f27c9123c2c1774e690dd447e8064184688c553156bdec46d2ba1b1ae3bad7edd2b58a5ef32ac569e1ad814b36e7ee05eba10526d329983 + languageName: node + linkType: hard + "@smithy/util-config-provider@npm:^2.1.0": version: 2.1.0 resolution: "@smithy/util-config-provider@npm:2.1.0" @@ -13105,6 +14020,15 @@ __metadata: languageName: node linkType: hard +"@smithy/util-config-provider@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-config-provider@npm:3.0.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: a2c25eac31223eddea306beff2bb3c32e8761f8cb50e8cb2a9d61417a5040e9565dc715a655787e99a37465fdd35bbd0668ff36e06043a5f6b7be48a76974792 + languageName: node + linkType: hard + "@smithy/util-defaults-mode-browser@npm:^2.0.24": version: 2.0.24 resolution: "@smithy/util-defaults-mode-browser@npm:2.0.24" @@ -13118,6 +14042,19 @@ __metadata: languageName: node linkType: hard +"@smithy/util-defaults-mode-browser@npm:^3.0.9": + version: 3.0.9 + resolution: "@smithy/util-defaults-mode-browser@npm:3.0.9" + dependencies: + "@smithy/property-provider": "npm:^3.1.3" + "@smithy/smithy-client": "npm:^3.1.7" + "@smithy/types": "npm:^3.3.0" + bowser: "npm:^2.11.0" + tslib: "npm:^2.6.2" + checksum: 1bbd022fb81e19a9b839c2dceeb36d560e18959cf8e1b73737bb077b9f4783a78bf81e62249910ecb54ef0fc87bd48c32a62c8161d0046855226b3c6d35ea7a8 + languageName: node + linkType: hard + "@smithy/util-defaults-mode-node@npm:^2.0.32": version: 2.0.32 resolution: "@smithy/util-defaults-mode-node@npm:2.0.32" @@ -13133,6 +14070,21 @@ __metadata: languageName: node linkType: hard +"@smithy/util-defaults-mode-node@npm:^3.0.9": + version: 3.0.9 + resolution: "@smithy/util-defaults-mode-node@npm:3.0.9" + dependencies: + "@smithy/config-resolver": "npm:^3.0.5" + "@smithy/credential-provider-imds": "npm:^3.1.4" + "@smithy/node-config-provider": "npm:^3.1.4" + "@smithy/property-provider": "npm:^3.1.3" + "@smithy/smithy-client": "npm:^3.1.7" + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: f00925d9681628af4c3542fa2ffe0c70ee6e487c994009d731d0975931ba237d4f579e7e5970a69c645ced74620fa602bef343f4a5673a7a65b9230ffef8b43a + languageName: node + linkType: hard + "@smithy/util-endpoints@npm:^1.0.8": version: 1.0.8 resolution: "@smithy/util-endpoints@npm:1.0.8" @@ -13144,6 +14096,17 @@ __metadata: languageName: node linkType: hard +"@smithy/util-endpoints@npm:^2.0.5": + version: 2.0.5 + resolution: "@smithy/util-endpoints@npm:2.0.5" + dependencies: + "@smithy/node-config-provider": "npm:^3.1.4" + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: 4dd0740eaca169dc1078ef7e10dd0b0cc186e8c2bb1bf26c7ab8dff557c59f146bf6496a3e44a7bbb9ac6bfbcb587f1a100d81466f29b20dbb58e3e5cf5bceeb + languageName: node + linkType: hard + "@smithy/util-hex-encoding@npm:^2.0.0": version: 2.0.0 resolution: "@smithy/util-hex-encoding@npm:2.0.0" @@ -13153,6 +14116,15 @@ __metadata: languageName: node linkType: hard +"@smithy/util-hex-encoding@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-hex-encoding@npm:3.0.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: d2fa7270853cc8f22c4f4635c72bf52e303731a68a3999e3ea9da1d38b6bf08c0f884e7d20b65741e3bc68bb3821e1abd1c3406d7a3dce8fc02df019aea59162 + languageName: node + linkType: hard + "@smithy/util-middleware@npm:^2.0.9": version: 2.0.9 resolution: "@smithy/util-middleware@npm:2.0.9" @@ -13163,6 +14135,16 @@ __metadata: languageName: node linkType: hard +"@smithy/util-middleware@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/util-middleware@npm:3.0.3" + dependencies: + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: 1d7d01f75ab6d116e6d539bbcfc6f5d7f2b6e3a25f970758872a2e45c4a6b5795326d2f51b2566ca9fe5ba260d9176b33260bde15759c5296ab9f8557835364e + languageName: node + linkType: hard + "@smithy/util-retry@npm:^2.0.9": version: 2.0.9 resolution: "@smithy/util-retry@npm:2.0.9" @@ -13174,6 +14156,17 @@ __metadata: languageName: node linkType: hard +"@smithy/util-retry@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/util-retry@npm:3.0.3" + dependencies: + "@smithy/service-error-classification": "npm:^3.0.3" + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: bea28dff13ae32222dda579eb9bccfaf34b427ab46165509cd524a7080463361a39acc5d1aa7452714c38193a5523f3ab810cd2e60eef9bc768fd1ab23b5bde6 + languageName: node + linkType: hard + "@smithy/util-stream@npm:^2.0.24": version: 2.0.24 resolution: "@smithy/util-stream@npm:2.0.24" @@ -13190,6 +14183,22 @@ __metadata: languageName: node linkType: hard +"@smithy/util-stream@npm:^3.0.6": + version: 3.0.6 + resolution: "@smithy/util-stream@npm:3.0.6" + dependencies: + "@smithy/fetch-http-handler": "npm:^3.2.1" + "@smithy/node-http-handler": "npm:^3.1.2" + "@smithy/types": "npm:^3.3.0" + "@smithy/util-base64": "npm:^3.0.0" + "@smithy/util-buffer-from": "npm:^3.0.0" + "@smithy/util-hex-encoding": "npm:^3.0.0" + "@smithy/util-utf8": "npm:^3.0.0" + tslib: "npm:^2.6.2" + checksum: 2910c28ddbda13101515e8d75abd14f937bce8c6a256c8ad71ac3565952ec9edb19ce59b7dfe04301b721c44e5e365cb6e59281bdd66b07a27771dfccea3e72f + languageName: node + linkType: hard + "@smithy/util-uri-escape@npm:^2.0.0": version: 2.0.0 resolution: "@smithy/util-uri-escape@npm:2.0.0" @@ -13199,6 +14208,25 @@ __metadata: languageName: node linkType: hard +"@smithy/util-uri-escape@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-uri-escape@npm:3.0.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: b8d831348412cfafd9300069e74a12e0075b5e786d7ef6a210ba4ab576001c2525653eec68b71dfe6d7aef71c52f547404c4f0345c0fb476a67277f9d44b1156 + languageName: node + linkType: hard + +"@smithy/util-utf8@npm:^2.0.0": + version: 2.3.0 + resolution: "@smithy/util-utf8@npm:2.3.0" + dependencies: + "@smithy/util-buffer-from": "npm:^2.2.0" + tslib: "npm:^2.6.2" + checksum: e18840c58cc507ca57fdd624302aefd13337ee982754c9aa688463ffcae598c08461e8620e9852a424d662ffa948fc64919e852508028d09e89ced459bd506ab + languageName: node + linkType: hard + "@smithy/util-utf8@npm:^2.0.2": version: 2.0.2 resolution: "@smithy/util-utf8@npm:2.0.2" @@ -13209,6 +14237,16 @@ __metadata: languageName: node linkType: hard +"@smithy/util-utf8@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-utf8@npm:3.0.0" + dependencies: + "@smithy/util-buffer-from": "npm:^3.0.0" + tslib: "npm:^2.6.2" + checksum: b568ed84b4770d2ae9b632eb85603765195a791f045af7f47df1369dc26b001056f4edf488b42ca1cd6d852d0155ad306a0d6531e912cb4e633c0d87abaa8899 + languageName: node + linkType: hard + "@smithy/util-waiter@npm:^2.0.16": version: 2.0.16 resolution: "@smithy/util-waiter@npm:2.0.16" @@ -13220,6 +14258,17 @@ __metadata: languageName: node linkType: hard +"@smithy/util-waiter@npm:^3.1.2": + version: 3.1.2 + resolution: "@smithy/util-waiter@npm:3.1.2" + dependencies: + "@smithy/abort-controller": "npm:^3.1.1" + "@smithy/types": "npm:^3.3.0" + tslib: "npm:^2.6.2" + checksum: 50e7ef8de9779650aec125b81b28e01e9b696f121841d6b1037fd7a2e1296db21c2399b3cf87381a256b3db04a63013c65dba187d22d2a38d31e389ef356c066 + languageName: node + linkType: hard + "@sniptt/guards@npm:^0.2.0": version: 0.2.0 resolution: "@sniptt/guards@npm:0.2.0" @@ -20039,6 +21088,21 @@ __metadata: languageName: node linkType: hard +"archiver-utils@npm:^5.0.0, archiver-utils@npm:^5.0.2": + version: 5.0.2 + resolution: "archiver-utils@npm:5.0.2" + dependencies: + glob: "npm:^10.0.0" + graceful-fs: "npm:^4.2.0" + is-stream: "npm:^2.0.1" + lazystream: "npm:^1.0.0" + lodash: "npm:^4.17.15" + normalize-path: "npm:^3.0.0" + readable-stream: "npm:^4.0.0" + checksum: 3782c5fa9922186aa1a8e41ed0c2867569faa5f15c8e5e6418ea4c1b730b476e21bd68270b3ea457daf459ae23aaea070b2b9f90cf90a59def8dc79b9e4ef538 + languageName: node + linkType: hard + "archiver@npm:5.3.1": version: 5.3.1 resolution: "archiver@npm:5.3.1" @@ -20054,6 +21118,21 @@ __metadata: languageName: node linkType: hard +"archiver@npm:^7.0.1": + version: 7.0.1 + resolution: "archiver@npm:7.0.1" + dependencies: + archiver-utils: "npm:^5.0.2" + async: "npm:^3.2.4" + buffer-crc32: "npm:^1.0.0" + readable-stream: "npm:^4.0.0" + readdir-glob: "npm:^1.1.2" + tar-stream: "npm:^3.0.0" + zip-stream: "npm:^6.0.1" + checksum: 02afd87ca16f6184f752db8e26884e6eff911c476812a0e7f7b26c4beb09f06119807f388a8e26ed2558aa8ba9db28646ebd147a4f99e46813b8b43158e1438e + languageName: node + linkType: hard + "archy@npm:^1.0.0": version: 1.0.0 resolution: "archy@npm:1.0.0" @@ -20441,7 +21520,7 @@ __metadata: languageName: node linkType: hard -"async@npm:^3.2.3": +"async@npm:^3.2.3, async@npm:^3.2.4": version: 3.2.5 resolution: "async@npm:3.2.5" checksum: 1408287b26c6db67d45cb346e34892cee555b8b59e6c68e6f8c3e495cad5ca13b4f218180e871f3c2ca30df4ab52693b66f2f6ff43644760cab0b2198bda79c1 @@ -20510,6 +21589,24 @@ __metadata: languageName: node linkType: hard +"aws-sdk@npm:^2.1658.0": + version: 2.1658.0 + resolution: "aws-sdk@npm:2.1658.0" + dependencies: + buffer: "npm:4.9.2" + events: "npm:1.1.1" + ieee754: "npm:1.1.13" + jmespath: "npm:0.16.0" + querystring: "npm:0.2.0" + sax: "npm:1.2.1" + url: "npm:0.10.3" + util: "npm:^0.12.4" + uuid: "npm:8.0.0" + xml2js: "npm:0.6.2" + checksum: e80072b09155e6f83bbd59e49345a603f00db557cbfed020363f7fe950f65315a063ff53009aab1d52b6add42a96ad3549fc0d277b80ac0b91b9af4fa6894dde + languageName: node + linkType: hard + "aws-sign2@npm:~0.7.0": version: 0.7.0 resolution: "aws-sign2@npm:0.7.0" @@ -22317,6 +23414,13 @@ __metadata: languageName: node linkType: hard +"buffer-crc32@npm:^1.0.0": + version: 1.0.0 + resolution: "buffer-crc32@npm:1.0.0" + checksum: 8b86e161cee4bb48d5fa622cbae4c18f25e4857e5203b89e23de59e627ab26beb82d9d7999f2b8de02580165f61f83f997beaf02980cdf06affd175b651921ab + languageName: node + linkType: hard + "buffer-equal-constant-time@npm:1.0.1": version: 1.0.1 resolution: "buffer-equal-constant-time@npm:1.0.1" @@ -22345,6 +23449,17 @@ __metadata: languageName: node linkType: hard +"buffer@npm:4.9.2": + version: 4.9.2 + resolution: "buffer@npm:4.9.2" + dependencies: + base64-js: "npm:^1.0.2" + ieee754: "npm:^1.1.4" + isarray: "npm:^1.0.0" + checksum: dc443d7e7caab23816b58aacdde710b72f525ad6eecd7d738fcaa29f6d6c12e8d9c13fed7219fd502be51ecf0615f5c077d4bdc6f9308dde2e53f8e5393c5b21 + languageName: node + linkType: hard + "buffer@npm:^5.5.0": version: 5.7.1 resolution: "buffer@npm:5.7.1" @@ -23910,6 +25025,19 @@ __metadata: languageName: node linkType: hard +"compress-commons@npm:^6.0.2": + version: 6.0.2 + resolution: "compress-commons@npm:6.0.2" + dependencies: + crc-32: "npm:^1.2.0" + crc32-stream: "npm:^6.0.0" + is-stream: "npm:^2.0.1" + normalize-path: "npm:^3.0.0" + readable-stream: "npm:^4.0.0" + checksum: 2347031b7c92c8ed5011b07b93ec53b298fa2cd1800897532ac4d4d1aeae06567883f481b6e35f13b65fc31b190c751df6635434d525562f0203fde76f1f0814 + languageName: node + linkType: hard + "compressible@npm:~2.0.16": version: 2.0.18 resolution: "compressible@npm:2.0.18" @@ -24380,6 +25508,16 @@ __metadata: languageName: node linkType: hard +"crc32-stream@npm:^6.0.0": + version: 6.0.0 + resolution: "crc32-stream@npm:6.0.0" + dependencies: + crc-32: "npm:^1.2.0" + readable-stream: "npm:^4.0.0" + checksum: bf9c84571ede2d119c2b4f3a9ef5eeb9ff94b588493c0d3862259af86d3679dcce1c8569dd2b0a6eff2f35f5e2081cc1263b846d2538d4054da78cf34f262a3d + languageName: node + linkType: hard + "create-ecdh@npm:^4.0.0": version: 4.0.4 resolution: "create-ecdh@npm:4.0.4" @@ -27802,6 +28940,13 @@ __metadata: languageName: node linkType: hard +"events@npm:1.1.1": + version: 1.1.1 + resolution: "events@npm:1.1.1" + checksum: 29ba5a4c7d03dd2f4a2d3d9d4dfd8332225256f666cd69f490975d2eff8d7c73f1fb4872877b2c1f3b485e8fb42462153d65e5a21ea994eb928c3bec9e0c826e + languageName: node + linkType: hard + "events@npm:^3.0.0, events@npm:^3.2.0, events@npm:^3.3.0": version: 3.3.0 resolution: "events@npm:3.3.0" @@ -29084,7 +30229,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^11.1.0, fs-extra@npm:^11.1.1": +"fs-extra@npm:^11.1.0, fs-extra@npm:^11.1.1, fs-extra@npm:^11.2.0": version: 11.2.0 resolution: "fs-extra@npm:11.2.0" dependencies: @@ -31530,6 +32675,13 @@ __metadata: languageName: node linkType: hard +"ieee754@npm:1.1.13": + version: 1.1.13 + resolution: "ieee754@npm:1.1.13" + checksum: eaf8c87e014282bfb5b13670991a2ed086eaef35ccc3fb713833863f2e7213041b2c29246adbc5f6561d51d53861c3b11f3b82b28fc6fa1352edeff381f056e5 + languageName: node + linkType: hard + "ieee754@npm:^1.1.13, ieee754@npm:^1.1.4, ieee754@npm:^1.2.1": version: 1.2.1 resolution: "ieee754@npm:1.2.1" @@ -32602,7 +33754,7 @@ __metadata: languageName: node linkType: hard -"is-stream@npm:^2.0.0": +"is-stream@npm:^2.0.0, is-stream@npm:^2.0.1": version: 2.0.1 resolution: "is-stream@npm:2.0.1" checksum: 7c284241313fc6efc329b8d7f08e16c0efeb6baab1b4cd0ba579eb78e5af1aa5da11e68559896a2067cd6c526bd29241dda4eb1225e627d5aa1a89a76d4635a5 @@ -32780,6 +33932,13 @@ __metadata: languageName: node linkType: hard +"isarray@npm:^1.0.0, isarray@npm:~1.0.0": + version: 1.0.0 + resolution: "isarray@npm:1.0.0" + checksum: 18b5be6669be53425f0b84098732670ed4e727e3af33bc7f948aac01782110eb9a18b3b329c5323bcdd3acdaae547ee077d3951317e7f133bff7105264b3003d + languageName: node + linkType: hard + "isarray@npm:^2.0.5": version: 2.0.5 resolution: "isarray@npm:2.0.5" @@ -32787,13 +33946,6 @@ __metadata: languageName: node linkType: hard -"isarray@npm:~1.0.0": - version: 1.0.0 - resolution: "isarray@npm:1.0.0" - checksum: 18b5be6669be53425f0b84098732670ed4e727e3af33bc7f948aac01782110eb9a18b3b329c5323bcdd3acdaae547ee077d3951317e7f133bff7105264b3003d - languageName: node - linkType: hard - "isbinaryfile@npm:^5.0.0": version: 5.0.0 resolution: "isbinaryfile@npm:5.0.0" @@ -33643,6 +34795,13 @@ __metadata: languageName: node linkType: hard +"jmespath@npm:0.16.0": + version: 0.16.0 + resolution: "jmespath@npm:0.16.0" + checksum: 84cdca62c4a3d339701f01cc53decf16581c76ce49e6455119be1c5f6ab09a19e6788372536bd261d348d21cd817981605f8debae67affadba966219a2bac1c5 + languageName: node + linkType: hard + "joi@npm:^17.11.0, joi@npm:^17.9.2": version: 17.11.0 resolution: "joi@npm:17.11.0" @@ -42265,6 +43424,13 @@ __metadata: languageName: node linkType: hard +"punycode@npm:1.3.2": + version: 1.3.2 + resolution: "punycode@npm:1.3.2" + checksum: 281fd20eaf4704f79d80cb0dc65065bf6452ee67989b3e8941aed6360a5a9a8a01d3e2ed71d0bde3cd74fb5a5dd9db4160bed5a8c20bed4b6764c24ce4c7d2d2 + languageName: node + linkType: hard + "punycode@npm:^1.3.2, punycode@npm:^1.4.1": version: 1.4.1 resolution: "punycode@npm:1.4.1" @@ -42389,6 +43555,13 @@ __metadata: languageName: node linkType: hard +"querystring@npm:0.2.0": + version: 0.2.0 + resolution: "querystring@npm:0.2.0" + checksum: 2036c9424beaacd3978bac9e4ba514331cc73163bea7bf3ad7e2c7355e55501938ec195312c607753f9c6e70b1bf9dfcda38db6241bd299c034e27ac639d64ed + languageName: node + linkType: hard + "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -43339,6 +44512,19 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^4.0.0": + version: 4.5.2 + resolution: "readable-stream@npm:4.5.2" + dependencies: + abort-controller: "npm:^3.0.0" + buffer: "npm:^6.0.3" + events: "npm:^3.3.0" + process: "npm:^0.11.10" + string_decoder: "npm:^1.3.0" + checksum: a2c80e0e53aabd91d7df0330929e32d0a73219f9477dbbb18472f6fdd6a11a699fc5d172a1beff98d50eae4f1496c950ffa85b7cc2c4c196963f289a5f39275d + languageName: node + linkType: hard + "readable-web-to-node-stream@npm:^3.0.0, readable-web-to-node-stream@npm:^3.0.2": version: 3.0.2 resolution: "readable-web-to-node-stream@npm:3.0.2" @@ -43348,7 +44534,7 @@ __metadata: languageName: node linkType: hard -"readdir-glob@npm:^1.0.0": +"readdir-glob@npm:^1.0.0, readdir-glob@npm:^1.1.2": version: 1.1.3 resolution: "readdir-glob@npm:1.1.3" dependencies: @@ -44755,7 +45941,14 @@ __metadata: languageName: node linkType: hard -"sax@npm:^1.2.4": +"sax@npm:1.2.1": + version: 1.2.1 + resolution: "sax@npm:1.2.1" + checksum: 1ae269cfde0b3774b4c92eb744452b6740bde5c5744fe5cadef6f496e42d9b632f483fb6aff9a23c0749c94c6951b06b0c5a90a5e99c879d3401cfd5ba61dc02 + languageName: node + linkType: hard + +"sax@npm:>=0.6.0, sax@npm:^1.2.4": version: 1.3.0 resolution: "sax@npm:1.3.0" checksum: 599dbe0ba9d8bd55e92d920239b21d101823a6cedff71e542589303fa0fa8f3ece6cf608baca0c51be846a2e88365fac94a9101a9c341d94b98e30c4deea5bea @@ -46873,6 +48066,17 @@ __metadata: languageName: node linkType: hard +"tar-stream@npm:^3.0.0": + version: 3.1.7 + resolution: "tar-stream@npm:3.1.7" + dependencies: + b4a: "npm:^1.6.4" + fast-fifo: "npm:^1.2.0" + streamx: "npm:^2.15.0" + checksum: a09199d21f8714bd729993ac49b6c8efcb808b544b89f23378ad6ffff6d1cb540878614ba9d4cfec11a64ef39e1a6f009a5398371491eb1fda606ffc7f70f718 + languageName: node + linkType: hard + "tar-stream@npm:^3.1.5": version: 3.1.6 resolution: "tar-stream@npm:3.1.6" @@ -47896,6 +49100,7 @@ __metadata: "@air/react-drag-to-select": "npm:^5.0.8" "@apollo/client": "npm:^3.7.17" "@apollo/server": "npm:^4.7.3" + "@aws-sdk/client-lambda": "npm:^3.614.0" "@aws-sdk/client-s3": "npm:^3.363.0" "@aws-sdk/credential-providers": "npm:^3.363.0" "@babel/core": "npm:^7.14.5" @@ -48048,6 +49253,8 @@ __metadata: afterframe: "npm:^1.0.2" apollo-server-express: "npm:^3.12.0" apollo-upload-client: "npm:^17.0.0" + archiver: "npm:^7.0.1" + aws-sdk: "npm:^2.1658.0" axios: "npm:^1.6.2" bcrypt: "npm:^5.1.1" better-sqlite3: "npm:^9.2.2" @@ -48091,6 +49298,7 @@ __metadata: facepaint: "npm:^1.2.1" file-type: "npm:16.5.4" framer-motion: "npm:^10.12.17" + fs-extra: "npm:^11.2.0" googleapis: "npm:105" graphiql: "npm:^3.1.1" graphql: "npm:16.8.0" @@ -49191,6 +50399,16 @@ __metadata: languageName: node linkType: hard +"url@npm:0.10.3": + version: 0.10.3 + resolution: "url@npm:0.10.3" + dependencies: + punycode: "npm:1.3.2" + querystring: "npm:0.2.0" + checksum: f0a1c7d99ac35dd68a8962bc7b3dd38f08d457387fc686f0669ff881b00a68eabd9cb3aded09dfbe25401d7b632fc4a9c074cb373f6a4bd1d8b5324d1d442a0d + languageName: node + linkType: hard + "url@npm:^0.11.0, url@npm:~0.11.0": version: 0.11.3 resolution: "url@npm:0.11.3" @@ -49389,6 +50607,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:8.0.0": + version: 8.0.0 + resolution: "uuid@npm:8.0.0" + bin: + uuid: dist/bin/uuid + checksum: e62301a1c6102da5ce9a147b492a4b5cfa14d2e8fdf4a6ebfda7929cb72d186f84173815ec18fa4160a03bf9724b16ece3737b3ac6701815bc965f8fa4279298 + languageName: node + linkType: hard + "uuid@npm:8.3.2, uuid@npm:^8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2" @@ -49407,7 +50634,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:9.0.1, uuid@npm:^9.0.0": +"uuid@npm:9.0.1, uuid@npm:^9.0.0, uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" bin: @@ -50856,6 +52083,16 @@ __metadata: languageName: node linkType: hard +"xml2js@npm:0.6.2": + version: 0.6.2 + resolution: "xml2js@npm:0.6.2" + dependencies: + sax: "npm:>=0.6.0" + xmlbuilder: "npm:~11.0.0" + checksum: e98a84e9c172c556ee2c5afa0fc7161b46919e8b53ab20de140eedea19903ed82f7cd5b1576fb345c84f0a18da1982ddf65908129b58fc3d7cbc658ae232108f + languageName: node + linkType: hard + "xml@npm:^1.0.1": version: 1.0.1 resolution: "xml@npm:1.0.1" @@ -50863,6 +52100,13 @@ __metadata: languageName: node linkType: hard +"xmlbuilder@npm:~11.0.0": + version: 11.0.1 + resolution: "xmlbuilder@npm:11.0.1" + checksum: 74b979f89a0a129926bc786b913459bdbcefa809afaa551c5ab83f89b1915bdaea14c11c759284bb9b931e3b53004dbc2181e21d3ca9553eeb0b2a7b4e40c35b + languageName: node + linkType: hard + "xmlchars@npm:^2.2.0": version: 2.2.0 resolution: "xmlchars@npm:2.2.0" @@ -51305,6 +52549,17 @@ __metadata: languageName: node linkType: hard +"zip-stream@npm:^6.0.1": + version: 6.0.1 + resolution: "zip-stream@npm:6.0.1" + dependencies: + archiver-utils: "npm:^5.0.0" + compress-commons: "npm:^6.0.2" + readable-stream: "npm:^4.0.0" + checksum: 50f2fb30327fb9d09879abf7ae2493705313adf403e794b030151aaae00009162419d60d0519e807673ec04d442e140c8879ca14314df0a0192de3b233e8f28b + languageName: node + linkType: hard + "zod-to-json-schema@npm:^3.22.3, zod-to-json-schema@npm:^3.22.4": version: 3.23.0 resolution: "zod-to-json-schema@npm:3.23.0"