Poc lambda deployment duration (#10340)
closes https://github.com/twentyhq/core-team-issues/issues/436 ## Acheivements Improve aws lambda deployment time from ~10/15 secs to less that 1 sec ## Done - migrate with the new code executor architecture for local and lambda drivers - support old and new executor architecture to avoid breaking changes - first run is long, next runs are quick even if code step is updated ## Demo using `lambda` driver ### Before https://github.com/user-attachments/assets/7f7664b4-658f-4689-8949-ea2c31131252 ### After https://github.com/user-attachments/assets/d486c8e2-f8f8-4dbd-a801-c9901e440b29
This commit is contained in:
@ -34,7 +34,6 @@ const documents = {
|
|||||||
"\n mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) {\n deleteOneRelation(input: { id: $idToDelete }) {\n id\n }\n }\n": types.DeleteOneRelationMetadataItemDocument,
|
"\n mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) {\n deleteOneRelation(input: { id: $idToDelete }) {\n id\n }\n }\n": types.DeleteOneRelationMetadataItemDocument,
|
||||||
"\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n duplicateCriteria\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument,
|
"\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n duplicateCriteria\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument,
|
||||||
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
||||||
"\n \n mutation BuildDraftServerlessFunction(\n $input: BuildDraftServerlessFunctionInput!\n ) {\n buildDraftServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.BuildDraftServerlessFunctionDocument,
|
|
||||||
"\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument,
|
"\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument,
|
||||||
"\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument,
|
"\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument,
|
||||||
"\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument,
|
"\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument,
|
||||||
@ -144,10 +143,6 @@ export function graphql(source: "\n query ObjectMetadataItems {\n objects(pa
|
|||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n"];
|
export function graphql(source: "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n"];
|
||||||
/**
|
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
|
||||||
*/
|
|
||||||
export function graphql(source: "\n \n mutation BuildDraftServerlessFunction(\n $input: BuildDraftServerlessFunctionInput!\n ) {\n buildDraftServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n"): (typeof documents)["\n \n mutation BuildDraftServerlessFunction(\n $input: BuildDraftServerlessFunctionInput!\n ) {\n buildDraftServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n"];
|
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -243,11 +243,6 @@ export type BooleanFieldComparison = {
|
|||||||
isNot?: InputMaybe<Scalars['Boolean']>;
|
isNot?: InputMaybe<Scalars['Boolean']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BuildDraftServerlessFunctionInput = {
|
|
||||||
/** The id of the function. */
|
|
||||||
id: Scalars['ID'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum CalendarChannelVisibility {
|
export enum CalendarChannelVisibility {
|
||||||
METADATA = 'METADATA',
|
METADATA = 'METADATA',
|
||||||
SHARE_EVERYTHING = 'SHARE_EVERYTHING'
|
SHARE_EVERYTHING = 'SHARE_EVERYTHING'
|
||||||
@ -760,7 +755,6 @@ export type Mutation = {
|
|||||||
activateWorkflowVersion: Scalars['Boolean'];
|
activateWorkflowVersion: Scalars['Boolean'];
|
||||||
activateWorkspace: Workspace;
|
activateWorkspace: Workspace;
|
||||||
authorizeApp: AuthorizeApp;
|
authorizeApp: AuthorizeApp;
|
||||||
buildDraftServerlessFunction: ServerlessFunction;
|
|
||||||
checkCustomDomainValidRecords?: Maybe<CustomDomainValidRecords>;
|
checkCustomDomainValidRecords?: Maybe<CustomDomainValidRecords>;
|
||||||
checkoutSession: BillingSessionOutput;
|
checkoutSession: BillingSessionOutput;
|
||||||
computeStepOutputSchema: Scalars['JSON'];
|
computeStepOutputSchema: Scalars['JSON'];
|
||||||
@ -837,11 +831,6 @@ export type MutationAuthorizeAppArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationBuildDraftServerlessFunctionArgs = {
|
|
||||||
input: BuildDraftServerlessFunctionInput;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export type MutationCheckoutSessionArgs = {
|
export type MutationCheckoutSessionArgs = {
|
||||||
plan?: BillingPlanKey;
|
plan?: BillingPlanKey;
|
||||||
recurringInterval: SubscriptionInterval;
|
recurringInterval: SubscriptionInterval;
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import {
|
|||||||
IconSquareRoundedCheck,
|
IconSquareRoundedCheck,
|
||||||
IconSquareRoundedX,
|
IconSquareRoundedX,
|
||||||
IconLoader,
|
IconLoader,
|
||||||
IconSettings,
|
|
||||||
AnimatedCircleLoading,
|
AnimatedCircleLoading,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
import { ServerlessFunctionExecutionStatus } from '~/generated-metadata/graphql';
|
import { ServerlessFunctionExecutionStatus } from '~/generated-metadata/graphql';
|
||||||
@ -41,11 +40,9 @@ const StyledOutput = styled.div<{ accent?: OutputAccent }>`
|
|||||||
export const ServerlessFunctionExecutionResult = ({
|
export const ServerlessFunctionExecutionResult = ({
|
||||||
serverlessFunctionTestData,
|
serverlessFunctionTestData,
|
||||||
isTesting = false,
|
isTesting = false,
|
||||||
isBuilding = false,
|
|
||||||
}: {
|
}: {
|
||||||
serverlessFunctionTestData: ServerlessFunctionTestData;
|
serverlessFunctionTestData: ServerlessFunctionTestData;
|
||||||
isTesting?: boolean;
|
isTesting?: boolean;
|
||||||
isBuilding?: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
@ -70,23 +67,17 @@ export const ServerlessFunctionExecutionResult = ({
|
|||||||
|
|
||||||
const IdleLeftNode = 'Output';
|
const IdleLeftNode = 'Output';
|
||||||
|
|
||||||
const PendingLeftNode = (isTesting || isBuilding) && (
|
const PendingLeftNode = isTesting && (
|
||||||
<StyledOutput>
|
<StyledOutput>
|
||||||
<AnimatedCircleLoading>
|
<AnimatedCircleLoading>
|
||||||
{isTesting ? (
|
<IconLoader size={theme.icon.size.md} />
|
||||||
<IconLoader size={theme.icon.size.md} />
|
|
||||||
) : (
|
|
||||||
<IconSettings size={theme.icon.size.md} />
|
|
||||||
)}
|
|
||||||
</AnimatedCircleLoading>
|
</AnimatedCircleLoading>
|
||||||
<StyledInfoContainer>
|
<StyledInfoContainer>Running function</StyledInfoContainer>
|
||||||
{isTesting ? 'Running function' : 'Building function'}
|
|
||||||
</StyledInfoContainer>
|
|
||||||
</StyledOutput>
|
</StyledOutput>
|
||||||
);
|
);
|
||||||
|
|
||||||
const computeLeftNode = () => {
|
const computeLeftNode = () => {
|
||||||
if (isTesting || isBuilding) {
|
if (isTesting) {
|
||||||
return PendingLeftNode;
|
return PendingLeftNode;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@ -115,7 +106,7 @@ export const ServerlessFunctionExecutionResult = ({
|
|||||||
language={serverlessFunctionTestData.language}
|
language={serverlessFunctionTestData.language}
|
||||||
height={serverlessFunctionTestData.height}
|
height={serverlessFunctionTestData.height}
|
||||||
options={{ readOnly: true, domReadOnly: true }}
|
options={{ readOnly: true, domReadOnly: true }}
|
||||||
isLoading={isTesting || isBuilding}
|
isLoading={isTesting}
|
||||||
withHeader
|
withHeader
|
||||||
/>
|
/>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { useBuildDraftServerlessFunction } from '@/settings/serverless-functions/hooks/useBuildDraftServerlessFunction';
|
|
||||||
import { useExecuteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useExecuteOneServerlessFunction';
|
import { useExecuteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useExecuteOneServerlessFunction';
|
||||||
import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState';
|
import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@ -14,21 +13,12 @@ export const useTestServerlessFunction = ({
|
|||||||
callback?: (testResult: object) => void;
|
callback?: (testResult: object) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [isTesting, setIsTesting] = useState(false);
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
const [isBuilding, setIsBuilding] = useState(false);
|
|
||||||
const { executeOneServerlessFunction } = useExecuteOneServerlessFunction();
|
const { executeOneServerlessFunction } = useExecuteOneServerlessFunction();
|
||||||
const { buildDraftServerlessFunction } = useBuildDraftServerlessFunction();
|
|
||||||
const [serverlessFunctionTestData, setServerlessFunctionTestData] =
|
const [serverlessFunctionTestData, setServerlessFunctionTestData] =
|
||||||
useRecoilState(serverlessFunctionTestDataFamilyState(serverlessFunctionId));
|
useRecoilState(serverlessFunctionTestDataFamilyState(serverlessFunctionId));
|
||||||
|
|
||||||
const testServerlessFunction = async (shouldBuild = true) => {
|
const testServerlessFunction = async () => {
|
||||||
try {
|
try {
|
||||||
if (shouldBuild) {
|
|
||||||
setIsBuilding(true);
|
|
||||||
await buildDraftServerlessFunction({
|
|
||||||
id: serverlessFunctionId,
|
|
||||||
});
|
|
||||||
setIsBuilding(false);
|
|
||||||
}
|
|
||||||
setIsTesting(true);
|
setIsTesting(true);
|
||||||
await sleep(200); // Delay artificially to avoid flashing the UI
|
await sleep(200); // Delay artificially to avoid flashing the UI
|
||||||
const result = await executeOneServerlessFunction({
|
const result = await executeOneServerlessFunction({
|
||||||
@ -67,11 +57,10 @@ export const useTestServerlessFunction = ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setIsBuilding(false);
|
|
||||||
setIsTesting(false);
|
setIsTesting(false);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { testServerlessFunction, isTesting, isBuilding };
|
return { testServerlessFunction, isTesting };
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
import { gql } from '@apollo/client';
|
|
||||||
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
|
|
||||||
|
|
||||||
export const BUILD_DRAFT_SERVERLESS_FUNCTION = gql`
|
|
||||||
${SERVERLESS_FUNCTION_FRAGMENT}
|
|
||||||
mutation BuildDraftServerlessFunction(
|
|
||||||
$input: BuildDraftServerlessFunctionInput!
|
|
||||||
) {
|
|
||||||
buildDraftServerlessFunction(input: $input) {
|
|
||||||
...ServerlessFunctionFields
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
|
||||||
import { useMutation } from '@apollo/client';
|
|
||||||
import { BUILD_DRAFT_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/buildDraftServerlessFunction';
|
|
||||||
import {
|
|
||||||
BuildDraftServerlessFunctionMutation,
|
|
||||||
BuildDraftServerlessFunctionMutationVariables,
|
|
||||||
BuildDraftServerlessFunctionInput,
|
|
||||||
} from '~/generated-metadata/graphql';
|
|
||||||
|
|
||||||
export const useBuildDraftServerlessFunction = () => {
|
|
||||||
const apolloMetadataClient = useApolloMetadataClient();
|
|
||||||
const [mutate] = useMutation<
|
|
||||||
BuildDraftServerlessFunctionMutation,
|
|
||||||
BuildDraftServerlessFunctionMutationVariables
|
|
||||||
>(BUILD_DRAFT_SERVERLESS_FUNCTION, {
|
|
||||||
client: apolloMetadataClient,
|
|
||||||
});
|
|
||||||
|
|
||||||
const buildDraftServerlessFunction = async (
|
|
||||||
input: BuildDraftServerlessFunctionInput,
|
|
||||||
) => {
|
|
||||||
return await mutate({
|
|
||||||
variables: {
|
|
||||||
input,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return { buildDraftServerlessFunction };
|
|
||||||
};
|
|
||||||
@ -81,8 +81,6 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
|||||||
}: WorkflowEditActionFormServerlessFunctionProps) => {
|
}: WorkflowEditActionFormServerlessFunctionProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
const [shouldBuildServerlessFunction, setShouldBuildServerlessFunction] =
|
|
||||||
useState(false);
|
|
||||||
const serverlessFunctionId = action.settings.input.serverlessFunctionId;
|
const serverlessFunctionId = action.settings.input.serverlessFunctionId;
|
||||||
const serverlessFunctionVersion =
|
const serverlessFunctionVersion =
|
||||||
action.settings.input.serverlessFunctionVersion;
|
action.settings.input.serverlessFunctionVersion;
|
||||||
@ -123,14 +121,12 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const { testServerlessFunction, isTesting, isBuilding } =
|
const { testServerlessFunction, isTesting } = useTestServerlessFunction({
|
||||||
useTestServerlessFunction({
|
serverlessFunctionId,
|
||||||
serverlessFunctionId,
|
callback: updateOutputSchemaFromTestResult,
|
||||||
callback: updateOutputSchemaFromTestResult,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const handleSave = useDebouncedCallback(async () => {
|
const handleSave = useDebouncedCallback(async () => {
|
||||||
setShouldBuildServerlessFunction(true);
|
|
||||||
await updateOneServerlessFunction({
|
await updateOneServerlessFunction({
|
||||||
name: formValues.name,
|
name: formValues.name,
|
||||||
description: formValues.description,
|
description: formValues.description,
|
||||||
@ -238,8 +234,7 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isTesting) {
|
if (!isTesting) {
|
||||||
await testServerlessFunction(shouldBuildServerlessFunction);
|
await testServerlessFunction();
|
||||||
setShouldBuildServerlessFunction(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -348,7 +343,6 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
|||||||
<InputLabel>Result</InputLabel>
|
<InputLabel>Result</InputLabel>
|
||||||
<ServerlessFunctionExecutionResult
|
<ServerlessFunctionExecutionResult
|
||||||
serverlessFunctionTestData={serverlessFunctionTestData}
|
serverlessFunctionTestData={serverlessFunctionTestData}
|
||||||
isBuilding={isBuilding}
|
|
||||||
isTesting={isTesting}
|
isTesting={isTesting}
|
||||||
/>
|
/>
|
||||||
</StyledCodeEditorContainer>
|
</StyledCodeEditorContainer>
|
||||||
@ -361,7 +355,7 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
|||||||
<CmdEnterActionButton
|
<CmdEnterActionButton
|
||||||
title="Test"
|
title="Test"
|
||||||
onClick={handleRunFunction}
|
onClick={handleRunFunction}
|
||||||
disabled={isTesting || isBuilding || actionOptions.readonly}
|
disabled={isTesting || actionOptions.readonly}
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -21,6 +21,10 @@
|
|||||||
{
|
{
|
||||||
"include": "**/serverless/drivers/layers/engine/**",
|
"include": "**/serverless/drivers/layers/engine/**",
|
||||||
"outDir": "dist/assets"
|
"outDir": "dist/assets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "**/serverless/drivers/constants/executor/index.mjs",
|
||||||
|
"outDir": "dist/assets"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"watchAssets": true
|
"watchAssets": true
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
|
||||||
|
export const handler = async (event) => {
|
||||||
|
const mainPath = `/tmp/${v4()}.mjs`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { code, params } = event;
|
||||||
|
|
||||||
|
await fs.writeFile(mainPath, code, 'utf8');
|
||||||
|
|
||||||
|
process.env = {}
|
||||||
|
|
||||||
|
const mainFile = await import(mainPath);
|
||||||
|
|
||||||
|
return await mainFile.main(params);
|
||||||
|
} finally {
|
||||||
|
await fs.rm(mainPath, { force: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -16,11 +16,7 @@ export type ServerlessExecuteResult = {
|
|||||||
|
|
||||||
export interface ServerlessDriver {
|
export interface ServerlessDriver {
|
||||||
delete(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
|
delete(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
|
||||||
build(
|
build(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
|
||||||
serverlessFunction: ServerlessFunctionEntity,
|
|
||||||
version: string,
|
|
||||||
): Promise<void>;
|
|
||||||
publish(serverlessFunction: ServerlessFunctionEntity): Promise<string>;
|
|
||||||
execute(
|
execute(
|
||||||
serverlessFunction: ServerlessFunctionEntity,
|
serverlessFunction: ServerlessFunctionEntity,
|
||||||
payload: object,
|
payload: object,
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import ts, { transpileModule } from 'typescript';
|
||||||
import {
|
import {
|
||||||
|
CreateFunctionCommandInput,
|
||||||
CreateFunctionCommand,
|
CreateFunctionCommand,
|
||||||
DeleteFunctionCommand,
|
DeleteFunctionCommand,
|
||||||
GetFunctionCommand,
|
GetFunctionCommand,
|
||||||
@ -13,18 +15,10 @@ import {
|
|||||||
ListLayerVersionsCommandInput,
|
ListLayerVersionsCommandInput,
|
||||||
PublishLayerVersionCommand,
|
PublishLayerVersionCommand,
|
||||||
PublishLayerVersionCommandInput,
|
PublishLayerVersionCommandInput,
|
||||||
PublishVersionCommand,
|
|
||||||
PublishVersionCommandInput,
|
|
||||||
ResourceNotFoundException,
|
ResourceNotFoundException,
|
||||||
UpdateFunctionCodeCommand,
|
|
||||||
UpdateFunctionConfigurationCommand,
|
|
||||||
UpdateFunctionConfigurationCommandInput,
|
|
||||||
waitUntilFunctionUpdatedV2,
|
waitUntilFunctionUpdatedV2,
|
||||||
} from '@aws-sdk/client-lambda';
|
} from '@aws-sdk/client-lambda';
|
||||||
import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts';
|
import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts';
|
||||||
import { CreateFunctionCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/CreateFunctionCommand';
|
|
||||||
import { UpdateFunctionCodeCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/UpdateFunctionCodeCommand';
|
|
||||||
import dotenv from 'dotenv';
|
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -34,10 +28,6 @@ import {
|
|||||||
|
|
||||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||||
import { COMMON_LAYER_NAME } from 'src/engine/core-modules/serverless/drivers/constants/common-layer-name';
|
import { COMMON_LAYER_NAME } from 'src/engine/core-modules/serverless/drivers/constants/common-layer-name';
|
||||||
import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name';
|
|
||||||
import { OUTDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/outdir-folder';
|
|
||||||
import { SERVERLESS_TMPDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/serverless-tmpdir-folder';
|
|
||||||
import { compileTypescript } from 'src/engine/core-modules/serverless/drivers/utils/compile-typescript';
|
|
||||||
import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies';
|
import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies';
|
||||||
import { createZipFile } from 'src/engine/core-modules/serverless/drivers/utils/create-zip-file';
|
import { createZipFile } from 'src/engine/core-modules/serverless/drivers/utils/create-zip-file';
|
||||||
import {
|
import {
|
||||||
@ -54,9 +44,13 @@ import {
|
|||||||
ServerlessFunctionException,
|
ServerlessFunctionException,
|
||||||
ServerlessFunctionExceptionCode,
|
ServerlessFunctionExceptionCode,
|
||||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||||
|
import { copyExecutor } from 'src/engine/core-modules/serverless/drivers/utils/copy-executor';
|
||||||
|
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
|
||||||
|
import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content';
|
||||||
|
|
||||||
const UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS = 60;
|
const UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS = 60;
|
||||||
const CREDENTIALS_DURATION_IN_SECONDS = 10 * 60 * 60; // 10h
|
const CREDENTIALS_DURATION_IN_SECONDS = 10 * 60 * 60; // 10h
|
||||||
|
const LAMBDA_EXECUTOR_DESCRIPTION = 'User script executor';
|
||||||
|
|
||||||
export interface LambdaDriverOptions extends LambdaClientConfig {
|
export interface LambdaDriverOptions extends LambdaClientConfig {
|
||||||
fileStorageService: FileStorageService;
|
fileStorageService: FileStorageService;
|
||||||
@ -127,10 +121,12 @@ export class LambdaDriver implements ServerlessDriver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async waitFunctionUpdates(
|
private async waitFunctionUpdates(
|
||||||
serverlessFunctionId: string,
|
serverlessFunction: ServerlessFunctionEntity,
|
||||||
maxWaitTime: number = UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS,
|
maxWaitTime: number = UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS,
|
||||||
) {
|
) {
|
||||||
const waitParams = { FunctionName: serverlessFunctionId };
|
const waitParams = {
|
||||||
|
FunctionName: serverlessFunction.id,
|
||||||
|
};
|
||||||
|
|
||||||
await waitUntilFunctionUpdatedV2(
|
await waitUntilFunctionUpdatedV2(
|
||||||
{ client: await this.getLambdaClient(), maxWaitTime },
|
{ client: await this.getLambdaClient(), maxWaitTime },
|
||||||
@ -192,29 +188,26 @@ export class LambdaDriver implements ServerlessDriver {
|
|||||||
return result.LayerVersionArn;
|
return result.LayerVersionArn;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkFunctionExists(functionName: string): Promise<boolean> {
|
private async getLambdaExecutor(
|
||||||
|
serverlessFunction: ServerlessFunctionEntity,
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const getFunctionCommand = new GetFunctionCommand({
|
const getFunctionCommand: GetFunctionCommand = new GetFunctionCommand({
|
||||||
FunctionName: functionName,
|
FunctionName: serverlessFunction.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
await (await this.getLambdaClient()).send(getFunctionCommand);
|
return await (await this.getLambdaClient()).send(getFunctionCommand);
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ResourceNotFoundException) {
|
if (!(error instanceof ResourceNotFoundException)) {
|
||||||
return false;
|
throw error;
|
||||||
}
|
}
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(serverlessFunction: ServerlessFunctionEntity) {
|
async delete(serverlessFunction: ServerlessFunctionEntity) {
|
||||||
const functionExists = await this.checkFunctionExists(
|
const lambdaExecutor = await this.getLambdaExecutor(serverlessFunction);
|
||||||
serverlessFunction.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (functionExists) {
|
if (isDefined(lambdaExecutor)) {
|
||||||
const deleteFunctionCommand = new DeleteFunctionCommand({
|
const deleteFunctionCommand = new DeleteFunctionCommand({
|
||||||
FunctionName: serverlessFunction.id,
|
FunctionName: serverlessFunction.id,
|
||||||
});
|
});
|
||||||
@ -223,162 +216,92 @@ export class LambdaDriver implements ServerlessDriver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getInMemoryServerlessFunctionFolderPath = (
|
async build(serverlessFunction: ServerlessFunctionEntity) {
|
||||||
serverlessFunction: ServerlessFunctionEntity,
|
const lambdaExecutor = await this.getLambdaExecutor(serverlessFunction);
|
||||||
version: string,
|
|
||||||
) => {
|
|
||||||
return join(SERVERLESS_TMPDIR_FOLDER, serverlessFunction.id, version);
|
|
||||||
};
|
|
||||||
|
|
||||||
async build(serverlessFunction: ServerlessFunctionEntity, version: 'draft') {
|
if (isDefined(lambdaExecutor)) {
|
||||||
if (version !== 'draft') {
|
if (
|
||||||
throw new Error("We can only build 'draft' version with lambda driver");
|
lambdaExecutor.Configuration?.Description ===
|
||||||
|
LAMBDA_EXECUTOR_DESCRIPTION
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.delete(serverlessFunction);
|
||||||
}
|
}
|
||||||
|
|
||||||
const inMemoryServerlessFunctionFolderPath =
|
const layerArn = await this.createLayerIfNotExists(
|
||||||
this.getInMemoryServerlessFunctionFolderPath(serverlessFunction, version);
|
serverlessFunction.layerVersion,
|
||||||
|
|
||||||
const folderPath = getServerlessFolder({
|
|
||||||
serverlessFunction,
|
|
||||||
version,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.fileStorageService.download({
|
|
||||||
from: { folderPath },
|
|
||||||
to: { folderPath: inMemoryServerlessFunctionFolderPath },
|
|
||||||
});
|
|
||||||
|
|
||||||
compileTypescript(inMemoryServerlessFunctionFolderPath);
|
|
||||||
|
|
||||||
const lambdaZipPath = join(
|
|
||||||
inMemoryServerlessFunctionFolderPath,
|
|
||||||
'lambda.zip',
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await createZipFile(
|
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
|
||||||
join(inMemoryServerlessFunctionFolderPath, OUTDIR_FOLDER),
|
|
||||||
lambdaZipPath,
|
|
||||||
);
|
|
||||||
|
|
||||||
const envFileContent = await fs.readFile(
|
const { sourceTemporaryDir, lambdaZipPath } =
|
||||||
join(inMemoryServerlessFunctionFolderPath, ENV_FILE_NAME),
|
await lambdaBuildDirectoryManager.init();
|
||||||
);
|
|
||||||
|
|
||||||
const envVariables = dotenv.parse(envFileContent);
|
await copyExecutor(sourceTemporaryDir);
|
||||||
|
|
||||||
const functionExists = await this.checkFunctionExists(
|
await createZipFile(sourceTemporaryDir, lambdaZipPath);
|
||||||
serverlessFunction.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!functionExists) {
|
const params: CreateFunctionCommandInput = {
|
||||||
const layerArn = await this.createLayerIfNotExists(
|
Code: {
|
||||||
serverlessFunction.layerVersion,
|
|
||||||
);
|
|
||||||
|
|
||||||
const params: CreateFunctionCommandInput = {
|
|
||||||
Code: {
|
|
||||||
ZipFile: await fs.readFile(lambdaZipPath),
|
|
||||||
},
|
|
||||||
FunctionName: serverlessFunction.id,
|
|
||||||
Handler: 'src/index.main',
|
|
||||||
Layers: [layerArn],
|
|
||||||
Environment: {
|
|
||||||
Variables: envVariables,
|
|
||||||
},
|
|
||||||
Role: this.options.lambdaRole,
|
|
||||||
Runtime: serverlessFunction.runtime,
|
|
||||||
Description: 'Lambda function to run user script',
|
|
||||||
Timeout: serverlessFunction.timeoutSeconds,
|
|
||||||
};
|
|
||||||
|
|
||||||
const command = new CreateFunctionCommand(params);
|
|
||||||
|
|
||||||
await (await this.getLambdaClient()).send(command);
|
|
||||||
} else {
|
|
||||||
const updateCodeParams: UpdateFunctionCodeCommandInput = {
|
|
||||||
ZipFile: await fs.readFile(lambdaZipPath),
|
ZipFile: await fs.readFile(lambdaZipPath),
|
||||||
FunctionName: serverlessFunction.id,
|
},
|
||||||
};
|
|
||||||
|
|
||||||
const updateCodeCommand = new UpdateFunctionCodeCommand(updateCodeParams);
|
|
||||||
|
|
||||||
await (await this.getLambdaClient()).send(updateCodeCommand);
|
|
||||||
|
|
||||||
const updateConfigurationParams: UpdateFunctionConfigurationCommandInput =
|
|
||||||
{
|
|
||||||
Environment: {
|
|
||||||
Variables: envVariables,
|
|
||||||
},
|
|
||||||
FunctionName: serverlessFunction.id,
|
|
||||||
Timeout: serverlessFunction.timeoutSeconds,
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateConfigurationCommand = new UpdateFunctionConfigurationCommand(
|
|
||||||
updateConfigurationParams,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.waitFunctionUpdates(serverlessFunction.id);
|
|
||||||
|
|
||||||
await (await this.getLambdaClient()).send(updateConfigurationCommand);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.waitFunctionUpdates(serverlessFunction.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async publish(serverlessFunction: ServerlessFunctionEntity) {
|
|
||||||
await this.build(serverlessFunction, 'draft');
|
|
||||||
const params: PublishVersionCommandInput = {
|
|
||||||
FunctionName: serverlessFunction.id,
|
FunctionName: serverlessFunction.id,
|
||||||
|
Layers: [layerArn],
|
||||||
|
Handler: 'index.handler',
|
||||||
|
Role: this.options.lambdaRole,
|
||||||
|
Runtime: serverlessFunction.runtime,
|
||||||
|
Description: LAMBDA_EXECUTOR_DESCRIPTION,
|
||||||
|
Timeout: serverlessFunction.timeoutSeconds,
|
||||||
};
|
};
|
||||||
|
|
||||||
const command = new PublishVersionCommand(params);
|
const command = new CreateFunctionCommand(params);
|
||||||
|
|
||||||
const result = await (await this.getLambdaClient()).send(command);
|
await (await this.getLambdaClient()).send(command);
|
||||||
const newVersion = result.Version;
|
|
||||||
|
|
||||||
if (!newVersion) {
|
await lambdaBuildDirectoryManager.clean();
|
||||||
throw new Error('New published version is undefined');
|
|
||||||
}
|
|
||||||
|
|
||||||
const draftFolderPath = getServerlessFolder({
|
|
||||||
serverlessFunction: serverlessFunction,
|
|
||||||
version: 'draft',
|
|
||||||
});
|
|
||||||
const newFolderPath = getServerlessFolder({
|
|
||||||
serverlessFunction: serverlessFunction,
|
|
||||||
version: newVersion,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.fileStorageService.copy({
|
|
||||||
from: { folderPath: draftFolderPath },
|
|
||||||
to: { folderPath: newFolderPath },
|
|
||||||
});
|
|
||||||
|
|
||||||
return newVersion;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
functionToExecute: ServerlessFunctionEntity,
|
serverlessFunction: ServerlessFunctionEntity,
|
||||||
payload: object,
|
payload: object,
|
||||||
version: string,
|
version: string,
|
||||||
): Promise<ServerlessExecuteResult> {
|
): Promise<ServerlessExecuteResult> {
|
||||||
const computedVersion =
|
await this.build(serverlessFunction);
|
||||||
version === 'latest' ? functionToExecute.latestVersion : version;
|
await this.waitFunctionUpdates(serverlessFunction);
|
||||||
|
|
||||||
const functionName =
|
|
||||||
computedVersion === 'draft'
|
|
||||||
? functionToExecute.id
|
|
||||||
: `${functionToExecute.id}:${computedVersion}`;
|
|
||||||
|
|
||||||
if (version === 'draft') {
|
|
||||||
await this.waitFunctionUpdates(functionToExecute.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const computedVersion =
|
||||||
|
version === 'latest' ? serverlessFunction.latestVersion : version;
|
||||||
|
|
||||||
|
const folderPath = getServerlessFolder({
|
||||||
|
serverlessFunction,
|
||||||
|
version: computedVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tsCodeStream = await this.fileStorageService.read({
|
||||||
|
folderPath: join(folderPath, 'src'),
|
||||||
|
filename: INDEX_FILE_NAME,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tsCode = await readFileContent(tsCodeStream);
|
||||||
|
|
||||||
|
const compiledCode = transpileModule(tsCode, {
|
||||||
|
compilerOptions: {
|
||||||
|
module: ts.ModuleKind.ESNext,
|
||||||
|
target: ts.ScriptTarget.ES2017,
|
||||||
|
},
|
||||||
|
}).outputText;
|
||||||
|
|
||||||
|
const executorPayload = {
|
||||||
|
params: payload,
|
||||||
|
code: compiledCode,
|
||||||
|
};
|
||||||
|
|
||||||
const params: InvokeCommandInput = {
|
const params: InvokeCommandInput = {
|
||||||
FunctionName: functionName,
|
FunctionName: serverlessFunction.id,
|
||||||
Payload: JSON.stringify(payload),
|
Payload: JSON.stringify(executorPayload),
|
||||||
};
|
};
|
||||||
|
|
||||||
const command = new InvokeCommand(params);
|
const command = new InvokeCommand(params);
|
||||||
|
|||||||
@ -1,27 +1,23 @@
|
|||||||
import { fork } from 'child_process';
|
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
import dotenv from 'dotenv';
|
import ts, { transpileModule } from 'typescript';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ServerlessDriver,
|
ServerlessDriver,
|
||||||
ServerlessExecuteError,
|
|
||||||
ServerlessExecuteResult,
|
ServerlessExecuteResult,
|
||||||
} from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
|
} from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
|
||||||
|
|
||||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||||
import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
|
import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
|
||||||
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
|
||||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||||
import { COMMON_LAYER_NAME } from 'src/engine/core-modules/serverless/drivers/constants/common-layer-name';
|
import { COMMON_LAYER_NAME } from 'src/engine/core-modules/serverless/drivers/constants/common-layer-name';
|
||||||
import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies';
|
import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies';
|
||||||
import { SERVERLESS_TMPDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/serverless-tmpdir-folder';
|
import { SERVERLESS_TMPDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/serverless-tmpdir-folder';
|
||||||
import { compileTypescript } from 'src/engine/core-modules/serverless/drivers/utils/compile-typescript';
|
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
|
||||||
import { OUTDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/outdir-folder';
|
import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content';
|
||||||
import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name';
|
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
||||||
|
|
||||||
const LISTENER_FILE_NAME = 'listener.js';
|
|
||||||
|
|
||||||
export interface LocalDriverOptions {
|
export interface LocalDriverOptions {
|
||||||
fileStorageService: FileStorageService;
|
fileStorageService: FileStorageService;
|
||||||
@ -34,13 +30,6 @@ export class LocalDriver implements ServerlessDriver {
|
|||||||
this.fileStorageService = options.fileStorageService;
|
this.fileStorageService = options.fileStorageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getInMemoryServerlessFunctionFolderPath = (
|
|
||||||
serverlessFunction: ServerlessFunctionEntity,
|
|
||||||
version: string,
|
|
||||||
) => {
|
|
||||||
return join(SERVERLESS_TMPDIR_FOLDER, serverlessFunction.id, version);
|
|
||||||
};
|
|
||||||
|
|
||||||
private getInMemoryLayerFolderPath = (version: number) => {
|
private getInMemoryLayerFolderPath = (version: number) => {
|
||||||
return join(SERVERLESS_TMPDIR_FOLDER, COMMON_LAYER_NAME, `${version}`);
|
return join(SERVERLESS_TMPDIR_FOLDER, COMMON_LAYER_NAME, `${version}`);
|
||||||
};
|
};
|
||||||
@ -58,107 +47,29 @@ export class LocalDriver implements ServerlessDriver {
|
|||||||
|
|
||||||
async delete() {}
|
async delete() {}
|
||||||
|
|
||||||
async build(serverlessFunction: ServerlessFunctionEntity, version: string) {
|
async build(serverlessFunction: ServerlessFunctionEntity) {
|
||||||
const computedVersion =
|
|
||||||
version === 'latest' ? serverlessFunction.latestVersion : version;
|
|
||||||
|
|
||||||
await this.createLayerIfNotExists(serverlessFunction.layerVersion);
|
await this.createLayerIfNotExists(serverlessFunction.layerVersion);
|
||||||
|
|
||||||
const inMemoryServerlessFunctionFolderPath =
|
|
||||||
this.getInMemoryServerlessFunctionFolderPath(
|
|
||||||
serverlessFunction,
|
|
||||||
computedVersion,
|
|
||||||
);
|
|
||||||
|
|
||||||
const folderPath = getServerlessFolder({
|
|
||||||
serverlessFunction,
|
|
||||||
version,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.fileStorageService.download({
|
|
||||||
from: { folderPath },
|
|
||||||
to: { folderPath: inMemoryServerlessFunctionFolderPath },
|
|
||||||
});
|
|
||||||
|
|
||||||
compileTypescript(inMemoryServerlessFunctionFolderPath);
|
|
||||||
|
|
||||||
const envFileContent = await fs.readFile(
|
|
||||||
join(inMemoryServerlessFunctionFolderPath, ENV_FILE_NAME),
|
|
||||||
);
|
|
||||||
|
|
||||||
const envVariables = dotenv.parse(envFileContent);
|
|
||||||
|
|
||||||
const listener = `
|
|
||||||
const index_1 = require("./src/index");
|
|
||||||
|
|
||||||
process.env = ${JSON.stringify(envVariables)}
|
|
||||||
|
|
||||||
process.on('message', async (message) => {
|
|
||||||
const { params } = message;
|
|
||||||
try {
|
|
||||||
const result = await index_1.main(params);
|
|
||||||
process.send(result);
|
|
||||||
} catch (error) {
|
|
||||||
process.send({
|
|
||||||
errorType: error.name,
|
|
||||||
errorMessage: error.message,
|
|
||||||
stackTrace: error.stack.split('\\n').filter((line) => line.trim() !== ''),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
`;
|
|
||||||
|
|
||||||
await fs.writeFile(
|
|
||||||
join(
|
|
||||||
inMemoryServerlessFunctionFolderPath,
|
|
||||||
OUTDIR_FOLDER,
|
|
||||||
LISTENER_FILE_NAME,
|
|
||||||
),
|
|
||||||
listener,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fs.symlink(
|
|
||||||
join(
|
|
||||||
this.getInMemoryLayerFolderPath(serverlessFunction.layerVersion),
|
|
||||||
'node_modules',
|
|
||||||
),
|
|
||||||
join(
|
|
||||||
inMemoryServerlessFunctionFolderPath,
|
|
||||||
OUTDIR_FOLDER,
|
|
||||||
'node_modules',
|
|
||||||
),
|
|
||||||
'dir',
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code !== 'EEXIST') {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async publish(serverlessFunction: ServerlessFunctionEntity) {
|
private async executeWithTimeout<T>(
|
||||||
const newVersion = serverlessFunction.latestVersion
|
fn: () => Promise<T>,
|
||||||
? `${parseInt(serverlessFunction.latestVersion, 10) + 1}`
|
timeoutMs: number,
|
||||||
: '1';
|
): Promise<T> {
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
reject(new Error(`Task timed out after ${timeoutMs / 1_000} seconds`));
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
const draftFolderPath = getServerlessFolder({
|
fn()
|
||||||
serverlessFunction: serverlessFunction,
|
.then((result) => {
|
||||||
version: 'draft',
|
clearTimeout(timer);
|
||||||
|
resolve(result);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
const newFolderPath = getServerlessFolder({
|
|
||||||
serverlessFunction: serverlessFunction,
|
|
||||||
version: newVersion,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.fileStorageService.copy({
|
|
||||||
from: { folderPath: draftFolderPath },
|
|
||||||
to: { folderPath: newFolderPath },
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.build(serverlessFunction, newVersion);
|
|
||||||
|
|
||||||
return newVersion;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
@ -166,100 +77,73 @@ export class LocalDriver implements ServerlessDriver {
|
|||||||
payload: object,
|
payload: object,
|
||||||
version: string,
|
version: string,
|
||||||
): Promise<ServerlessExecuteResult> {
|
): Promise<ServerlessExecuteResult> {
|
||||||
|
await this.build(serverlessFunction);
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
const computedVersion =
|
const computedVersion =
|
||||||
version === 'latest' ? serverlessFunction.latestVersion : version;
|
version === 'latest' ? serverlessFunction.latestVersion : version;
|
||||||
|
|
||||||
const listenerFile = join(
|
const folderPath = getServerlessFolder({
|
||||||
this.getInMemoryServerlessFunctionFolderPath(
|
serverlessFunction,
|
||||||
serverlessFunction,
|
version: computedVersion,
|
||||||
computedVersion,
|
});
|
||||||
),
|
|
||||||
OUTDIR_FOLDER,
|
const tsCodeStream = await this.fileStorageService.read({
|
||||||
LISTENER_FILE_NAME,
|
folderPath: join(folderPath, 'src'),
|
||||||
|
filename: INDEX_FILE_NAME,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tsCode = await readFileContent(tsCodeStream);
|
||||||
|
|
||||||
|
const compiledCode = transpileModule(tsCode, {
|
||||||
|
compilerOptions: {
|
||||||
|
module: ts.ModuleKind.CommonJS,
|
||||||
|
target: ts.ScriptTarget.ES2017,
|
||||||
|
},
|
||||||
|
}).outputText;
|
||||||
|
|
||||||
|
const compiledCodeFolderPath = join(
|
||||||
|
SERVERLESS_TMPDIR_FOLDER,
|
||||||
|
`compiled-code-${v4()}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const compiledCodeFilePath = join(compiledCodeFolderPath, 'main.js');
|
||||||
|
|
||||||
|
await fs.mkdir(compiledCodeFolderPath, { recursive: true });
|
||||||
|
|
||||||
|
await fs.writeFile(compiledCodeFilePath, compiledCode, 'utf8');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await new Promise((resolve, reject) => {
|
await fs.symlink(
|
||||||
const child = fork(listenerFile, { silent: true });
|
join(
|
||||||
|
this.getInMemoryLayerFolderPath(serverlessFunction.layerVersion),
|
||||||
|
'node_modules',
|
||||||
|
),
|
||||||
|
join(compiledCodeFolderPath, 'node_modules'),
|
||||||
|
'dir',
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'EEXIST') {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const timeoutMs = serverlessFunction.timeoutSeconds * 1_000;
|
try {
|
||||||
|
const mainFile = await import(compiledCodeFilePath);
|
||||||
|
|
||||||
const timeoutHandler = setTimeout(() => {
|
const result = await this.executeWithTimeout<object | null>(
|
||||||
child.kill();
|
() => mainFile.main(payload),
|
||||||
const duration = Date.now() - startTime;
|
serverlessFunction.timeoutSeconds * 1_000,
|
||||||
|
);
|
||||||
|
|
||||||
reject(new Error(`Task timed out after ${duration / 1_000} seconds`));
|
const duration = Date.now() - startTime;
|
||||||
}, timeoutMs);
|
|
||||||
|
|
||||||
child.on('message', (message: object | ServerlessExecuteError) => {
|
return {
|
||||||
clearTimeout(timeoutHandler);
|
data: result,
|
||||||
const duration = Date.now() - startTime;
|
duration,
|
||||||
|
status: ServerlessFunctionExecutionStatus.SUCCESS,
|
||||||
if ('errorType' in message) {
|
};
|
||||||
resolve({
|
|
||||||
data: null,
|
|
||||||
duration,
|
|
||||||
error: message,
|
|
||||||
status: ServerlessFunctionExecutionStatus.ERROR,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
resolve({
|
|
||||||
data: message,
|
|
||||||
duration,
|
|
||||||
status: ServerlessFunctionExecutionStatus.SUCCESS,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
child.kill();
|
|
||||||
});
|
|
||||||
|
|
||||||
child.stderr?.on('data', (data) => {
|
|
||||||
clearTimeout(timeoutHandler);
|
|
||||||
const stackTrace = data
|
|
||||||
.toString()
|
|
||||||
.split('\n')
|
|
||||||
.filter((line: string) => line.trim() !== '');
|
|
||||||
const errorTrace = stackTrace.filter((line: string) =>
|
|
||||||
line.includes('Error: '),
|
|
||||||
)?.[0];
|
|
||||||
|
|
||||||
let errorType = 'Unknown';
|
|
||||||
let errorMessage = '';
|
|
||||||
|
|
||||||
if (errorTrace) {
|
|
||||||
errorType = errorTrace.split(':')[0];
|
|
||||||
errorMessage = errorTrace.split(': ')[1];
|
|
||||||
}
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
data: null,
|
|
||||||
duration,
|
|
||||||
status: ServerlessFunctionExecutionStatus.ERROR,
|
|
||||||
error: {
|
|
||||||
errorType,
|
|
||||||
errorMessage,
|
|
||||||
stackTrace: stackTrace,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
child.kill();
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on('error', (error) => {
|
|
||||||
clearTimeout(timeoutHandler);
|
|
||||||
reject(error);
|
|
||||||
child.kill();
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on('exit', (code) => {
|
|
||||||
clearTimeout(timeoutHandler);
|
|
||||||
if (code && code !== 0) {
|
|
||||||
reject(new Error(`Child process exited with code ${code}`));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
child.send({ params: payload });
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
data: null,
|
data: null,
|
||||||
@ -271,6 +155,8 @@ export class LocalDriver implements ServerlessDriver {
|
|||||||
},
|
},
|
||||||
status: ServerlessFunctionExecutionStatus.ERROR,
|
status: ServerlessFunctionExecutionStatus.ERROR,
|
||||||
};
|
};
|
||||||
|
} finally {
|
||||||
|
await fs.rm(compiledCodeFolderPath, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
import ts, { createProgram } from 'typescript';
|
|
||||||
|
|
||||||
import { OUTDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/outdir-folder';
|
|
||||||
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
|
|
||||||
|
|
||||||
export const compileTypescript = (folderPath: string) => {
|
|
||||||
const options: ts.CompilerOptions = {
|
|
||||||
module: ts.ModuleKind.CommonJS,
|
|
||||||
target: ts.ScriptTarget.ES2017,
|
|
||||||
moduleResolution: ts.ModuleResolutionKind.Node10,
|
|
||||||
esModuleInterop: true,
|
|
||||||
resolveJsonModule: true,
|
|
||||||
allowSyntheticDefaultImports: true,
|
|
||||||
outDir: join(folderPath, OUTDIR_FOLDER, 'src'),
|
|
||||||
types: ['node'],
|
|
||||||
};
|
|
||||||
|
|
||||||
createProgram([join(folderPath, 'src', INDEX_FILE_NAME)], options).emit();
|
|
||||||
};
|
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { promises as fs } from 'fs';
|
||||||
|
|
||||||
|
import { getExecutorFilePath } from 'src/engine/core-modules/serverless/drivers/utils/get-executor-file-path';
|
||||||
|
|
||||||
|
export const copyExecutor = async (buildDirectory: string) => {
|
||||||
|
await fs.mkdir(buildDirectory, {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
await fs.cp(getExecutorFilePath(), buildDirectory, {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { ASSET_PATH } from 'src/constants/assets-path';
|
||||||
|
|
||||||
|
export const getExecutorFilePath = (): string => {
|
||||||
|
const baseTypescriptProjectPath = path.join(
|
||||||
|
ASSET_PATH,
|
||||||
|
`engine/core-modules/serverless/drivers/constants/executor`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return path.resolve(__dirname, baseTypescriptProjectPath);
|
||||||
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import path, { join } from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version';
|
import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version';
|
||||||
import { ASSET_PATH } from 'src/constants/assets-path';
|
import { ASSET_PATH } from 'src/constants/assets-path';
|
||||||
@ -8,10 +8,10 @@ export const getLayerDependenciesDirName = (
|
|||||||
): string => {
|
): string => {
|
||||||
const formattedVersion = version === 'latest' ? LAST_LAYER_VERSION : version;
|
const formattedVersion = version === 'latest' ? LAST_LAYER_VERSION : version;
|
||||||
|
|
||||||
const baseTypescriptProjectPath = join(
|
const baseTypescriptProjectPath = path.join(
|
||||||
ASSET_PATH,
|
ASSET_PATH,
|
||||||
`engine/core-modules/serverless/drivers/layers/${formattedVersion}`,
|
`engine/core-modules/serverless/drivers/layers/${formattedVersion}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return path.resolve(baseTypescriptProjectPath);
|
return path.resolve(__dirname, baseTypescriptProjectPath);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,15 +16,8 @@ export class ServerlessService implements ServerlessDriver {
|
|||||||
return this.driver.delete(serverlessFunction);
|
return this.driver.delete(serverlessFunction);
|
||||||
}
|
}
|
||||||
|
|
||||||
async build(
|
async build(serverlessFunction: ServerlessFunctionEntity): Promise<void> {
|
||||||
serverlessFunction: ServerlessFunctionEntity,
|
return this.driver.build(serverlessFunction);
|
||||||
version: string,
|
|
||||||
): Promise<void> {
|
|
||||||
return this.driver.build(serverlessFunction, version);
|
|
||||||
}
|
|
||||||
|
|
||||||
async publish(serverlessFunction: ServerlessFunctionEntity): Promise<string> {
|
|
||||||
return this.driver.publish(serverlessFunction);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
|
|||||||
@ -24,7 +24,6 @@ import {
|
|||||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||||
import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils';
|
import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils';
|
||||||
import { BuildDraftServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/build-draft-serverless-function.input';
|
|
||||||
|
|
||||||
@UseGuards(WorkspaceAuthGuard)
|
@UseGuards(WorkspaceAuthGuard)
|
||||||
@Resolver()
|
@Resolver()
|
||||||
@ -205,22 +204,4 @@ export class ServerlessFunctionResolver {
|
|||||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => ServerlessFunctionDTO)
|
|
||||||
async buildDraftServerlessFunction(
|
|
||||||
@Args('input') input: BuildDraftServerlessFunctionInput,
|
|
||||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
await this.checkFeatureFlag(workspaceId);
|
|
||||||
const { id } = input;
|
|
||||||
|
|
||||||
return await this.serverlessFunctionService.buildDraftServerlessFunction(
|
|
||||||
id,
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -137,21 +137,12 @@ export class ServerlessFunctionService {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
|
||||||
version === 'draft' &&
|
|
||||||
functionToExecute.syncStatus !== ServerlessFunctionSyncStatus.READY
|
|
||||||
) {
|
|
||||||
await this.buildDraftServerlessFunction(
|
|
||||||
functionToExecute.id,
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resultServerlessFunction = await this.serverlessService.execute(
|
const resultServerlessFunction = await this.serverlessService.execute(
|
||||||
functionToExecute,
|
functionToExecute,
|
||||||
payload,
|
payload,
|
||||||
version,
|
version,
|
||||||
);
|
);
|
||||||
|
|
||||||
const eventInput = {
|
const eventInput = {
|
||||||
action: 'serverlessFunction.executed',
|
action: 'serverlessFunction.executed',
|
||||||
payload: {
|
payload: {
|
||||||
@ -200,9 +191,23 @@ export class ServerlessFunctionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newVersion = await this.serverlessService.publish(
|
const newVersion = existingServerlessFunction.latestVersion
|
||||||
existingServerlessFunction,
|
? `${parseInt(existingServerlessFunction.latestVersion, 10) + 1}`
|
||||||
);
|
: '1';
|
||||||
|
|
||||||
|
const draftFolderPath = getServerlessFolder({
|
||||||
|
serverlessFunction: existingServerlessFunction,
|
||||||
|
version: 'draft',
|
||||||
|
});
|
||||||
|
const newFolderPath = getServerlessFolder({
|
||||||
|
serverlessFunction: existingServerlessFunction,
|
||||||
|
version: newVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.fileStorageService.copy({
|
||||||
|
from: { folderPath: draftFolderPath },
|
||||||
|
to: { folderPath: newFolderPath },
|
||||||
|
});
|
||||||
|
|
||||||
const newPublishedVersions = [
|
const newPublishedVersions = [
|
||||||
...existingServerlessFunction.publishedVersions,
|
...existingServerlessFunction.publishedVersions,
|
||||||
@ -264,7 +269,6 @@ export class ServerlessFunctionService {
|
|||||||
{
|
{
|
||||||
name: serverlessFunctionInput.name,
|
name: serverlessFunctionInput.name,
|
||||||
description: serverlessFunctionInput.description,
|
description: serverlessFunctionInput.description,
|
||||||
syncStatus: ServerlessFunctionSyncStatus.NOT_READY,
|
|
||||||
timeoutSeconds: serverlessFunctionInput.timeoutSeconds,
|
timeoutSeconds: serverlessFunctionInput.timeoutSeconds,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -343,6 +347,8 @@ export class ServerlessFunctionService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.serverlessService.build(createdServerlessFunction);
|
||||||
|
|
||||||
return this.serverlessFunctionRepository.findOneBy({
|
return this.serverlessFunctionRepository.findOneBy({
|
||||||
id: createdServerlessFunction.id,
|
id: createdServerlessFunction.id,
|
||||||
});
|
});
|
||||||
@ -380,10 +386,6 @@ export class ServerlessFunctionService {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.serverlessFunctionRepository.update(serverlessFunction.id, {
|
|
||||||
syncStatus: ServerlessFunctionSyncStatus.NOT_READY,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async throttleExecution(workspaceId: string) {
|
private async throttleExecution(workspaceId: string) {
|
||||||
@ -400,32 +402,4 @@ export class ServerlessFunctionService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async buildDraftServerlessFunction(id: string, workspaceId: string) {
|
|
||||||
const functionToBuild = await this.findOneOrFail({
|
|
||||||
id,
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (functionToBuild.syncStatus === ServerlessFunctionSyncStatus.READY) {
|
|
||||||
return functionToBuild;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (functionToBuild.syncStatus === ServerlessFunctionSyncStatus.BUILDING) {
|
|
||||||
throw new ServerlessFunctionException(
|
|
||||||
'This function is currently building. Please try later',
|
|
||||||
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_BUILDING,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.serverlessFunctionRepository.update(functionToBuild.id, {
|
|
||||||
syncStatus: ServerlessFunctionSyncStatus.BUILDING,
|
|
||||||
});
|
|
||||||
await this.serverlessService.build(functionToBuild, 'draft');
|
|
||||||
await this.serverlessFunctionRepository.update(functionToBuild.id, {
|
|
||||||
syncStatus: ServerlessFunctionSyncStatus.READY,
|
|
||||||
});
|
|
||||||
|
|
||||||
return functionToBuild;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user