6654 serverless functions add a deploy button disable deploy when autosave (#6715)
- improvements on serverless function behavior (autosave performances, deploy on execution only) - add versioning to serverless functions - add a publish endpoint to create a new version of a serverless function - add deploy and reset to lastVersion button in the settings section: <img width="736" alt="image" src="https://github.com/user-attachments/assets/2001f8d2-07a4-4f79-84dd-ec74b6f301d3">
This commit is contained in:
@ -33,13 +33,15 @@ const documents = {
|
|||||||
"\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n }\n }\n": types.DeleteOneFieldMetadataItemDocument,
|
"\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n }\n }\n": types.DeleteOneFieldMetadataItemDocument,
|
||||||
"\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 $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\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 fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n createdAt\n updatedAt\n fromRelationMetadata {\n id\n relationType\n toObjectMetadata {\n id\n dataSourceId\n nameSingular\n namePlural\n isSystem\n isRemote\n }\n toFieldMetadataId\n }\n toRelationMetadata {\n id\n relationType\n fromObjectMetadata {\n id\n dataSourceId\n nameSingular\n namePlural\n isSystem\n isRemote\n }\n fromFieldMetadataId\n }\n defaultValue\n options\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 pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument,
|
"\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\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 fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n createdAt\n updatedAt\n fromRelationMetadata {\n id\n relationType\n toObjectMetadata {\n id\n dataSourceId\n nameSingular\n namePlural\n isSystem\n isRemote\n }\n toFieldMetadataId\n }\n toRelationMetadata {\n id\n relationType\n fromObjectMetadata {\n id\n dataSourceId\n nameSingular\n namePlural\n isSystem\n isRemote\n }\n fromFieldMetadataId\n }\n defaultValue\n options\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 pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\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 sourceCodeHash\n sourceCodeFullPath\n runtime\n syncStatus\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n sourceCodeHash\n runtime\n syncStatus\n latestVersion\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
||||||
"\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: DeleteServerlessFunctionInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument,
|
"\n \n mutation DeleteOneServerlessFunction($input: DeleteServerlessFunctionInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument,
|
||||||
"\n mutation ExecuteOneServerlessFunction($id: UUID!, $payload: JSON!) {\n executeOneServerlessFunction(id: $id, payload: $payload) {\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,
|
||||||
|
"\n \n mutation PublishOneServerlessFunction(\n $input: PublishServerlessFunctionInput!\n ) {\n publishServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.PublishOneServerlessFunctionDocument,
|
||||||
"\n \n mutation UpdateOneServerlessFunction($input: UpdateServerlessFunctionInput!) {\n updateOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.UpdateOneServerlessFunctionDocument,
|
"\n \n mutation UpdateOneServerlessFunction($input: UpdateServerlessFunctionInput!) {\n updateOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.UpdateOneServerlessFunctionDocument,
|
||||||
"\n \n query GetManyServerlessFunctions {\n serverlessFunctions(paging: { first: 100 }) {\n edges {\n node {\n ...ServerlessFunctionFields\n }\n }\n }\n }\n": types.GetManyServerlessFunctionsDocument,
|
"\n \n query GetManyServerlessFunctions {\n serverlessFunctions(paging: { first: 100 }) {\n edges {\n node {\n ...ServerlessFunctionFields\n }\n }\n }\n }\n": types.GetManyServerlessFunctionsDocument,
|
||||||
"\n \n query GetOneServerlessFunction($id: UUID!) {\n serverlessFunction(id: $id) {\n ...ServerlessFunctionFields\n }\n }\n": types.GetOneServerlessFunctionDocument,
|
"\n \n query GetOneServerlessFunction($id: UUID!) {\n serverlessFunction(id: $id) {\n ...ServerlessFunctionFields\n }\n }\n": types.GetOneServerlessFunctionDocument,
|
||||||
|
"\n query FindOneServerlessFunctionSourceCode(\n $input: GetServerlessFunctionSourceCodeInput!\n ) {\n getServerlessFunctionSourceCode(input: $input)\n }\n": types.FindOneServerlessFunctionSourceCodeDocument,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -139,7 +141,7 @@ export function graphql(source: "\n query ObjectMetadataItems(\n $objectFilt
|
|||||||
/**
|
/**
|
||||||
* 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 sourceCodeHash\n sourceCodeFullPath\n runtime\n syncStatus\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n sourceCodeHash\n sourceCodeFullPath\n runtime\n syncStatus\n createdAt\n updatedAt\n }\n"];
|
export function graphql(source: "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n sourceCodeHash\n runtime\n syncStatus\n latestVersion\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n sourceCodeHash\n runtime\n syncStatus\n latestVersion\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.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
@ -151,7 +153,11 @@ export function graphql(source: "\n \n mutation DeleteOneServerlessFunction($i
|
|||||||
/**
|
/**
|
||||||
* 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 mutation ExecuteOneServerlessFunction($id: UUID!, $payload: JSON!) {\n executeOneServerlessFunction(id: $id, payload: $payload) {\n data\n duration\n status\n error\n }\n }\n"): (typeof documents)["\n mutation ExecuteOneServerlessFunction($id: UUID!, $payload: JSON!) {\n executeOneServerlessFunction(id: $id, payload: $payload) {\n data\n duration\n status\n error\n }\n }\n"];
|
export function graphql(source: "\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\n }\n"): (typeof documents)["\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\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 PublishOneServerlessFunction(\n $input: PublishServerlessFunctionInput!\n ) {\n publishServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n"): (typeof documents)["\n \n mutation PublishOneServerlessFunction(\n $input: PublishServerlessFunctionInput!\n ) {\n publishServerlessFunction(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.
|
||||||
*/
|
*/
|
||||||
@ -164,6 +170,10 @@ export function graphql(source: "\n \n query GetManyServerlessFunctions {\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.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n \n query GetOneServerlessFunction($id: UUID!) {\n serverlessFunction(id: $id) {\n ...ServerlessFunctionFields\n }\n }\n"): (typeof documents)["\n \n query GetOneServerlessFunction($id: UUID!) {\n serverlessFunction(id: $id) {\n ...ServerlessFunctionFields\n }\n }\n"];
|
export function graphql(source: "\n \n query GetOneServerlessFunction($id: UUID!) {\n serverlessFunction(id: $id) {\n ...ServerlessFunctionFields\n }\n }\n"): (typeof documents)["\n \n query GetOneServerlessFunction($id: UUID!) {\n serverlessFunction(id: $id) {\n ...ServerlessFunctionFields\n }\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 query FindOneServerlessFunctionSourceCode(\n $input: GetServerlessFunctionSourceCodeInput!\n ) {\n getServerlessFunctionSourceCode(input: $input)\n }\n"): (typeof documents)["\n query FindOneServerlessFunctionSourceCode(\n $input: GetServerlessFunctionSourceCodeInput!\n ) {\n getServerlessFunctionSourceCode(input: $input)\n }\n"];
|
||||||
|
|
||||||
export function graphql(source: string) {
|
export function graphql(source: string) {
|
||||||
return (documents as any)[source] ?? {};
|
return (documents as any)[source] ?? {};
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -58,7 +58,9 @@ export const useRestoreManyRecords = ({
|
|||||||
|
|
||||||
// TODO: fix optimistic effect
|
// TODO: fix optimistic effect
|
||||||
const findOneQueryName = `FindOne${capitalize(objectNameSingular)}`;
|
const findOneQueryName = `FindOne${capitalize(objectNameSingular)}`;
|
||||||
const findManyQueryName = `FindMany${capitalize(objectMetadataItem.namePlural)}`;
|
const findManyQueryName = `FindMany${capitalize(
|
||||||
|
objectMetadataItem.namePlural,
|
||||||
|
)}`;
|
||||||
|
|
||||||
const restoredRecordsResponse = await apolloClient.mutate({
|
const restoredRecordsResponse = await apolloClient.mutate({
|
||||||
mutation: restoreManyRecordsMutation,
|
mutation: restoreManyRecordsMutation,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { H2Title, IconPlayerPlay } from 'twenty-ui';
|
import { H2Title, IconPlayerPlay, IconGitCommit, IconRestore } from 'twenty-ui';
|
||||||
import { CodeEditor } from '@/ui/input/code-editor/components/CodeEditor';
|
import { CodeEditor } from '@/ui/input/code-editor/components/CodeEditor';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||||
@ -14,13 +14,23 @@ const StyledTabList = styled(TabList)`
|
|||||||
export const SettingsServerlessFunctionCodeEditorTab = ({
|
export const SettingsServerlessFunctionCodeEditorTab = ({
|
||||||
formValues,
|
formValues,
|
||||||
handleExecute,
|
handleExecute,
|
||||||
|
handlePublish,
|
||||||
|
handleReset,
|
||||||
|
resetDisabled,
|
||||||
|
publishDisabled,
|
||||||
onChange,
|
onChange,
|
||||||
|
setIsCodeValid,
|
||||||
}: {
|
}: {
|
||||||
formValues: ServerlessFunctionFormValues;
|
formValues: ServerlessFunctionFormValues;
|
||||||
handleExecute: () => void;
|
handleExecute: () => void;
|
||||||
|
handlePublish: () => void;
|
||||||
|
handleReset: () => void;
|
||||||
|
resetDisabled: boolean;
|
||||||
|
publishDisabled: boolean;
|
||||||
onChange: (key: string) => (value: string) => void;
|
onChange: (key: string) => (value: string) => void;
|
||||||
|
setIsCodeValid: (isCodeValid: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const HeaderButton = (
|
const TestButton = (
|
||||||
<Button
|
<Button
|
||||||
title="Test"
|
title="Test"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
@ -30,6 +40,26 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
|
|||||||
onClick={handleExecute}
|
onClick={handleExecute}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
const PublishButton = (
|
||||||
|
<Button
|
||||||
|
title="Publish"
|
||||||
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
Icon={IconGitCommit}
|
||||||
|
onClick={handlePublish}
|
||||||
|
disabled={publishDisabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const ResetButton = (
|
||||||
|
<Button
|
||||||
|
title="Reset"
|
||||||
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
Icon={IconRestore}
|
||||||
|
onClick={handleReset}
|
||||||
|
disabled={resetDisabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const TAB_LIST_COMPONENT_ID = 'serverless-function-editor';
|
const TAB_LIST_COMPONENT_ID = 'serverless-function-editor';
|
||||||
|
|
||||||
@ -41,7 +71,10 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const Header = (
|
const Header = (
|
||||||
<CoreEditorHeader leftNodes={[HeaderTabList]} rightNodes={[HeaderButton]} />
|
<CoreEditorHeader
|
||||||
|
leftNodes={[HeaderTabList]}
|
||||||
|
rightNodes={[ResetButton, PublishButton, TestButton]}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -53,6 +86,7 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
|
|||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={formValues.code}
|
value={formValues.code}
|
||||||
onChange={onChange('code')}
|
onChange={onChange('code')}
|
||||||
|
setIsCodeValid={setIsCodeValid}
|
||||||
header={Header}
|
header={Header}
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import {
|
import {
|
||||||
DEFAULT_OUTPUT_VALUE,
|
DEFAULT_OUTPUT_VALUE,
|
||||||
@ -13,11 +13,16 @@ export const SettingsServerlessFunctionTestTabEffect = () => {
|
|||||||
const setSettingsServerlessFunctionCodeEditorOutputParams = useSetRecoilState(
|
const setSettingsServerlessFunctionCodeEditorOutputParams = useSetRecoilState(
|
||||||
settingsServerlessFunctionCodeEditorOutputParamsState,
|
settingsServerlessFunctionCodeEditorOutputParamsState,
|
||||||
);
|
);
|
||||||
if (settingsServerlessFunctionOutput.data !== DEFAULT_OUTPUT_VALUE) {
|
useEffect(() => {
|
||||||
setSettingsServerlessFunctionCodeEditorOutputParams({
|
if (settingsServerlessFunctionOutput.data !== DEFAULT_OUTPUT_VALUE) {
|
||||||
language: 'json',
|
setSettingsServerlessFunctionCodeEditorOutputParams({
|
||||||
height: 300,
|
language: 'json',
|
||||||
});
|
height: 300,
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
settingsServerlessFunctionOutput.data,
|
||||||
|
setSettingsServerlessFunctionCodeEditorOutputParams,
|
||||||
|
]);
|
||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,9 +6,9 @@ export const SERVERLESS_FUNCTION_FRAGMENT = gql`
|
|||||||
name
|
name
|
||||||
description
|
description
|
||||||
sourceCodeHash
|
sourceCodeHash
|
||||||
sourceCodeFullPath
|
|
||||||
runtime
|
runtime
|
||||||
syncStatus
|
syncStatus
|
||||||
|
latestVersion
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
export const EXECUTE_ONE_SERVERLESS_FUNCTION = gql`
|
export const EXECUTE_ONE_SERVERLESS_FUNCTION = gql`
|
||||||
mutation ExecuteOneServerlessFunction($id: UUID!, $payload: JSON!) {
|
mutation ExecuteOneServerlessFunction(
|
||||||
executeOneServerlessFunction(id: $id, payload: $payload) {
|
$input: ExecuteServerlessFunctionInput!
|
||||||
|
) {
|
||||||
|
executeOneServerlessFunction(input: $input) {
|
||||||
data
|
data
|
||||||
duration
|
duration
|
||||||
status
|
status
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
|
||||||
|
|
||||||
|
export const PUBLISH_ONE_SERVERLESS_FUNCTION = gql`
|
||||||
|
${SERVERLESS_FUNCTION_FRAGMENT}
|
||||||
|
mutation PublishOneServerlessFunction(
|
||||||
|
$input: PublishServerlessFunctionInput!
|
||||||
|
) {
|
||||||
|
publishServerlessFunction(input: $input) {
|
||||||
|
...ServerlessFunctionFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE = gql`
|
||||||
|
query FindOneServerlessFunctionSourceCode(
|
||||||
|
$input: GetServerlessFunctionSourceCodeInput!
|
||||||
|
) {
|
||||||
|
getServerlessFunctionSourceCode(input: $input)
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -9,6 +9,13 @@ jest.mock(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
'@/settings/serverless-functions/hooks/useGetOneServerlessFunctionSourceCode',
|
||||||
|
() => ({
|
||||||
|
useGetOneServerlessFunctionSourceCode: jest.fn(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
describe('useServerlessFunctionUpdateFormState', () => {
|
describe('useServerlessFunctionUpdateFormState', () => {
|
||||||
test('should return a form', () => {
|
test('should return a form', () => {
|
||||||
const serverlessFunctionId = 'serverlessFunctionId';
|
const serverlessFunctionId = 'serverlessFunctionId';
|
||||||
@ -20,6 +27,14 @@ describe('useServerlessFunctionUpdateFormState', () => {
|
|||||||
serverlessFunction: { sourceCodeFullPath: undefined },
|
serverlessFunction: { sourceCodeFullPath: undefined },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
const useGetOneServerlessFunctionSourceCodeMock = jest.requireMock(
|
||||||
|
'@/settings/serverless-functions/hooks/useGetOneServerlessFunctionSourceCode',
|
||||||
|
);
|
||||||
|
useGetOneServerlessFunctionSourceCodeMock.useGetOneServerlessFunctionSourceCode.mockReturnValue(
|
||||||
|
{
|
||||||
|
code: 'export const handler = () => {}',
|
||||||
|
},
|
||||||
|
);
|
||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() => useServerlessFunctionUpdateFormState(serverlessFunctionId),
|
() => useServerlessFunctionUpdateFormState(serverlessFunctionId),
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||||
import { ApolloClient, useMutation } from '@apollo/client';
|
import { ApolloClient, useMutation } from '@apollo/client';
|
||||||
import { getOperationName } from '@apollo/client/utilities';
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions';
|
|
||||||
import { DELETE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/deleteOneServerlessFunction';
|
import { DELETE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/deleteOneServerlessFunction';
|
||||||
import {
|
import {
|
||||||
DeleteServerlessFunctionInput,
|
DeleteServerlessFunctionInput,
|
||||||
DeleteOneServerlessFunctionMutation,
|
DeleteOneServerlessFunctionMutation,
|
||||||
DeleteOneServerlessFunctionMutationVariables,
|
DeleteOneServerlessFunctionMutationVariables,
|
||||||
} from '~/generated-metadata/graphql';
|
} from '~/generated-metadata/graphql';
|
||||||
|
import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode';
|
||||||
|
|
||||||
export const useDeleteOneServerlessFunction = () => {
|
export const useDeleteOneServerlessFunction = () => {
|
||||||
const apolloMetadataClient = useApolloMetadataClient();
|
const apolloMetadataClient = useApolloMetadataClient();
|
||||||
@ -26,7 +26,9 @@ export const useDeleteOneServerlessFunction = () => {
|
|||||||
input,
|
input,
|
||||||
},
|
},
|
||||||
awaitRefetchQueries: true,
|
awaitRefetchQueries: true,
|
||||||
refetchQueries: [getOperationName(FIND_MANY_SERVERLESS_FUNCTIONS) ?? ''],
|
refetchQueries: [
|
||||||
|
getOperationName(FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE) ?? '',
|
||||||
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetada
|
|||||||
import { ApolloClient, useMutation } from '@apollo/client';
|
import { ApolloClient, useMutation } from '@apollo/client';
|
||||||
import { EXECUTE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/executeOneServerlessFunction';
|
import { EXECUTE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/executeOneServerlessFunction';
|
||||||
import {
|
import {
|
||||||
|
ExecuteServerlessFunctionInput,
|
||||||
ExecuteOneServerlessFunctionMutation,
|
ExecuteOneServerlessFunctionMutation,
|
||||||
ExecuteOneServerlessFunctionMutationVariables,
|
ExecuteOneServerlessFunctionMutationVariables,
|
||||||
} from '~/generated-metadata/graphql';
|
} from '~/generated-metadata/graphql';
|
||||||
@ -16,13 +17,11 @@ export const useExecuteOneServerlessFunction = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const executeOneServerlessFunction = async (
|
const executeOneServerlessFunction = async (
|
||||||
id: string,
|
input: ExecuteServerlessFunctionInput,
|
||||||
payload: object = {},
|
|
||||||
) => {
|
) => {
|
||||||
return await mutate({
|
return await mutate({
|
||||||
variables: {
|
variables: {
|
||||||
id,
|
input,
|
||||||
payload,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { useQuery } from '@apollo/client';
|
||||||
|
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||||
|
import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode';
|
||||||
|
import {
|
||||||
|
FindOneServerlessFunctionSourceCodeQuery,
|
||||||
|
FindOneServerlessFunctionSourceCodeQueryVariables,
|
||||||
|
} from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
export const useGetOneServerlessFunctionSourceCode = ({
|
||||||
|
id,
|
||||||
|
version,
|
||||||
|
onCompleted,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
version: string;
|
||||||
|
onCompleted?: (data: FindOneServerlessFunctionSourceCodeQuery) => void;
|
||||||
|
}) => {
|
||||||
|
const apolloMetadataClient = useApolloMetadataClient();
|
||||||
|
const { data, loading } = useQuery<
|
||||||
|
FindOneServerlessFunctionSourceCodeQuery,
|
||||||
|
FindOneServerlessFunctionSourceCodeQueryVariables
|
||||||
|
>(FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE, {
|
||||||
|
client: apolloMetadataClient ?? undefined,
|
||||||
|
variables: {
|
||||||
|
input: { id, version },
|
||||||
|
},
|
||||||
|
onCompleted,
|
||||||
|
});
|
||||||
|
return { code: data?.getServerlessFunctionSourceCode, loading };
|
||||||
|
};
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import { ApolloClient, useMutation } from '@apollo/client';
|
||||||
|
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||||
|
import { PUBLISH_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/publishOneServerlessFunction';
|
||||||
|
import {
|
||||||
|
PublishServerlessFunctionInput,
|
||||||
|
PublishOneServerlessFunctionMutation,
|
||||||
|
PublishOneServerlessFunctionMutationVariables,
|
||||||
|
} from '~/generated-metadata/graphql';
|
||||||
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
|
import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode';
|
||||||
|
|
||||||
|
export const usePublishOneServerlessFunction = () => {
|
||||||
|
const apolloMetadataClient = useApolloMetadataClient();
|
||||||
|
const [mutate] = useMutation<
|
||||||
|
PublishOneServerlessFunctionMutation,
|
||||||
|
PublishOneServerlessFunctionMutationVariables
|
||||||
|
>(PUBLISH_ONE_SERVERLESS_FUNCTION, {
|
||||||
|
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
|
||||||
|
});
|
||||||
|
|
||||||
|
const publishOneServerlessFunction = async (
|
||||||
|
input: PublishServerlessFunctionInput,
|
||||||
|
) => {
|
||||||
|
return await mutate({
|
||||||
|
variables: {
|
||||||
|
input,
|
||||||
|
},
|
||||||
|
awaitRefetchQueries: true,
|
||||||
|
refetchQueries: [
|
||||||
|
getOperationName(FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE) ?? '',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { publishOneServerlessFunction };
|
||||||
|
};
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
import { Dispatch, SetStateAction, useState } from 'react';
|
||||||
import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
import { useGetOneServerlessFunction } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunction';
|
import { useGetOneServerlessFunction } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunction';
|
||||||
|
import { useGetOneServerlessFunctionSourceCode } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunctionSourceCode';
|
||||||
|
import { FindOneServerlessFunctionSourceCodeQuery } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
export type ServerlessFunctionNewFormValues = {
|
export type ServerlessFunctionNewFormValues = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -28,30 +28,21 @@ export const useServerlessFunctionUpdateFormState = (
|
|||||||
const { serverlessFunction } =
|
const { serverlessFunction } =
|
||||||
useGetOneServerlessFunction(serverlessFunctionId);
|
useGetOneServerlessFunction(serverlessFunctionId);
|
||||||
|
|
||||||
useEffect(() => {
|
useGetOneServerlessFunctionSourceCode({
|
||||||
const getFileContent = async () => {
|
id: serverlessFunctionId,
|
||||||
const resp = await fetch(
|
version: 'draft',
|
||||||
getFileAbsoluteURI(serverlessFunction?.sourceCodeFullPath),
|
onCompleted: (data: FindOneServerlessFunctionSourceCodeQuery) => {
|
||||||
);
|
const newState = {
|
||||||
if (resp.status !== 200) {
|
code: data?.getServerlessFunctionSourceCode || '',
|
||||||
throw new Error('Network response was not ok');
|
name: serverlessFunction?.name || '',
|
||||||
} else {
|
description: serverlessFunction?.description || '',
|
||||||
const result = await resp.text();
|
};
|
||||||
const newState = {
|
setFormValues((prevState) => ({
|
||||||
code: result,
|
...prevState,
|
||||||
name: serverlessFunction?.name || '',
|
...newState,
|
||||||
description: serverlessFunction?.description || '',
|
}));
|
||||||
};
|
},
|
||||||
setFormValues((prevState) => ({
|
});
|
||||||
...prevState,
|
|
||||||
...newState,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (isDefined(serverlessFunction?.sourceCodeFullPath)) {
|
|
||||||
getFileContent();
|
|
||||||
}
|
|
||||||
}, [serverlessFunction, setFormValues]);
|
|
||||||
|
|
||||||
return [formValues, setFormValues];
|
return [formValues, setFormValues];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import Editor, { Monaco, EditorProps } from '@monaco-editor/react';
|
import Editor, { Monaco, EditorProps } from '@monaco-editor/react';
|
||||||
import { editor } from 'monaco-editor';
|
import { editor, MarkerSeverity } from 'monaco-editor';
|
||||||
import { codeEditorTheme } from '@/ui/input/code-editor/theme/CodeEditorTheme';
|
import { codeEditorTheme } from '@/ui/input/code-editor/theme/CodeEditorTheme';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
@ -24,17 +24,20 @@ const StyledEditor = styled(Editor)`
|
|||||||
type CodeEditorProps = Omit<EditorProps, 'onChange'> & {
|
type CodeEditorProps = Omit<EditorProps, 'onChange'> & {
|
||||||
header: React.ReactNode;
|
header: React.ReactNode;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
|
setIsCodeValid?: (isCodeValid: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CodeEditor = ({
|
export const CodeEditor = ({
|
||||||
value = DEFAULT_CODE,
|
value = DEFAULT_CODE,
|
||||||
onChange,
|
onChange,
|
||||||
|
setIsCodeValid,
|
||||||
language = 'typescript',
|
language = 'typescript',
|
||||||
height = 500,
|
height = 450,
|
||||||
options = undefined,
|
options = undefined,
|
||||||
header,
|
header,
|
||||||
}: CodeEditorProps) => {
|
}: CodeEditorProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const handleEditorDidMount = (
|
const handleEditorDidMount = (
|
||||||
editor: editor.IStandaloneCodeEditor,
|
editor: editor.IStandaloneCodeEditor,
|
||||||
monaco: Monaco,
|
monaco: Monaco,
|
||||||
@ -42,6 +45,17 @@ export const CodeEditor = ({
|
|||||||
monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme));
|
monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme));
|
||||||
monaco.editor.setTheme('codeEditorTheme');
|
monaco.editor.setTheme('codeEditorTheme');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEditorValidation = (markers: editor.IMarker[]) => {
|
||||||
|
for (const marker of markers) {
|
||||||
|
if (marker.severity === MarkerSeverity.Error) {
|
||||||
|
setIsCodeValid?.(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsCodeValid?.(true);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.innerHTML = `
|
style.innerHTML = `
|
||||||
@ -63,6 +77,7 @@ export const CodeEditor = ({
|
|||||||
value={value}
|
value={value}
|
||||||
onMount={handleEditorDidMount}
|
onMount={handleEditorDidMount}
|
||||||
onChange={(value?: string) => value && onChange?.(value)}
|
onChange={(value?: string) => value && onChange?.(value)}
|
||||||
|
onValidate={handleEditorValidation}
|
||||||
options={{
|
options={{
|
||||||
...options,
|
...options,
|
||||||
overviewRulerLanes: 0,
|
overviewRulerLanes: 0,
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/setting
|
|||||||
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
||||||
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
||||||
import { useResetRecoilState } from 'recoil';
|
import { useResetRecoilState } from 'recoil';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export const ResetServerlessFunctionStatesEffect = () => {
|
export const ResetServerlessFunctionStatesEffect = () => {
|
||||||
const resetSettingsServerlessFunctionInput = useResetRecoilState(
|
const resetSettingsServerlessFunctionInput = useResetRecoilState(
|
||||||
@ -13,8 +14,14 @@ export const ResetServerlessFunctionStatesEffect = () => {
|
|||||||
const resetSettingsServerlessFunctionCodeEditorOutputParamsState =
|
const resetSettingsServerlessFunctionCodeEditorOutputParamsState =
|
||||||
useResetRecoilState(settingsServerlessFunctionCodeEditorOutputParamsState);
|
useResetRecoilState(settingsServerlessFunctionCodeEditorOutputParamsState);
|
||||||
|
|
||||||
resetSettingsServerlessFunctionInput();
|
useEffect(() => {
|
||||||
resetSettingsServerlessFunctionOutput();
|
resetSettingsServerlessFunctionInput();
|
||||||
resetSettingsServerlessFunctionCodeEditorOutputParamsState();
|
resetSettingsServerlessFunctionOutput();
|
||||||
|
resetSettingsServerlessFunctionCodeEditorOutputParamsState();
|
||||||
|
}, [
|
||||||
|
resetSettingsServerlessFunctionInput,
|
||||||
|
resetSettingsServerlessFunctionOutput,
|
||||||
|
resetSettingsServerlessFunctionCodeEditorOutputParamsState,
|
||||||
|
]);
|
||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { SettingsServerlessFunctionTestTabEffect } from '@/settings/serverless-f
|
|||||||
import { useExecuteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useExecuteOneServerlessFunction';
|
import { useExecuteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useExecuteOneServerlessFunction';
|
||||||
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||||
import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction';
|
import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction';
|
||||||
|
import { usePublishOneServerlessFunction } from '@/settings/serverless-functions/hooks/usePublishOneServerlessFunction';
|
||||||
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
||||||
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
@ -18,7 +19,10 @@ import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { IconCode, IconFunction, IconSettings, IconTestPipe } from 'twenty-ui';
|
import { IconCode, IconFunction, IconSettings, IconTestPipe } from 'twenty-ui';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
import { useGetOneServerlessFunctionSourceCode } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunctionSourceCode';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
const TAB_LIST_COMPONENT_ID = 'serverless-function-detail';
|
const TAB_LIST_COMPONENT_ID = 'serverless-function-detail';
|
||||||
|
|
||||||
@ -29,10 +33,16 @@ export const SettingsServerlessFunctionDetail = () => {
|
|||||||
TAB_LIST_COMPONENT_ID,
|
TAB_LIST_COMPONENT_ID,
|
||||||
);
|
);
|
||||||
const activeTabId = useRecoilValue(activeTabIdState);
|
const activeTabId = useRecoilValue(activeTabIdState);
|
||||||
|
const [isCodeValid, setIsCodeValid] = useState(true);
|
||||||
const { executeOneServerlessFunction } = useExecuteOneServerlessFunction();
|
const { executeOneServerlessFunction } = useExecuteOneServerlessFunction();
|
||||||
const { updateOneServerlessFunction } = useUpdateOneServerlessFunction();
|
const { updateOneServerlessFunction } = useUpdateOneServerlessFunction();
|
||||||
|
const { publishOneServerlessFunction } = usePublishOneServerlessFunction();
|
||||||
const [formValues, setFormValues] =
|
const [formValues, setFormValues] =
|
||||||
useServerlessFunctionUpdateFormState(serverlessFunctionId);
|
useServerlessFunctionUpdateFormState(serverlessFunctionId);
|
||||||
|
const { code: latestVersionCode } = useGetOneServerlessFunctionSourceCode({
|
||||||
|
id: serverlessFunctionId,
|
||||||
|
version: 'latest',
|
||||||
|
});
|
||||||
const setSettingsServerlessFunctionOutput = useSetRecoilState(
|
const setSettingsServerlessFunctionOutput = useSetRecoilState(
|
||||||
settingsServerlessFunctionOutputState,
|
settingsServerlessFunctionOutputState,
|
||||||
);
|
);
|
||||||
@ -70,13 +80,56 @@ export const SettingsServerlessFunctionDetail = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExecute = async () => {
|
const resetDisabled =
|
||||||
await handleSave();
|
!isDefined(latestVersionCode) || latestVersionCode === formValues.code;
|
||||||
|
const publishDisabled = !isCodeValid || latestVersionCode === formValues.code;
|
||||||
|
|
||||||
|
const handleReset = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await executeOneServerlessFunction(
|
const newState = {
|
||||||
serverlessFunctionId,
|
code: latestVersionCode || '',
|
||||||
JSON.parse(settingsServerlessFunctionInput),
|
};
|
||||||
|
setFormValues((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
...newState,
|
||||||
|
}));
|
||||||
|
await handleSave();
|
||||||
|
} catch (err) {
|
||||||
|
enqueueSnackBar(
|
||||||
|
(err as Error)?.message || 'An error occurred while reset function',
|
||||||
|
{
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePublish = async () => {
|
||||||
|
try {
|
||||||
|
await publishOneServerlessFunction({
|
||||||
|
id: serverlessFunctionId,
|
||||||
|
});
|
||||||
|
enqueueSnackBar(`New function version has been published`, {
|
||||||
|
variant: SnackBarVariant.Success,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
enqueueSnackBar(
|
||||||
|
(err as Error)?.message ||
|
||||||
|
'An error occurred while publishing new version',
|
||||||
|
{
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExecute = async () => {
|
||||||
|
try {
|
||||||
|
const result = await executeOneServerlessFunction({
|
||||||
|
id: serverlessFunctionId,
|
||||||
|
payload: JSON.parse(settingsServerlessFunctionInput),
|
||||||
|
version: 'draft',
|
||||||
|
});
|
||||||
setSettingsServerlessFunctionOutput({
|
setSettingsServerlessFunctionOutput({
|
||||||
data: result?.data?.executeOneServerlessFunction?.data
|
data: result?.data?.executeOneServerlessFunction?.data
|
||||||
? JSON.stringify(
|
? JSON.stringify(
|
||||||
@ -119,7 +172,12 @@ export const SettingsServerlessFunctionDetail = () => {
|
|||||||
<SettingsServerlessFunctionCodeEditorTab
|
<SettingsServerlessFunctionCodeEditorTab
|
||||||
formValues={formValues}
|
formValues={formValues}
|
||||||
handleExecute={handleExecute}
|
handleExecute={handleExecute}
|
||||||
|
handlePublish={handlePublish}
|
||||||
|
handleReset={handleReset}
|
||||||
|
resetDisabled={resetDisabled}
|
||||||
|
publishDisabled={publishDisabled}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
setIsCodeValid={setIsCodeValid}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'test':
|
case 'test':
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class UpdateServerlessFunction1724171584314
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'UpdateServerlessFunction1724171584314';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."serverlessFunction" DROP COLUMN "sourceCodeFullPath"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."serverlessFunction" ADD "latestVersion" character varying`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."serverlessFunction" DROP COLUMN "latestVersion"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."serverlessFunction" ADD "sourceCodeFullPath" character varying NOT NULL`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,4 +13,8 @@ export interface StorageDriver {
|
|||||||
from: { folderPath: string; filename: string };
|
from: { folderPath: string; filename: string };
|
||||||
to: { folderPath: string; filename: string };
|
to: { folderPath: string; filename: string };
|
||||||
}): Promise<void>;
|
}): Promise<void>;
|
||||||
|
copy(params: {
|
||||||
|
from: { folderPath: string; filename?: string };
|
||||||
|
to: { folderPath: string; filename?: string };
|
||||||
|
}): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -70,6 +70,13 @@ export class LocalDriver implements StorageDriver {
|
|||||||
params.filename,
|
params.filename,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!existsSync(filePath)) {
|
||||||
|
throw new FileStorageException(
|
||||||
|
'File not found',
|
||||||
|
FileStorageExceptionCode.FILE_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return createReadStream(filePath);
|
return createReadStream(filePath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -115,4 +122,34 @@ export class LocalDriver implements StorageDriver {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async copy(params: {
|
||||||
|
from: { folderPath: string; filename?: string };
|
||||||
|
to: { folderPath: string; filename?: string };
|
||||||
|
}): Promise<void> {
|
||||||
|
const fromPath = join(
|
||||||
|
`${this.options.storagePath}/`,
|
||||||
|
params.from.folderPath,
|
||||||
|
params.from.filename || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const toPath = join(
|
||||||
|
`${this.options.storagePath}/`,
|
||||||
|
params.to.folderPath,
|
||||||
|
params.to.filename || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.cp(fromPath, toPath, { recursive: true });
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
throw new FileStorageException(
|
||||||
|
'File not found',
|
||||||
|
FileStorageExceptionCode.FILE_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -187,6 +187,42 @@ export class S3Driver implements StorageDriver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async copy(params: {
|
||||||
|
from: { folderPath: string; filename?: string };
|
||||||
|
to: { folderPath: string; filename?: string };
|
||||||
|
}): Promise<void> {
|
||||||
|
const fromKey = `${params.from.folderPath}/${params.from.filename || ''}`;
|
||||||
|
const toKey = `${params.to.folderPath}/${params.to.filename || ''}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if the source file exists
|
||||||
|
await this.s3Client.send(
|
||||||
|
new HeadObjectCommand({
|
||||||
|
Bucket: this.bucketName,
|
||||||
|
Key: fromKey,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Copy the object to the new location
|
||||||
|
await this.s3Client.send(
|
||||||
|
new CopyObjectCommand({
|
||||||
|
CopySource: `${this.bucketName}/${fromKey}`,
|
||||||
|
Bucket: this.bucketName,
|
||||||
|
Key: toKey,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === 'NotFound') {
|
||||||
|
throw new FileStorageException(
|
||||||
|
'File not found',
|
||||||
|
FileStorageExceptionCode.FILE_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// For other errors, throw the original error
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async checkBucketExists(args: HeadBucketCommandInput) {
|
async checkBucketExists(args: HeadBucketCommandInput) {
|
||||||
try {
|
try {
|
||||||
await this.s3Client.headBucket(args);
|
await this.s3Client.headBucket(args);
|
||||||
|
|||||||
@ -33,4 +33,11 @@ export class FileStorageService implements StorageDriver {
|
|||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
return this.driver.move(params);
|
return this.driver.move(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copy(params: {
|
||||||
|
from: { folderPath: string; filename?: string };
|
||||||
|
to: { folderPath: string; filename?: string };
|
||||||
|
}): Promise<void> {
|
||||||
|
return this.driver.copy(params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +1,19 @@
|
|||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
|
||||||
|
|
||||||
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
|
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
|
||||||
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
|
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
|
||||||
import { SOURCE_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/source-file-name';
|
import { SOURCE_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/source-file-name';
|
||||||
import { compileTypescript } from 'src/engine/integrations/serverless/drivers/utils/compile-typescript';
|
import { compileTypescript } from 'src/engine/integrations/serverless/drivers/utils/compile-typescript';
|
||||||
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 { getServerlessFolder } from 'src/engine/integrations/serverless/utils/serverless-get-folder.utils';
|
||||||
|
|
||||||
export class BaseServerlessDriver {
|
export class BaseServerlessDriver {
|
||||||
getFolderPath(serverlessFunction: ServerlessFunctionEntity) {
|
|
||||||
return join(
|
|
||||||
'workspace-' + serverlessFunction.workspaceId,
|
|
||||||
FileFolder.ServerlessFunction,
|
|
||||||
serverlessFunction.id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCompiledCode(
|
async getCompiledCode(
|
||||||
serverlessFunction: ServerlessFunctionEntity,
|
serverlessFunction: ServerlessFunctionEntity,
|
||||||
fileStorageService: FileStorageService,
|
fileStorageService: FileStorageService,
|
||||||
) {
|
) {
|
||||||
const folderPath = this.getFolderPath(serverlessFunction);
|
const folderPath = getServerlessFolder({
|
||||||
|
serverlessFunction,
|
||||||
|
version: 'draft',
|
||||||
|
});
|
||||||
const fileStream = await fileStorageService.read({
|
const fileStream = await fileStorageService.read({
|
||||||
folderPath,
|
folderPath,
|
||||||
filename: SOURCE_FILE_NAME,
|
filename: SOURCE_FILE_NAME,
|
||||||
|
|||||||
@ -16,9 +16,14 @@ export type ServerlessExecuteResult = {
|
|||||||
|
|
||||||
export interface ServerlessDriver {
|
export interface ServerlessDriver {
|
||||||
delete(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
|
delete(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
|
||||||
build(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
|
build(
|
||||||
|
serverlessFunction: ServerlessFunctionEntity,
|
||||||
|
version: string,
|
||||||
|
): Promise<void>;
|
||||||
|
publish(serverlessFunction: ServerlessFunctionEntity): Promise<string>;
|
||||||
execute(
|
execute(
|
||||||
serverlessFunction: ServerlessFunctionEntity,
|
serverlessFunction: ServerlessFunctionEntity,
|
||||||
payload: object | undefined,
|
payload: object | undefined,
|
||||||
|
version: string,
|
||||||
): Promise<ServerlessExecuteResult>;
|
): Promise<ServerlessExecuteResult>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,9 @@ import {
|
|||||||
UpdateFunctionCodeCommand,
|
UpdateFunctionCodeCommand,
|
||||||
DeleteFunctionCommand,
|
DeleteFunctionCommand,
|
||||||
ResourceNotFoundException,
|
ResourceNotFoundException,
|
||||||
|
waitUntilFunctionUpdatedV2,
|
||||||
|
PublishVersionCommandInput,
|
||||||
|
PublishVersionCommand,
|
||||||
} from '@aws-sdk/client-lambda';
|
} from '@aws-sdk/client-lambda';
|
||||||
import { CreateFunctionCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/CreateFunctionCommand';
|
import { CreateFunctionCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/CreateFunctionCommand';
|
||||||
import { UpdateFunctionCodeCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/UpdateFunctionCodeCommand';
|
import { UpdateFunctionCodeCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/UpdateFunctionCodeCommand';
|
||||||
@ -24,6 +27,10 @@ import { FileStorageService } from 'src/engine/integrations/file-storage/file-st
|
|||||||
import { BaseServerlessDriver } from 'src/engine/integrations/serverless/drivers/base-serverless.driver';
|
import { BaseServerlessDriver } from 'src/engine/integrations/serverless/drivers/base-serverless.driver';
|
||||||
import { BuildDirectoryManagerService } from 'src/engine/integrations/serverless/drivers/services/build-directory-manager.service';
|
import { BuildDirectoryManagerService } from 'src/engine/integrations/serverless/drivers/services/build-directory-manager.service';
|
||||||
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
||||||
|
import {
|
||||||
|
ServerlessFunctionException,
|
||||||
|
ServerlessFunctionExceptionCode,
|
||||||
|
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||||
|
|
||||||
export interface LambdaDriverOptions extends LambdaClientConfig {
|
export interface LambdaDriverOptions extends LambdaClientConfig {
|
||||||
fileStorageService: FileStorageService;
|
fileStorageService: FileStorageService;
|
||||||
@ -51,12 +58,10 @@ export class LambdaDriver
|
|||||||
this.buildDirectoryManagerService = options.buildDirectoryManagerService;
|
this.buildDirectoryManagerService = options.buildDirectoryManagerService;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkFunctionExists(
|
private async checkFunctionExists(functionName: string): Promise<boolean> {
|
||||||
serverlessFunctionId: string,
|
|
||||||
): Promise<boolean> {
|
|
||||||
try {
|
try {
|
||||||
const getFunctionCommand = new GetFunctionCommand({
|
const getFunctionCommand = new GetFunctionCommand({
|
||||||
FunctionName: serverlessFunctionId,
|
FunctionName: functionName,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.lambdaClient.send(getFunctionCommand);
|
await this.lambdaClient.send(getFunctionCommand);
|
||||||
@ -132,42 +137,85 @@ export class LambdaDriver
|
|||||||
await this.lambdaClient.send(command);
|
await this.lambdaClient.send(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const waitParams = { FunctionName: serverlessFunction.id };
|
||||||
|
|
||||||
|
await waitUntilFunctionUpdatedV2(
|
||||||
|
{ client: this.lambdaClient, maxWaitTime: 5 },
|
||||||
|
waitParams,
|
||||||
|
);
|
||||||
|
|
||||||
await this.buildDirectoryManagerService.clean();
|
await this.buildDirectoryManagerService.clean();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async publish(serverlessFunction: ServerlessFunctionEntity) {
|
||||||
|
await this.build(serverlessFunction);
|
||||||
|
const params: PublishVersionCommandInput = {
|
||||||
|
FunctionName: serverlessFunction.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const command = new PublishVersionCommand(params);
|
||||||
|
|
||||||
|
const result = await this.lambdaClient.send(command);
|
||||||
|
const newVersion = result.Version;
|
||||||
|
|
||||||
|
if (!newVersion) {
|
||||||
|
throw new Error('New published version is undefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
return newVersion;
|
||||||
|
}
|
||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
functionToExecute: ServerlessFunctionEntity,
|
functionToExecute: ServerlessFunctionEntity,
|
||||||
payload: object | undefined = undefined,
|
payload: object | undefined = undefined,
|
||||||
|
version: string,
|
||||||
): Promise<ServerlessExecuteResult> {
|
): Promise<ServerlessExecuteResult> {
|
||||||
|
const computedVersion =
|
||||||
|
version === 'latest' ? functionToExecute.latestVersion : version;
|
||||||
|
|
||||||
|
const functionName =
|
||||||
|
computedVersion === 'draft'
|
||||||
|
? functionToExecute.id
|
||||||
|
: `${functionToExecute.id}:${computedVersion}`;
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const params = {
|
const params = {
|
||||||
FunctionName: functionToExecute.id,
|
FunctionName: functionName,
|
||||||
Payload: JSON.stringify(payload),
|
Payload: JSON.stringify(payload),
|
||||||
};
|
};
|
||||||
|
|
||||||
const command = new InvokeCommand(params);
|
const command = new InvokeCommand(params);
|
||||||
|
|
||||||
const result = await this.lambdaClient.send(command);
|
try {
|
||||||
|
const result = await this.lambdaClient.send(command);
|
||||||
|
|
||||||
const parsedResult = result.Payload
|
const parsedResult = result.Payload
|
||||||
? JSON.parse(result.Payload.transformToString())
|
? JSON.parse(result.Payload.transformToString())
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
if (result.FunctionError) {
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
duration,
|
||||||
|
status: ServerlessFunctionExecutionStatus.ERROR,
|
||||||
|
error: parsedResult,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (result.FunctionError) {
|
|
||||||
return {
|
return {
|
||||||
data: null,
|
data: parsedResult,
|
||||||
duration,
|
duration,
|
||||||
status: ServerlessFunctionExecutionStatus.ERROR,
|
status: ServerlessFunctionExecutionStatus.SUCCESS,
|
||||||
error: parsedResult,
|
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ResourceNotFoundException) {
|
||||||
|
throw new ServerlessFunctionException(
|
||||||
|
`Function Version '${version}' does not exist`,
|
||||||
|
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
data: parsedResult,
|
|
||||||
duration,
|
|
||||||
status: ServerlessFunctionExecutionStatus.SUCCESS,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
ServerlessExecuteError,
|
ServerlessExecuteError,
|
||||||
ServerlessExecuteResult,
|
ServerlessExecuteResult,
|
||||||
} from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
|
} from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
|
||||||
|
import { FileStorageExceptionCode } from 'src/engine/integrations/file-storage/interfaces/file-storage-exception';
|
||||||
|
|
||||||
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
|
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
|
||||||
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
|
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
|
||||||
@ -17,6 +18,11 @@ import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless
|
|||||||
import { BUILD_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/build-file-name';
|
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';
|
import { BaseServerlessDriver } from 'src/engine/integrations/serverless/drivers/base-serverless.driver';
|
||||||
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
||||||
|
import {
|
||||||
|
ServerlessFunctionException,
|
||||||
|
ServerlessFunctionExceptionCode,
|
||||||
|
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||||
|
import { getServerlessFolder } from 'src/engine/integrations/serverless/utils/serverless-get-folder.utils';
|
||||||
|
|
||||||
export interface LocalDriverOptions {
|
export interface LocalDriverOptions {
|
||||||
fileStorageService: FileStorageService;
|
fileStorageService: FileStorageService;
|
||||||
@ -33,11 +39,7 @@ export class LocalDriver
|
|||||||
this.fileStorageService = options.fileStorageService;
|
this.fileStorageService = options.fileStorageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(serverlessFunction: ServerlessFunctionEntity) {
|
async delete() {}
|
||||||
await this.fileStorageService.delete({
|
|
||||||
folderPath: this.getFolderPath(serverlessFunction),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async build(serverlessFunction: ServerlessFunctionEntity) {
|
async build(serverlessFunction: ServerlessFunctionEntity) {
|
||||||
const javascriptCode = await this.getCompiledCode(
|
const javascriptCode = await this.getCompiledCode(
|
||||||
@ -49,20 +51,48 @@ export class LocalDriver
|
|||||||
file: javascriptCode,
|
file: javascriptCode,
|
||||||
name: BUILD_FILE_NAME,
|
name: BUILD_FILE_NAME,
|
||||||
mimeType: undefined,
|
mimeType: undefined,
|
||||||
folder: this.getFolderPath(serverlessFunction),
|
folder: getServerlessFolder({
|
||||||
|
serverlessFunction,
|
||||||
|
version: 'draft',
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async publish(serverlessFunction: ServerlessFunctionEntity) {
|
||||||
|
await this.build(serverlessFunction);
|
||||||
|
|
||||||
|
return serverlessFunction.latestVersion
|
||||||
|
? `${parseInt(serverlessFunction.latestVersion, 10) + 1}`
|
||||||
|
: '1';
|
||||||
|
}
|
||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
serverlessFunction: ServerlessFunctionEntity,
|
serverlessFunction: ServerlessFunctionEntity,
|
||||||
payload: object | undefined = undefined,
|
payload: object | undefined = undefined,
|
||||||
|
version: string,
|
||||||
): Promise<ServerlessExecuteResult> {
|
): Promise<ServerlessExecuteResult> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const fileStream = await this.fileStorageService.read({
|
let fileContent = '';
|
||||||
folderPath: this.getFolderPath(serverlessFunction),
|
|
||||||
filename: BUILD_FILE_NAME,
|
try {
|
||||||
});
|
const fileStream = await this.fileStorageService.read({
|
||||||
const fileContent = await readFileContent(fileStream);
|
folderPath: getServerlessFolder({
|
||||||
|
serverlessFunction,
|
||||||
|
version,
|
||||||
|
}),
|
||||||
|
filename: BUILD_FILE_NAME,
|
||||||
|
});
|
||||||
|
|
||||||
|
fileContent = await readFileContent(fileStream);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === FileStorageExceptionCode.FILE_NOT_FOUND) {
|
||||||
|
throw new ServerlessFunctionException(
|
||||||
|
`Function Version '${version}' does not exist`,
|
||||||
|
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
const tmpFilePath = join(tmpdir(), `${v4()}.js`);
|
const tmpFilePath = join(tmpdir(), `${v4()}.js`);
|
||||||
|
|
||||||
|
|||||||
@ -16,14 +16,22 @@ export class ServerlessService implements ServerlessDriver {
|
|||||||
return this.driver.delete(serverlessFunction);
|
return this.driver.delete(serverlessFunction);
|
||||||
}
|
}
|
||||||
|
|
||||||
async build(serverlessFunction: ServerlessFunctionEntity): Promise<void> {
|
async build(
|
||||||
return this.driver.build(serverlessFunction);
|
serverlessFunction: ServerlessFunctionEntity,
|
||||||
|
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(
|
||||||
serverlessFunction: ServerlessFunctionEntity,
|
serverlessFunction: ServerlessFunctionEntity,
|
||||||
payload: object | undefined = undefined,
|
payload: object | undefined = undefined,
|
||||||
|
version: string,
|
||||||
): Promise<ServerlessExecuteResult> {
|
): Promise<ServerlessExecuteResult> {
|
||||||
return this.driver.execute(serverlessFunction, payload);
|
return this.driver.execute(serverlessFunction, payload, version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
export const getServerlessFolder = ({
|
||||||
|
serverlessFunction,
|
||||||
|
version,
|
||||||
|
}: {
|
||||||
|
serverlessFunction: ServerlessFunctionEntity;
|
||||||
|
version?: string;
|
||||||
|
}) => {
|
||||||
|
const computedVersion =
|
||||||
|
version === 'latest' ? serverlessFunction.latestVersion : version;
|
||||||
|
|
||||||
|
return join(
|
||||||
|
'workspace-' + serverlessFunction.workspaceId,
|
||||||
|
FileFolder.ServerlessFunction,
|
||||||
|
serverlessFunction.id,
|
||||||
|
computedVersion || '',
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,11 +1,11 @@
|
|||||||
import { ArgsType, Field } from '@nestjs/graphql';
|
import { ArgsType, Field, InputType } from '@nestjs/graphql';
|
||||||
|
|
||||||
import { IsNotEmpty, IsObject, IsOptional, IsUUID } from 'class-validator';
|
import { IsNotEmpty, IsObject, IsOptional, IsUUID } from 'class-validator';
|
||||||
import graphqlTypeJson from 'graphql-type-json';
|
import graphqlTypeJson from 'graphql-type-json';
|
||||||
|
|
||||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||||
|
|
||||||
@ArgsType()
|
@InputType()
|
||||||
export class ExecuteServerlessFunctionInput {
|
export class ExecuteServerlessFunctionInput {
|
||||||
@Field(() => UUIDScalarType, {
|
@Field(() => UUIDScalarType, {
|
||||||
description: 'Id of the serverless function to execute',
|
description: 'Id of the serverless function to execute',
|
||||||
@ -21,4 +21,11 @@ export class ExecuteServerlessFunctionInput {
|
|||||||
@IsObject()
|
@IsObject()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
payload?: JSON;
|
payload?: JSON;
|
||||||
|
|
||||||
|
@Field(() => String, {
|
||||||
|
nullable: false,
|
||||||
|
description: 'Version of the serverless function to execute',
|
||||||
|
defaultValue: 'latest',
|
||||||
|
})
|
||||||
|
version: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { Field, ID, InputType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class GetServerlessFunctionSourceCodeInput {
|
||||||
|
@IDField(() => ID, { description: 'The id of the function.' })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Field(() => String, {
|
||||||
|
nullable: false,
|
||||||
|
description: 'The version of the function',
|
||||||
|
defaultValue: 'draft',
|
||||||
|
})
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { ID, InputType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class PublishServerlessFunctionInput {
|
||||||
|
@IDField(() => ID, { description: 'The id of the function.' })
|
||||||
|
id!: string;
|
||||||
|
}
|
||||||
@ -14,6 +14,7 @@ import {
|
|||||||
IsDateString,
|
IsDateString,
|
||||||
IsEnum,
|
IsEnum,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
|
IsNumber,
|
||||||
IsString,
|
IsString,
|
||||||
IsUUID,
|
IsUUID,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
@ -59,12 +60,11 @@ export class ServerlessFunctionDTO {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@Field()
|
@Field()
|
||||||
sourceCodeFullPath: string;
|
runtime: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@Field({ nullable: true })
|
||||||
@Field()
|
latestVersion: string;
|
||||||
runtime: string;
|
|
||||||
|
|
||||||
@IsEnum(ServerlessFunctionSyncStatus)
|
@IsEnum(ServerlessFunctionSyncStatus)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@ -28,11 +28,11 @@ export class ServerlessFunctionEntity {
|
|||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
@Column({ nullable: false })
|
@Column({ nullable: true })
|
||||||
sourceCodeHash: string;
|
latestVersion: string;
|
||||||
|
|
||||||
@Column({ nullable: false })
|
@Column({ nullable: false })
|
||||||
sourceCodeFullPath: string;
|
sourceCodeHash: string;
|
||||||
|
|
||||||
@Column({ nullable: false, default: ServerlessFunctionRuntime.NODE18 })
|
@Column({ nullable: false, default: ServerlessFunctionRuntime.NODE18 })
|
||||||
runtime: ServerlessFunctionRuntime;
|
runtime: ServerlessFunctionRuntime;
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export class ServerlessFunctionException extends CustomException {
|
|||||||
|
|
||||||
export enum ServerlessFunctionExceptionCode {
|
export enum ServerlessFunctionExceptionCode {
|
||||||
SERVERLESS_FUNCTION_NOT_FOUND = 'SERVERLESS_FUNCTION_NOT_FOUND',
|
SERVERLESS_FUNCTION_NOT_FOUND = 'SERVERLESS_FUNCTION_NOT_FOUND',
|
||||||
|
SERVERLESS_FUNCTION_VERSION_NOT_FOUND = 'SERVERLESS_FUNCTION_VERSION_NOT_FOUND',
|
||||||
FEATURE_FLAG_INVALID = 'FEATURE_FLAG_INVALID',
|
FEATURE_FLAG_INVALID = 'FEATURE_FLAG_INVALID',
|
||||||
SERVERLESS_FUNCTION_ALREADY_EXIST = 'SERVERLESS_FUNCTION_ALREADY_EXIST',
|
SERVERLESS_FUNCTION_ALREADY_EXIST = 'SERVERLESS_FUNCTION_ALREADY_EXIST',
|
||||||
SERVERLESS_FUNCTION_NOT_READY = 'SERVERLESS_FUNCTION_NOT_READY',
|
SERVERLESS_FUNCTION_NOT_READY = 'SERVERLESS_FUNCTION_NOT_READY',
|
||||||
|
|||||||
@ -1,57 +0,0 @@
|
|||||||
import {
|
|
||||||
CallHandler,
|
|
||||||
ExecutionContext,
|
|
||||||
Injectable,
|
|
||||||
NestInterceptor,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
|
|
||||||
import { Observable, map } from 'rxjs';
|
|
||||||
|
|
||||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ServerlessFunctionInterceptor implements NestInterceptor {
|
|
||||||
constructor(private readonly fileService: FileService) {}
|
|
||||||
|
|
||||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
|
||||||
return next.handle().pipe(
|
|
||||||
map(async (data) => {
|
|
||||||
if (data.edges && Array.isArray(data.edges)) {
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
edges: Promise.all(
|
|
||||||
data.edges.map((item) => ({
|
|
||||||
...item,
|
|
||||||
node: this.processItem(item.node),
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return this.processItem(data);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processItem(item: any): Promise<any> {
|
|
||||||
if (item && item.sourceCodeFullPath) {
|
|
||||||
const workspaceId = item.workspace?.id || item.workspaceId;
|
|
||||||
|
|
||||||
if (!workspaceId) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
const signedPayload = await this.fileService.encodeFileToken({
|
|
||||||
serverlessFunctionId: item.id,
|
|
||||||
workspace_id: workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
sourceCodeFullPath: `${item.sourceCodeFullPath}?token=${signedPayload}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -15,7 +15,6 @@ import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
|||||||
import { ServerlessModule } from 'src/engine/integrations/serverless/serverless.module';
|
import { ServerlessModule } from 'src/engine/integrations/serverless/serverless.module';
|
||||||
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
|
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.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 { ServerlessFunctionInterceptor } from 'src/engine/metadata-modules/serverless-function/serverless-function.interceptor';
|
|
||||||
import { ServerlessFunctionResolver } from 'src/engine/metadata-modules/serverless-function/serverless-function.resolver';
|
import { ServerlessFunctionResolver } from 'src/engine/metadata-modules/serverless-function/serverless-function.resolver';
|
||||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||||
|
|
||||||
@ -45,7 +44,6 @@ import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverles
|
|||||||
update: { disabled: true },
|
update: { disabled: true },
|
||||||
delete: { disabled: true },
|
delete: { disabled: true },
|
||||||
guards: [JwtAuthGuard],
|
guards: [JwtAuthGuard],
|
||||||
interceptors: [ServerlessFunctionInterceptor],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { UseGuards, UseInterceptors } from '@nestjs/common';
|
import { UseGuards } from '@nestjs/common';
|
||||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { FileUpload, GraphQLUpload } from 'graphql-upload';
|
import { FileUpload, GraphQLUpload } from 'graphql-upload';
|
||||||
@ -21,9 +21,10 @@ 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 { ServerlessFunctionInterceptor } from 'src/engine/metadata-modules/serverless-function/serverless-function.interceptor';
|
|
||||||
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 { GetServerlessFunctionSourceCodeInput } from 'src/engine/metadata-modules/serverless-function/dtos/get-serverless-function-source-code.input';
|
||||||
|
import { PublishServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/publish-serverless-function.input';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Resolver()
|
@Resolver()
|
||||||
@ -50,6 +51,24 @@ export class ServerlessFunctionResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Query(() => String)
|
||||||
|
async getServerlessFunctionSourceCode(
|
||||||
|
@Args('input') input: GetServerlessFunctionSourceCodeInput,
|
||||||
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await this.checkFeatureFlag(workspaceId);
|
||||||
|
|
||||||
|
return await this.serverlessFunctionService.getServerlessFunctionSourceCode(
|
||||||
|
workspaceId,
|
||||||
|
input.id,
|
||||||
|
input.version,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Mutation(() => ServerlessFunctionDTO)
|
@Mutation(() => ServerlessFunctionDTO)
|
||||||
async deleteOneServerlessFunction(
|
async deleteOneServerlessFunction(
|
||||||
@Args('input') input: DeleteServerlessFunctionInput,
|
@Args('input') input: DeleteServerlessFunctionInput,
|
||||||
@ -67,7 +86,6 @@ export class ServerlessFunctionResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseInterceptors(ServerlessFunctionInterceptor)
|
|
||||||
@Mutation(() => ServerlessFunctionDTO)
|
@Mutation(() => ServerlessFunctionDTO)
|
||||||
async updateOneServerlessFunction(
|
async updateOneServerlessFunction(
|
||||||
@Args('input')
|
@Args('input')
|
||||||
@ -86,7 +104,6 @@ export class ServerlessFunctionResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseInterceptors(ServerlessFunctionInterceptor)
|
|
||||||
@Mutation(() => ServerlessFunctionDTO)
|
@Mutation(() => ServerlessFunctionDTO)
|
||||||
async createOneServerlessFunction(
|
async createOneServerlessFunction(
|
||||||
@Args('input')
|
@Args('input')
|
||||||
@ -109,7 +126,6 @@ export class ServerlessFunctionResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseInterceptors(ServerlessFunctionInterceptor)
|
|
||||||
@Mutation(() => ServerlessFunctionDTO)
|
@Mutation(() => ServerlessFunctionDTO)
|
||||||
async createOneServerlessFunctionFromFile(
|
async createOneServerlessFunctionFromFile(
|
||||||
@Args({ name: 'file', type: () => GraphQLUpload })
|
@Args({ name: 'file', type: () => GraphQLUpload })
|
||||||
@ -133,17 +149,36 @@ export class ServerlessFunctionResolver {
|
|||||||
|
|
||||||
@Mutation(() => ServerlessFunctionExecutionResultDTO)
|
@Mutation(() => ServerlessFunctionExecutionResultDTO)
|
||||||
async executeOneServerlessFunction(
|
async executeOneServerlessFunction(
|
||||||
@Args() executeServerlessFunctionInput: ExecuteServerlessFunctionInput,
|
@Args('input') input: ExecuteServerlessFunctionInput,
|
||||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
await this.checkFeatureFlag(workspaceId);
|
await this.checkFeatureFlag(workspaceId);
|
||||||
const { id, payload } = executeServerlessFunctionInput;
|
const { id, payload, version } = input;
|
||||||
|
|
||||||
return await this.serverlessFunctionService.executeOne(
|
return await this.serverlessFunctionService.executeOneServerlessFunction(
|
||||||
id,
|
id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
payload,
|
payload,
|
||||||
|
version,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => ServerlessFunctionDTO)
|
||||||
|
async publishServerlessFunction(
|
||||||
|
@Args('input') input: PublishServerlessFunctionInput,
|
||||||
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await this.checkFeatureFlag(workspaceId);
|
||||||
|
const { id } = input;
|
||||||
|
|
||||||
|
return await this.serverlessFunctionService.publishOneServerlessFunction(
|
||||||
|
id,
|
||||||
|
workspaceId,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||||
import { FileUpload } from 'graphql-upload';
|
import { FileUpload } from 'graphql-upload';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { v4 } from 'uuid';
|
|
||||||
|
|
||||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
|
||||||
import { ServerlessExecuteResult } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
|
import { ServerlessExecuteResult } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
|
||||||
|
import { FileStorageExceptionCode } from 'src/engine/integrations/file-storage/interfaces/file-storage-exception';
|
||||||
|
|
||||||
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
|
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
|
||||||
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
|
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
|
||||||
@ -26,6 +23,8 @@ import {
|
|||||||
ServerlessFunctionExceptionCode,
|
ServerlessFunctionExceptionCode,
|
||||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||||
import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils';
|
import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils';
|
||||||
|
import { isDefined } from 'src/utils/is-defined';
|
||||||
|
import { getServerlessFolder } from 'src/engine/integrations/serverless/utils/serverless-get-folder.utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFunctionEntity> {
|
export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFunctionEntity> {
|
||||||
@ -38,10 +37,54 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
|||||||
super(serverlessFunctionRepository);
|
super(serverlessFunctionRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeOne(
|
async getServerlessFunctionSourceCode(
|
||||||
|
workspaceId: string,
|
||||||
|
id: string,
|
||||||
|
version: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const serverlessFunction =
|
||||||
|
await this.serverlessFunctionRepository.findOne({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!serverlessFunction) {
|
||||||
|
throw new ServerlessFunctionException(
|
||||||
|
`Function does not exist`,
|
||||||
|
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderPath = getServerlessFolder({
|
||||||
|
serverlessFunction,
|
||||||
|
version,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileStream = await this.fileStorageService.read({
|
||||||
|
folderPath,
|
||||||
|
filename: SOURCE_FILE_NAME,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await readFileContent(fileStream);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === FileStorageExceptionCode.FILE_NOT_FOUND) {
|
||||||
|
throw new ServerlessFunctionException(
|
||||||
|
`Function Version '${version}' does not exist`,
|
||||||
|
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeOneServerlessFunction(
|
||||||
id: string,
|
id: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
payload: object | undefined = undefined,
|
payload: object | undefined = undefined,
|
||||||
|
version = 'latest',
|
||||||
): Promise<ServerlessExecuteResult> {
|
): Promise<ServerlessExecuteResult> {
|
||||||
const functionToExecute = await this.serverlessFunctionRepository.findOne({
|
const functionToExecute = await this.serverlessFunctionRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
@ -60,13 +103,73 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
|||||||
if (
|
if (
|
||||||
functionToExecute.syncStatus === ServerlessFunctionSyncStatus.NOT_READY
|
functionToExecute.syncStatus === ServerlessFunctionSyncStatus.NOT_READY
|
||||||
) {
|
) {
|
||||||
|
await this.serverlessService.build(functionToExecute, version);
|
||||||
|
await super.updateOne(functionToExecute.id, {
|
||||||
|
syncStatus: ServerlessFunctionSyncStatus.READY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.serverlessService.execute(functionToExecute, payload, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
async publishOneServerlessFunction(id: string, workspaceId: string) {
|
||||||
|
const existingServerlessFunction =
|
||||||
|
await this.serverlessFunctionRepository.findOne({
|
||||||
|
where: { id, workspaceId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingServerlessFunction) {
|
||||||
throw new ServerlessFunctionException(
|
throw new ServerlessFunctionException(
|
||||||
`Function is not ready to be executed`,
|
`Function does not exist`,
|
||||||
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.serverlessService.execute(functionToExecute, payload);
|
if (isDefined(existingServerlessFunction.latestVersion)) {
|
||||||
|
const latestCode = await this.getServerlessFunctionSourceCode(
|
||||||
|
workspaceId,
|
||||||
|
id,
|
||||||
|
'latest',
|
||||||
|
);
|
||||||
|
const draftCode = await this.getServerlessFunctionSourceCode(
|
||||||
|
workspaceId,
|
||||||
|
id,
|
||||||
|
'draft',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
serverlessFunctionCreateHash(latestCode) ===
|
||||||
|
serverlessFunctionCreateHash(draftCode)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'Cannot publish a new version when code has not changed',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newVersion = await this.serverlessService.publish(
|
||||||
|
existingServerlessFunction,
|
||||||
|
);
|
||||||
|
|
||||||
|
const draftFolderPath = getServerlessFolder({
|
||||||
|
serverlessFunction: existingServerlessFunction,
|
||||||
|
version: 'draft',
|
||||||
|
});
|
||||||
|
const newFolderPath = getServerlessFolder({
|
||||||
|
serverlessFunction: existingServerlessFunction,
|
||||||
|
version: newVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.fileStorageService.copy({
|
||||||
|
from: { folderPath: draftFolderPath },
|
||||||
|
to: { folderPath: newFolderPath },
|
||||||
|
});
|
||||||
|
|
||||||
|
await super.updateOne(existingServerlessFunction.id, {
|
||||||
|
latestVersion: newVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.findById(existingServerlessFunction.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteOneServerlessFunction(id: string, workspaceId: string) {
|
async deleteOneServerlessFunction(id: string, workspaceId: string) {
|
||||||
@ -86,6 +189,12 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
|||||||
|
|
||||||
await this.serverlessService.delete(existingServerlessFunction);
|
await this.serverlessService.delete(existingServerlessFunction);
|
||||||
|
|
||||||
|
await this.fileStorageService.delete({
|
||||||
|
folderPath: getServerlessFolder({
|
||||||
|
serverlessFunction: existingServerlessFunction,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
return existingServerlessFunction;
|
return existingServerlessFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,34 +214,23 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const codeHasChanged =
|
|
||||||
serverlessFunctionCreateHash(serverlessFunctionInput.code) !==
|
|
||||||
existingServerlessFunction.sourceCodeHash;
|
|
||||||
|
|
||||||
await super.updateOne(existingServerlessFunction.id, {
|
await super.updateOne(existingServerlessFunction.id, {
|
||||||
name: serverlessFunctionInput.name,
|
name: serverlessFunctionInput.name,
|
||||||
description: serverlessFunctionInput.description,
|
description: serverlessFunctionInput.description,
|
||||||
sourceCodeHash: serverlessFunctionCreateHash(
|
syncStatus: ServerlessFunctionSyncStatus.NOT_READY,
|
||||||
serverlessFunctionInput.code,
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (codeHasChanged) {
|
const fileFolder = getServerlessFolder({
|
||||||
const fileFolder = join(
|
serverlessFunction: existingServerlessFunction,
|
||||||
'workspace-' + workspaceId,
|
version: 'draft',
|
||||||
FileFolder.ServerlessFunction,
|
});
|
||||||
existingServerlessFunction.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.fileStorageService.write({
|
await this.fileStorageService.write({
|
||||||
file: serverlessFunctionInput.code,
|
file: serverlessFunctionInput.code,
|
||||||
name: SOURCE_FILE_NAME,
|
name: SOURCE_FILE_NAME,
|
||||||
mimeType: undefined,
|
mimeType: undefined,
|
||||||
folder: fileFolder,
|
folder: fileFolder,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.serverlessService.build(existingServerlessFunction);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.findById(existingServerlessFunction.id);
|
return await this.findById(existingServerlessFunction.id);
|
||||||
}
|
}
|
||||||
@ -162,41 +260,24 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
|||||||
typescriptCode = await readFileContent(code.createReadStream());
|
typescriptCode = await readFileContent(code.createReadStream());
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverlessFunctionId = v4();
|
const createdServerlessFunction = await super.createOne({
|
||||||
|
|
||||||
const fileFolderWithoutWorkspace = join(
|
|
||||||
FileFolder.ServerlessFunction,
|
|
||||||
serverlessFunctionId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const fileFolder = join(
|
|
||||||
'workspace-' + workspaceId,
|
|
||||||
fileFolderWithoutWorkspace,
|
|
||||||
);
|
|
||||||
|
|
||||||
const sourceCodeFullPath =
|
|
||||||
fileFolderWithoutWorkspace + '/' + SOURCE_FILE_NAME;
|
|
||||||
|
|
||||||
const serverlessFunction = await super.createOne({
|
|
||||||
...serverlessFunctionInput,
|
...serverlessFunctionInput,
|
||||||
id: serverlessFunctionId,
|
|
||||||
workspaceId,
|
workspaceId,
|
||||||
sourceCodeHash: serverlessFunctionCreateHash(typescriptCode),
|
sourceCodeHash: serverlessFunctionCreateHash(typescriptCode),
|
||||||
sourceCodeFullPath,
|
});
|
||||||
|
|
||||||
|
const draftFileFolder = getServerlessFolder({
|
||||||
|
serverlessFunction: createdServerlessFunction,
|
||||||
|
version: 'draft',
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.fileStorageService.write({
|
await this.fileStorageService.write({
|
||||||
file: typescriptCode,
|
file: typescriptCode,
|
||||||
name: SOURCE_FILE_NAME,
|
name: SOURCE_FILE_NAME,
|
||||||
mimeType: undefined,
|
mimeType: undefined,
|
||||||
folder: fileFolder,
|
folder: draftFileFolder,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.serverlessService.build(serverlessFunction);
|
return await this.findById(createdServerlessFunction.id);
|
||||||
await super.updateOne(serverlessFunctionId, {
|
|
||||||
syncStatus: ServerlessFunctionSyncStatus.READY,
|
|
||||||
});
|
|
||||||
|
|
||||||
return await this.findById(serverlessFunctionId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export const serverlessFunctionGraphQLApiExceptionHandler = (error: any) => {
|
|||||||
if (error instanceof ServerlessFunctionException) {
|
if (error instanceof ServerlessFunctionException) {
|
||||||
switch (error.code) {
|
switch (error.code) {
|
||||||
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND:
|
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND:
|
||||||
|
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_VERSION_NOT_FOUND:
|
||||||
throw new NotFoundError(error.message);
|
throw new NotFoundError(error.message);
|
||||||
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_ALREADY_EXIST:
|
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_ALREADY_EXIST:
|
||||||
throw new ConflictError(error.message);
|
throw new ConflictError(error.message);
|
||||||
|
|||||||
@ -33,11 +33,12 @@ export class CodeActionExecutor implements WorkflowStepExecutor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.serverlessFunctionService.executeOne(
|
const result =
|
||||||
step.settings.serverlessFunctionId,
|
await this.serverlessFunctionService.executeOneServerlessFunction(
|
||||||
workspaceId,
|
step.settings.serverlessFunctionId,
|
||||||
payload,
|
workspaceId,
|
||||||
);
|
payload,
|
||||||
|
);
|
||||||
|
|
||||||
return { data: result.data, ...(result.error && { error: result.error }) };
|
return { data: result.data, ...(result.error && { error: result.error }) };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,6 +90,7 @@ export {
|
|||||||
IconFocusCentered,
|
IconFocusCentered,
|
||||||
IconForbid,
|
IconForbid,
|
||||||
IconFunction,
|
IconFunction,
|
||||||
|
IconGitCommit,
|
||||||
IconGripVertical,
|
IconGripVertical,
|
||||||
IconH1,
|
IconH1,
|
||||||
IconH2,
|
IconH2,
|
||||||
|
|||||||
Reference in New Issue
Block a user