mirror of
https://github.com/lingble/twenty.git
synced 2025-10-29 03:42:30 +00:00
6653 serverless functions store and use environment variables in serverless function scripts (#7390)
 
This commit is contained in:
@@ -25,15 +25,15 @@ const documents = {
|
||||
"\n \n query GetManyRemoteTables($input: FindManyRemoteTablesInput!) {\n findDistantTablesWithStatus(input: $input) {\n ...RemoteTableFields\n }\n }\n": types.GetManyRemoteTablesDocument,
|
||||
"\n \n query GetOneDatabaseConnection($input: RemoteServerIdInput!) {\n findOneRemoteServerById(input: $input) {\n ...RemoteServerFields\n }\n }\n": types.GetOneDatabaseConnectionDocument,
|
||||
"\n mutation CreateOneObjectMetadataItem($input: CreateOneObjectInput!) {\n createOneObject(input: $input) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n }\n }\n": types.CreateOneObjectMetadataItemDocument,
|
||||
"\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n defaultValue\n options\n }\n }\n": types.CreateOneFieldMetadataItemDocument,
|
||||
"\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n defaultValue\n options\n }\n }\n": types.CreateOneFieldMetadataItemDocument,
|
||||
"\n mutation CreateOneRelationMetadata($input: CreateOneRelationInput!) {\n createOneRelation(input: $input) {\n id\n relationType\n fromObjectMetadataId\n toObjectMetadataId\n fromFieldMetadataId\n toFieldMetadataId\n createdAt\n updatedAt\n }\n }\n": types.CreateOneRelationMetadataDocument,
|
||||
"\n mutation UpdateOneFieldMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateFieldInput!\n ) {\n updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {\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.UpdateOneFieldMetadataItemDocument,
|
||||
"\n mutation UpdateOneFieldMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateFieldInput!\n ) {\n updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n": types.UpdateOneFieldMetadataItemDocument,
|
||||
"\n mutation UpdateOneObjectMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateObjectPayload!\n ) {\n updateOneObject(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n }\n }\n": types.UpdateOneObjectMetadataItemDocument,
|
||||
"\n mutation DeleteOneObjectMetadataItem($idToDelete: UUID!) {\n deleteOneObject(input: { id: $idToDelete }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n }\n }\n": types.DeleteOneObjectMetadataItemDocument,
|
||||
"\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 settings\n }\n }\n": types.DeleteOneFieldMetadataItemDocument,
|
||||
"\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 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 runtime\n syncStatus\n latestVersion\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
||||
"\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 defaultValue\n options\n settings\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 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 DeleteOneServerlessFunction($input: DeleteServerlessFunctionInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument,
|
||||
"\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument,
|
||||
@@ -110,7 +110,7 @@ export function graphql(source: "\n mutation CreateOneObjectMetadataItem($input
|
||||
/**
|
||||
* 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 CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n defaultValue\n options\n }\n }\n"): (typeof documents)["\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n defaultValue\n options\n }\n }\n"];
|
||||
export function graphql(source: "\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n defaultValue\n options\n }\n }\n"): (typeof documents)["\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n defaultValue\n options\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -118,7 +118,7 @@ export function graphql(source: "\n mutation CreateOneRelationMetadata($input:
|
||||
/**
|
||||
* 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 UpdateOneFieldMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateFieldInput!\n ) {\n updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {\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"): (typeof documents)["\n mutation UpdateOneFieldMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateFieldInput!\n ) {\n updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {\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"];
|
||||
export function graphql(source: "\n mutation UpdateOneFieldMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateFieldInput!\n ) {\n updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n"): (typeof documents)["\n mutation UpdateOneFieldMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateFieldInput!\n ) {\n updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -130,7 +130,7 @@ export function graphql(source: "\n mutation DeleteOneObjectMetadataItem($idToD
|
||||
/**
|
||||
* 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 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"): (typeof 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"];
|
||||
export function graphql(source: "\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 settings\n }\n }\n"): (typeof 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 settings\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -138,11 +138,11 @@ export function graphql(source: "\n mutation DeleteOneRelationMetadataItem($idT
|
||||
/**
|
||||
* 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 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 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"): (typeof documents)["\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 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"];
|
||||
export function graphql(source: "\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 defaultValue\n options\n settings\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"): (typeof documents)["\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 defaultValue\n options\n settings\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"];
|
||||
/**
|
||||
* 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 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"];
|
||||
export function graphql(source: "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\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 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.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,9 +1,8 @@
|
||||
import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope';
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { CodeEditor } from '@/ui/input/code-editor/components/CodeEditor';
|
||||
import { CodeEditor, File } from '@/ui/input/code-editor/components/CodeEditor';
|
||||
import { CoreEditorHeader } from '@/ui/input/code-editor/components/CodeEditorHeader';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||
@@ -13,13 +12,16 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { H2Title, IconGitCommit, IconPlayerPlay, IconRestore } from 'twenty-ui';
|
||||
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
|
||||
import { SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/settings/serverless-functions/constants/SettingsServerlessFunctionTabListComponentId';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
const StyledTabList = styled(TabList)`
|
||||
border-bottom: none;
|
||||
`;
|
||||
|
||||
export const SettingsServerlessFunctionCodeEditorTab = ({
|
||||
formValues,
|
||||
files,
|
||||
handleExecute,
|
||||
handlePublish,
|
||||
handleReset,
|
||||
@@ -28,15 +30,19 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
|
||||
onChange,
|
||||
setIsCodeValid,
|
||||
}: {
|
||||
formValues: ServerlessFunctionFormValues;
|
||||
files: File[];
|
||||
handleExecute: () => void;
|
||||
handlePublish: () => void;
|
||||
handleReset: () => void;
|
||||
resetDisabled: boolean;
|
||||
publishDisabled: boolean;
|
||||
onChange: (key: string) => (value: string) => void;
|
||||
onChange: (filePath: string, value: string) => void;
|
||||
setIsCodeValid: (isCodeValid: boolean) => void;
|
||||
}) => {
|
||||
const { activeTabIdState } = useTabList(
|
||||
SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID,
|
||||
);
|
||||
const activeTabId = useRecoilValue(activeTabIdState);
|
||||
const TestButton = (
|
||||
<Button
|
||||
title="Test"
|
||||
@@ -68,21 +74,15 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
|
||||
/>
|
||||
);
|
||||
|
||||
const TAB_LIST_COMPONENT_ID = 'serverless-function-editor';
|
||||
|
||||
const HeaderTabList = (
|
||||
<StyledTabList
|
||||
tabListId={TAB_LIST_COMPONENT_ID}
|
||||
tabs={[{ id: 'index.ts', title: 'index.ts' }]}
|
||||
tabListId={SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID}
|
||||
tabs={files.map((file) => {
|
||||
return { id: file.path, title: file.path.split('/').at(-1) || '' };
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
const Header = (
|
||||
<CoreEditorHeader
|
||||
leftNodes={[HeaderTabList]}
|
||||
rightNodes={[ResetButton, PublishButton, TestButton]}
|
||||
/>
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
useHotkeyScopeOnMount(
|
||||
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionEditorTab,
|
||||
@@ -95,18 +95,25 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
|
||||
},
|
||||
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionEditorTab,
|
||||
);
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Code your function"
|
||||
description="Write your function (in typescript) below"
|
||||
/>
|
||||
<CodeEditor
|
||||
value={formValues.code}
|
||||
onChange={onChange('code')}
|
||||
setIsCodeValid={setIsCodeValid}
|
||||
header={Header}
|
||||
<CoreEditorHeader
|
||||
leftNodes={[HeaderTabList]}
|
||||
rightNodes={[ResetButton, PublishButton, TestButton]}
|
||||
/>
|
||||
{activeTabId && (
|
||||
<CodeEditor
|
||||
files={files}
|
||||
currentFilePath={activeTabId}
|
||||
onChange={(newCodeValue) => onChange(activeTabId, newCodeValue)}
|
||||
setIsCodeValid={setIsCodeValid}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -44,28 +44,6 @@ export const SettingsServerlessFunctionTestTab = ({
|
||||
settingsServerlessFunctionOutput.error ||
|
||||
'';
|
||||
|
||||
const InputHeader = (
|
||||
<CoreEditorHeader
|
||||
title={'Input'}
|
||||
rightNodes={[
|
||||
<Button
|
||||
title="Run Function"
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
size="small"
|
||||
Icon={IconPlayerPlay}
|
||||
onClick={handleExecute}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const OutputHeader = (
|
||||
<CoreEditorHeader
|
||||
leftNodes={[<SettingsServerlessFunctionsOutputMetadataInfo />]}
|
||||
rightNodes={[<LightCopyIconButton copyText={result} />]}
|
||||
/>
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
useHotkeyScopeOnMount(
|
||||
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionTestTab,
|
||||
@@ -86,20 +64,52 @@ export const SettingsServerlessFunctionTestTab = ({
|
||||
description='Insert a JSON input, then press "Run" to test your function.'
|
||||
/>
|
||||
<StyledInputsContainer>
|
||||
<CodeEditor
|
||||
value={settingsServerlessFunctionInput}
|
||||
height={200}
|
||||
onChange={setSettingsServerlessFunctionInput}
|
||||
language={'json'}
|
||||
header={InputHeader}
|
||||
/>
|
||||
<CodeEditor
|
||||
value={result}
|
||||
height={settingsServerlessFunctionCodeEditorOutputParams.height}
|
||||
language={settingsServerlessFunctionCodeEditorOutputParams.language}
|
||||
options={{ readOnly: true, domReadOnly: true }}
|
||||
header={OutputHeader}
|
||||
/>
|
||||
<div>
|
||||
<CoreEditorHeader
|
||||
title={'Input'}
|
||||
rightNodes={[
|
||||
<Button
|
||||
title="Run Function"
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
size="small"
|
||||
Icon={IconPlayerPlay}
|
||||
onClick={handleExecute}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
<CodeEditor
|
||||
files={[
|
||||
{
|
||||
content: settingsServerlessFunctionInput,
|
||||
language: 'json',
|
||||
path: 'input.json',
|
||||
},
|
||||
]}
|
||||
currentFilePath={'input.json'}
|
||||
height={200}
|
||||
onChange={setSettingsServerlessFunctionInput}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<CoreEditorHeader
|
||||
leftNodes={[<SettingsServerlessFunctionsOutputMetadataInfo />]}
|
||||
rightNodes={[<LightCopyIconButton copyText={result} />]}
|
||||
/>
|
||||
<CodeEditor
|
||||
files={[
|
||||
{
|
||||
content: result,
|
||||
language:
|
||||
settingsServerlessFunctionCodeEditorOutputParams.language,
|
||||
path: 'result.any',
|
||||
},
|
||||
]}
|
||||
currentFilePath={'result.any'}
|
||||
height={settingsServerlessFunctionCodeEditorOutputParams.height}
|
||||
options={{ readOnly: true, domReadOnly: true }}
|
||||
/>
|
||||
</div>
|
||||
</StyledInputsContainer>
|
||||
</Section>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID =
|
||||
'settings-serverless-function-editor-tab-list';
|
||||
@@ -5,7 +5,6 @@ export const SERVERLESS_FUNCTION_FRAGMENT = gql`
|
||||
id
|
||||
name
|
||||
description
|
||||
sourceCodeHash
|
||||
runtime
|
||||
syncStatus
|
||||
latestVersion
|
||||
|
||||
@@ -9,7 +9,7 @@ export type ServerlessFunctionNewFormValues = {
|
||||
};
|
||||
|
||||
export type ServerlessFunctionFormValues = ServerlessFunctionNewFormValues & {
|
||||
code: string;
|
||||
code: { [filePath: string]: string } | undefined;
|
||||
};
|
||||
|
||||
type SetServerlessFunctionFormValues = Dispatch<
|
||||
@@ -26,7 +26,7 @@ export const useServerlessFunctionUpdateFormState = (
|
||||
const [formValues, setFormValues] = useState<ServerlessFunctionFormValues>({
|
||||
name: '',
|
||||
description: '',
|
||||
code: '',
|
||||
code: undefined,
|
||||
});
|
||||
|
||||
const { serverlessFunction } =
|
||||
@@ -37,7 +37,7 @@ export const useServerlessFunctionUpdateFormState = (
|
||||
version: 'draft',
|
||||
onCompleted: (data: FindOneServerlessFunctionSourceCodeQuery) => {
|
||||
const newState = {
|
||||
code: data?.getServerlessFunctionSourceCode || '',
|
||||
code: data?.getServerlessFunctionSourceCode || undefined,
|
||||
name: serverlessFunction?.name || '',
|
||||
description: serverlessFunction?.description || '',
|
||||
};
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import Editor, { Monaco, EditorProps } from '@monaco-editor/react';
|
||||
import dotenv from 'dotenv';
|
||||
import { AutoTypings } from 'monaco-editor-auto-typings';
|
||||
import { editor, MarkerSeverity } from 'monaco-editor';
|
||||
import { codeEditorTheme } from '@/ui/input/code-editor/theme/CodeEditorTheme';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useEffect } from 'react';
|
||||
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const DEFAULT_CODE = `export const handler = async (
|
||||
event: object,
|
||||
context: object
|
||||
): Promise<object> => {
|
||||
// Your code here
|
||||
return {};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledEditor = styled(Editor)`
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-top: none;
|
||||
@@ -24,25 +15,34 @@ const StyledEditor = styled(Editor)`
|
||||
${({ theme }) => theme.border.radius.sm};
|
||||
`;
|
||||
|
||||
export type File = {
|
||||
language: string;
|
||||
content: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
type CodeEditorProps = Omit<EditorProps, 'onChange'> & {
|
||||
header: React.ReactNode;
|
||||
currentFilePath: string;
|
||||
files: File[];
|
||||
onChange?: (value: string) => void;
|
||||
setIsCodeValid?: (isCodeValid: boolean) => void;
|
||||
};
|
||||
|
||||
export const CodeEditor = ({
|
||||
value = DEFAULT_CODE,
|
||||
currentFilePath,
|
||||
files,
|
||||
onChange,
|
||||
setIsCodeValid,
|
||||
language = 'typescript',
|
||||
height = 450,
|
||||
options = undefined,
|
||||
header,
|
||||
}: CodeEditorProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { availablePackages } = useGetAvailablePackages();
|
||||
|
||||
const currentFile = files.find((file) => file.path === currentFilePath);
|
||||
const environmentVariablesFile = files.find((file) => file.path === '.env');
|
||||
|
||||
const handleEditorDidMount = async (
|
||||
editor: editor.IStandaloneCodeEditor,
|
||||
monaco: Monaco,
|
||||
@@ -50,7 +50,57 @@ export const CodeEditor = ({
|
||||
monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme));
|
||||
monaco.editor.setTheme('codeEditorTheme');
|
||||
|
||||
if (language === 'typescript') {
|
||||
if (files.length > 1) {
|
||||
files.forEach((file) => {
|
||||
const model = monaco.editor.getModel(monaco.Uri.file(file.path));
|
||||
if (!isDefined(model)) {
|
||||
monaco.editor.createModel(
|
||||
file.content,
|
||||
file.language,
|
||||
monaco.Uri.file(file.path),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||
...monaco.languages.typescript.typescriptDefaults.getCompilerOptions(),
|
||||
moduleResolution:
|
||||
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
baseUrl: 'file:///src',
|
||||
paths: {
|
||||
'src/*': ['file:///src/*'],
|
||||
},
|
||||
allowSyntheticDefaultImports: true,
|
||||
esModuleInterop: true,
|
||||
noEmit: true,
|
||||
target: monaco.languages.typescript.ScriptTarget.ESNext,
|
||||
});
|
||||
|
||||
if (isDefined(environmentVariablesFile)) {
|
||||
const environmentVariables = dotenv.parse(
|
||||
environmentVariablesFile.content,
|
||||
);
|
||||
|
||||
const environmentDefinition = `
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
${Object.keys(environmentVariables)
|
||||
.map((key) => `${key}: string;`)
|
||||
.join('\n')}
|
||||
}
|
||||
}
|
||||
|
||||
declare const process: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
};
|
||||
`;
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
environmentDefinition,
|
||||
'ts:process-env.d.ts',
|
||||
);
|
||||
}
|
||||
|
||||
await AutoTypings.create(editor, {
|
||||
monaco,
|
||||
preloadPackages: true,
|
||||
@@ -71,43 +121,28 @@ export const CodeEditor = ({
|
||||
setIsCodeValid?.(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = `
|
||||
.monaco-editor .margin .line-numbers {
|
||||
font-weight: bold;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
return () => {
|
||||
document.head.removeChild(style);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
isDefined(currentFile) &&
|
||||
isDefined(availablePackages) && (
|
||||
<>
|
||||
{header}
|
||||
<StyledEditor
|
||||
height={height}
|
||||
language={language}
|
||||
value={value}
|
||||
onMount={handleEditorDidMount}
|
||||
onChange={(value?: string) => value && onChange?.(value)}
|
||||
onValidate={handleEditorValidation}
|
||||
options={{
|
||||
...options,
|
||||
overviewRulerLanes: 0,
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
},
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<StyledEditor
|
||||
height={height}
|
||||
value={currentFile.content}
|
||||
language={currentFile.language}
|
||||
onMount={handleEditorDidMount}
|
||||
onChange={(value?: string) => value && onChange?.(value)}
|
||||
onValidate={handleEditorValidation}
|
||||
options={{
|
||||
...options,
|
||||
overviewRulerLanes: 0,
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
},
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { IconCode, IconFunction, IconSettings, IconTestPipe } from 'twenty-ui';
|
||||
import { usePreventOverlapCallback } from '~/hooks/usePreventOverlapCallback';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
const TAB_LIST_COMPONENT_ID = 'serverless-function-detail';
|
||||
|
||||
@@ -81,14 +82,24 @@ export const SettingsServerlessFunctionDetail = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const onCodeChange = async (filePath: string, value: string) => {
|
||||
setFormValues((prevState) => ({
|
||||
...prevState,
|
||||
code: { ...prevState.code, [filePath]: value },
|
||||
}));
|
||||
await handleSave();
|
||||
};
|
||||
|
||||
const resetDisabled =
|
||||
!isDefined(latestVersionCode) || latestVersionCode === formValues.code;
|
||||
const publishDisabled = !isCodeValid || latestVersionCode === formValues.code;
|
||||
!isDefined(latestVersionCode) ||
|
||||
isDeeplyEqual(latestVersionCode, formValues.code);
|
||||
const publishDisabled =
|
||||
!isCodeValid || isDeeplyEqual(latestVersionCode, formValues.code);
|
||||
|
||||
const handleReset = async () => {
|
||||
try {
|
||||
const newState = {
|
||||
code: latestVersionCode || '',
|
||||
code: latestVersionCode || {},
|
||||
};
|
||||
setFormValues((prevState) => ({
|
||||
...prevState,
|
||||
@@ -166,18 +177,30 @@ export const SettingsServerlessFunctionDetail = () => {
|
||||
{ id: 'settings', title: 'Settings', Icon: IconSettings },
|
||||
];
|
||||
|
||||
const files = formValues.code
|
||||
? Object.keys(formValues.code)
|
||||
.map((key) => {
|
||||
return {
|
||||
path: key,
|
||||
language: key === '.env' ? 'ini' : 'typescript',
|
||||
content: formValues.code?.[key] || '',
|
||||
};
|
||||
})
|
||||
.reverse()
|
||||
: [];
|
||||
|
||||
const renderActiveTabContent = () => {
|
||||
switch (activeTabId) {
|
||||
case 'editor':
|
||||
return (
|
||||
<SettingsServerlessFunctionCodeEditorTab
|
||||
formValues={formValues}
|
||||
files={files}
|
||||
handleExecute={handleExecute}
|
||||
handlePublish={handlePublish}
|
||||
handleReset={handleReset}
|
||||
resetDisabled={resetDisabled}
|
||||
publishDisabled={publishDisabled}
|
||||
onChange={onChange}
|
||||
onChange={onCodeChange}
|
||||
setIsCodeValid={setIsCodeValid}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,6 @@ import { ServerlessFunctionNewFormValues } from '@/settings/serverless-functions
|
||||
import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope';
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { DEFAULT_CODE } from '@/ui/input/code-editor/components/CodeEditor';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
@@ -31,7 +30,6 @@ export const SettingsServerlessFunctionsNew = () => {
|
||||
const newServerlessFunction = await createOneServerlessFunction({
|
||||
name: formValues.name,
|
||||
description: formValues.description,
|
||||
code: DEFAULT_CODE,
|
||||
});
|
||||
|
||||
if (!isDefined(newServerlessFunction?.data)) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { DEFAULT_CODE } from '@/ui/input/code-editor/components/CodeEditor';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { within } from '@storybook/test';
|
||||
import { graphql, http, HttpResponse } from 'msw';
|
||||
@@ -38,7 +37,6 @@ const meta: Meta<PageDecoratorArgs> = {
|
||||
description: '',
|
||||
syncStatus: 'READY',
|
||||
runtime: 'nodejs18.x',
|
||||
sourceCodeHash: '42d2734b3dc8a7b45a16803ed7f417bc',
|
||||
updatedAt: '2024-02-24T10:23:10.673Z',
|
||||
createdAt: '2024-02-24T10:23:10.673Z',
|
||||
},
|
||||
@@ -46,7 +44,7 @@ const meta: Meta<PageDecoratorArgs> = {
|
||||
});
|
||||
}),
|
||||
http.get(getImageAbsoluteURI(SOURCE_CODE_FULL_PATH) || '', () => {
|
||||
return HttpResponse.text(DEFAULT_CODE);
|
||||
return HttpResponse.text('export const handler = () => {}');
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -6,17 +6,21 @@
|
||||
"builder": "swc",
|
||||
"typeCheck": true,
|
||||
"assets": [
|
||||
{
|
||||
"include": "**/serverless/drivers/constants/base-typescript-project/**",
|
||||
"outDir": "dist/assets"
|
||||
},
|
||||
{
|
||||
"include": "**/serverless/drivers/layers/*/package.json",
|
||||
"outDir": "dist/src"
|
||||
"outDir": "dist/assets"
|
||||
},
|
||||
{
|
||||
"include": "**/serverless/drivers/layers/*/yarn.lock",
|
||||
"outDir": "dist/src"
|
||||
"outDir": "dist/assets"
|
||||
},
|
||||
{
|
||||
"include": "**/serverless/drivers/layers/engine/**",
|
||||
"outDir": "dist/src"
|
||||
"outDir": "dist/assets"
|
||||
}
|
||||
],
|
||||
"watchAssets": true
|
||||
|
||||
3
packages/twenty-server/src/constants/assets-path.ts
Normal file
3
packages/twenty-server/src/constants/assets-path.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import path from 'path';
|
||||
|
||||
export const ASSET_PATH = path.resolve(__dirname, `../../assets`);
|
||||
@@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class RemoveServerlessSourceCodeHashColumn1726240847733
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'RemoveServerlessSourceCodeHashColumn1726240847733';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."serverlessFunction" DROP COLUMN "sourceCodeHash"`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."serverlessFunction" ADD "sourceCodeHash" character varying NOT NULL`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,4 +17,8 @@ export interface StorageDriver {
|
||||
from: { folderPath: string; filename?: string };
|
||||
to: { folderPath: string; filename?: string };
|
||||
}): Promise<void>;
|
||||
download(params: {
|
||||
from: { folderPath: string; filename?: string };
|
||||
to: { folderPath: string; filename?: string };
|
||||
}): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -21,10 +21,6 @@ export class LocalDriver implements StorageDriver {
|
||||
}
|
||||
|
||||
async createFolder(path: string) {
|
||||
if (existsSync(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return fs.mkdir(path, { recursive: true });
|
||||
}
|
||||
|
||||
@@ -122,21 +118,24 @@ export class LocalDriver implements StorageDriver {
|
||||
}
|
||||
}
|
||||
|
||||
async copy(params: {
|
||||
from: { folderPath: string; filename?: string };
|
||||
to: { folderPath: string; filename?: string };
|
||||
}): Promise<void> {
|
||||
async copy(
|
||||
params: {
|
||||
from: { folderPath: string; filename?: string };
|
||||
to: { folderPath: string; filename?: string };
|
||||
},
|
||||
toInMemory = false,
|
||||
): Promise<void> {
|
||||
if (!params.from.filename && params.to.filename) {
|
||||
throw new Error('Cannot copy folder to file');
|
||||
}
|
||||
const fromPath = join(
|
||||
`${this.options.storagePath}/`,
|
||||
this.options.storagePath,
|
||||
params.from.folderPath,
|
||||
params.from.filename || '',
|
||||
);
|
||||
|
||||
const toPath = join(
|
||||
`${this.options.storagePath}/`,
|
||||
toInMemory ? '' : this.options.storagePath,
|
||||
params.to.folderPath,
|
||||
params.to.filename || '',
|
||||
);
|
||||
@@ -156,4 +155,11 @@ export class LocalDriver implements StorageDriver {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async download(params: {
|
||||
from: { folderPath: string; filename?: string };
|
||||
to: { folderPath: string; filename?: string };
|
||||
}): Promise<void> {
|
||||
await this.copy(params, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Readable } from 'stream';
|
||||
import fs from 'fs';
|
||||
import { mkdir } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { pipeline } from 'stream/promises';
|
||||
|
||||
import {
|
||||
CopyObjectCommand,
|
||||
@@ -188,6 +192,23 @@ export class S3Driver implements StorageDriver {
|
||||
}
|
||||
}
|
||||
|
||||
extractFolderAndFilePaths(objectKey: string | undefined) {
|
||||
if (!isDefined(objectKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = /(?<folder>.*)\/(?<file>.*)/.exec(objectKey);
|
||||
|
||||
if (!isDefined(result) || !isDefined(result.groups)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fromFolderPath = result.groups.folder;
|
||||
const filename = result.groups.file;
|
||||
|
||||
return { fromFolderPath, filename };
|
||||
}
|
||||
|
||||
async copy(params: {
|
||||
from: { folderPath: string; filename?: string };
|
||||
to: { folderPath: string; filename?: string };
|
||||
@@ -239,17 +260,18 @@ export class S3Driver implements StorageDriver {
|
||||
);
|
||||
|
||||
if (!listedObjects.Contents || listedObjects.Contents.length === 0) {
|
||||
throw new Error('No objects found in the source folder.');
|
||||
throw new Error(`No objects found in the source folder ${fromKey}.`);
|
||||
}
|
||||
|
||||
for (const object of listedObjects.Contents) {
|
||||
const match = object.Key?.match(/(.*)\/(.*)/);
|
||||
const folderAndFilePaths = this.extractFolderAndFilePaths(object.Key);
|
||||
|
||||
if (!isDefined(match)) {
|
||||
if (!isDefined(folderAndFilePaths)) {
|
||||
continue;
|
||||
}
|
||||
const fromFolderPath = match[1];
|
||||
const filename = match[2];
|
||||
|
||||
const { fromFolderPath, filename } = folderAndFilePaths;
|
||||
|
||||
const toFolderPath = fromFolderPath.replace(
|
||||
params.from.folderPath,
|
||||
params.to.folderPath,
|
||||
@@ -269,6 +291,85 @@ export class S3Driver implements StorageDriver {
|
||||
}
|
||||
}
|
||||
|
||||
async download(params: {
|
||||
from: { folderPath: string; filename?: string };
|
||||
to: { folderPath: string; filename?: string };
|
||||
}): Promise<void> {
|
||||
if (!params.from.filename && params.to.filename) {
|
||||
throw new Error('Cannot copy folder to file');
|
||||
}
|
||||
|
||||
if (isDefined(params.from.filename)) {
|
||||
try {
|
||||
const dir = params.to.folderPath;
|
||||
|
||||
await mkdir(dir, { recursive: true });
|
||||
|
||||
const fileStream = await this.read({
|
||||
folderPath: params.from.folderPath,
|
||||
filename: params.from.filename,
|
||||
});
|
||||
|
||||
const toPath = join(
|
||||
params.to.folderPath,
|
||||
params.to.filename || params.from.filename,
|
||||
);
|
||||
|
||||
await pipeline(fileStream, fs.createWriteStream(toPath));
|
||||
|
||||
return;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
const listedObjects = await this.s3Client.send(
|
||||
new ListObjectsV2Command({
|
||||
Bucket: this.bucketName,
|
||||
Prefix: params.from.folderPath,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!listedObjects.Contents || listedObjects.Contents.length === 0) {
|
||||
throw new Error(
|
||||
`No objects found in the source folder ${params.from.folderPath}.`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const object of listedObjects.Contents) {
|
||||
const folderAndFilePaths = this.extractFolderAndFilePaths(object.Key);
|
||||
|
||||
if (!isDefined(folderAndFilePaths)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { fromFolderPath, filename } = folderAndFilePaths;
|
||||
const toFolderPath = fromFolderPath.replace(
|
||||
params.from.folderPath,
|
||||
params.to.folderPath,
|
||||
);
|
||||
|
||||
if (!isDefined(toFolderPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.download({
|
||||
from: {
|
||||
folderPath: fromFolderPath,
|
||||
filename,
|
||||
},
|
||||
to: { folderPath: toFolderPath, filename },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async checkBucketExists(args: HeadBucketCommandInput) {
|
||||
try {
|
||||
await this.s3Client.headBucket(args);
|
||||
|
||||
@@ -40,4 +40,11 @@ export class FileStorageService implements StorageDriver {
|
||||
}): Promise<void> {
|
||||
return this.driver.copy(params);
|
||||
}
|
||||
|
||||
download(params: {
|
||||
from: { folderPath: string; filename?: string };
|
||||
to: { folderPath: string; filename?: string };
|
||||
}): Promise<void> {
|
||||
return this.driver.download(params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content';
|
||||
import { SOURCE_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/source-file-name';
|
||||
import { compileTypescript } from 'src/engine/core-modules/serverless/drivers/utils/compile-typescript';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
|
||||
|
||||
export class BaseServerlessDriver {
|
||||
async getCompiledCode(
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
fileStorageService: FileStorageService,
|
||||
) {
|
||||
const folderPath = getServerlessFolder({
|
||||
serverlessFunction,
|
||||
version: 'draft',
|
||||
});
|
||||
const fileStream = await fileStorageService.read({
|
||||
folderPath,
|
||||
filename: SOURCE_FILE_NAME,
|
||||
});
|
||||
const typescriptCode = await readFileContent(fileStream);
|
||||
|
||||
return compileTypescript(typescriptCode);
|
||||
}
|
||||
}
|
||||
1
packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/.gitignore
vendored
Normal file
1
packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!base-typescript-project/**/.env
|
||||
@@ -0,0 +1,2 @@
|
||||
# Add your environment variables here.
|
||||
# Access them in your serverless function code using process.env.VARIABLE
|
||||
@@ -0,0 +1,7 @@
|
||||
export const handler = async (
|
||||
event: object,
|
||||
context: object,
|
||||
): Promise<object> => {
|
||||
// Your code here
|
||||
return {};
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const ENV_FILE_NAME = '.env';
|
||||
@@ -0,0 +1 @@
|
||||
export const INDEX_FILE_NAME = 'index.ts';
|
||||
@@ -0,0 +1 @@
|
||||
export const OUTDIR_FOLDER = 'dist';
|
||||
@@ -1 +0,0 @@
|
||||
export const SOURCE_FILE_NAME = 'source.ts';
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
import {
|
||||
CreateFunctionCommand,
|
||||
DeleteFunctionCommand,
|
||||
@@ -18,6 +19,8 @@ import {
|
||||
waitUntilFunctionUpdatedV2,
|
||||
ListLayerVersionsCommandInput,
|
||||
ListLayerVersionsCommand,
|
||||
UpdateFunctionConfigurationCommand,
|
||||
UpdateFunctionConfigurationCommandInput,
|
||||
} from '@aws-sdk/client-lambda';
|
||||
import { CreateFunctionCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/CreateFunctionCommand';
|
||||
import { UpdateFunctionCodeCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/UpdateFunctionCodeCommand';
|
||||
@@ -36,7 +39,6 @@ import {
|
||||
NODE_LAYER_SUBFOLDER,
|
||||
} from 'src/engine/core-modules/serverless/drivers/utils/lambda-build-directory-manager';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { BaseServerlessDriver } from 'src/engine/core-modules/serverless/drivers/base-serverless.driver';
|
||||
import { createZipFile } from 'src/engine/core-modules/serverless/drivers/utils/create-zip-file';
|
||||
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
||||
import {
|
||||
@@ -46,6 +48,11 @@ import {
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
import { COMMON_LAYER_NAME } from 'src/engine/core-modules/serverless/drivers/constants/common-layer-name';
|
||||
import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies';
|
||||
import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
|
||||
import { SERVERLESS_TMPDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/serverless-tmpdir-folder';
|
||||
import { compileTypescript } from 'src/engine/core-modules/serverless/drivers/utils/compile-typescript';
|
||||
import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name';
|
||||
import { OUTDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/outdir-folder';
|
||||
|
||||
export interface LambdaDriverOptions extends LambdaClientConfig {
|
||||
fileStorageService: FileStorageService;
|
||||
@@ -53,16 +60,12 @@ export interface LambdaDriverOptions extends LambdaClientConfig {
|
||||
role: string;
|
||||
}
|
||||
|
||||
export class LambdaDriver
|
||||
extends BaseServerlessDriver
|
||||
implements ServerlessDriver
|
||||
{
|
||||
export class LambdaDriver implements ServerlessDriver {
|
||||
private readonly lambdaClient: Lambda;
|
||||
private readonly lambdaRole: string;
|
||||
private readonly fileStorageService: FileStorageService;
|
||||
|
||||
constructor(options: LambdaDriverOptions) {
|
||||
super();
|
||||
const { region, role, ...lambdaOptions } = options;
|
||||
|
||||
this.lambdaClient = new Lambda({ ...lambdaOptions, region });
|
||||
@@ -165,24 +168,50 @@ export class LambdaDriver
|
||||
}
|
||||
}
|
||||
|
||||
async build(serverlessFunction: ServerlessFunctionEntity) {
|
||||
const javascriptCode = await this.getCompiledCode(
|
||||
private getInMemoryServerlessFunctionFolderPath = (
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
version: string,
|
||||
) => {
|
||||
return join(SERVERLESS_TMPDIR_FOLDER, serverlessFunction.id, version);
|
||||
};
|
||||
|
||||
async build(serverlessFunction: ServerlessFunctionEntity, version: string) {
|
||||
const computedVersion =
|
||||
version === 'latest' ? serverlessFunction.latestVersion : version;
|
||||
|
||||
const inMemoryServerlessFunctionFolderPath =
|
||||
this.getInMemoryServerlessFunctionFolderPath(
|
||||
serverlessFunction,
|
||||
computedVersion,
|
||||
);
|
||||
|
||||
const folderPath = getServerlessFolder({
|
||||
serverlessFunction,
|
||||
this.fileStorageService,
|
||||
version,
|
||||
});
|
||||
|
||||
await this.fileStorageService.download({
|
||||
from: { folderPath },
|
||||
to: { folderPath: inMemoryServerlessFunctionFolderPath },
|
||||
});
|
||||
|
||||
compileTypescript(inMemoryServerlessFunctionFolderPath);
|
||||
|
||||
const lambdaZipPath = join(
|
||||
inMemoryServerlessFunctionFolderPath,
|
||||
'lambda.zip',
|
||||
);
|
||||
|
||||
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
|
||||
|
||||
const {
|
||||
sourceTemporaryDir,
|
||||
await createZipFile(
|
||||
join(inMemoryServerlessFunctionFolderPath, OUTDIR_FOLDER),
|
||||
lambdaZipPath,
|
||||
javascriptFilePath,
|
||||
lambdaHandler,
|
||||
} = await lambdaBuildDirectoryManager.init();
|
||||
);
|
||||
|
||||
await fs.writeFile(javascriptFilePath, javascriptCode);
|
||||
const envFileContent = await fs.readFile(
|
||||
join(inMemoryServerlessFunctionFolderPath, ENV_FILE_NAME),
|
||||
);
|
||||
|
||||
await createZipFile(sourceTemporaryDir, lambdaZipPath);
|
||||
const envVariables = dotenv.parse(envFileContent);
|
||||
|
||||
const functionExists = await this.checkFunctionExists(
|
||||
serverlessFunction.id,
|
||||
@@ -198,8 +227,11 @@ export class LambdaDriver
|
||||
ZipFile: await fs.readFile(lambdaZipPath),
|
||||
},
|
||||
FunctionName: serverlessFunction.id,
|
||||
Handler: lambdaHandler,
|
||||
Handler: 'src/index.handler',
|
||||
Layers: [layerArn],
|
||||
Environment: {
|
||||
Variables: envVariables,
|
||||
},
|
||||
Role: this.lambdaRole,
|
||||
Runtime: serverlessFunction.runtime,
|
||||
Description: 'Lambda function to run user script',
|
||||
@@ -210,23 +242,37 @@ export class LambdaDriver
|
||||
|
||||
await this.lambdaClient.send(command);
|
||||
} else {
|
||||
const params: UpdateFunctionCodeCommandInput = {
|
||||
const updateCodeParams: UpdateFunctionCodeCommandInput = {
|
||||
ZipFile: await fs.readFile(lambdaZipPath),
|
||||
FunctionName: serverlessFunction.id,
|
||||
};
|
||||
|
||||
const command = new UpdateFunctionCodeCommand(params);
|
||||
const updateCodeCommand = new UpdateFunctionCodeCommand(updateCodeParams);
|
||||
|
||||
await this.lambdaClient.send(command);
|
||||
await this.lambdaClient.send(updateCodeCommand);
|
||||
|
||||
const updateConfigurationParams: UpdateFunctionConfigurationCommandInput =
|
||||
{
|
||||
Environment: {
|
||||
Variables: envVariables,
|
||||
},
|
||||
FunctionName: serverlessFunction.id,
|
||||
};
|
||||
|
||||
const updateConfigurationCommand = new UpdateFunctionConfigurationCommand(
|
||||
updateConfigurationParams,
|
||||
);
|
||||
|
||||
await this.waitFunctionUpdates(serverlessFunction.id, 10);
|
||||
|
||||
await this.lambdaClient.send(updateConfigurationCommand);
|
||||
}
|
||||
|
||||
await this.waitFunctionUpdates(serverlessFunction.id, 10);
|
||||
|
||||
await lambdaBuildDirectoryManager.clean();
|
||||
}
|
||||
|
||||
async publish(serverlessFunction: ServerlessFunctionEntity) {
|
||||
await this.build(serverlessFunction);
|
||||
await this.build(serverlessFunction, 'draft');
|
||||
const params: PublishVersionCommandInput = {
|
||||
FunctionName: serverlessFunction.id,
|
||||
};
|
||||
@@ -240,6 +286,20 @@ export class LambdaDriver
|
||||
throw new Error('New published version is undefined');
|
||||
}
|
||||
|
||||
const draftFolderPath = getServerlessFolder({
|
||||
serverlessFunction: serverlessFunction,
|
||||
version: 'draft',
|
||||
});
|
||||
const newFolderPath = getServerlessFolder({
|
||||
serverlessFunction: serverlessFunction,
|
||||
version: newVersion,
|
||||
});
|
||||
|
||||
await this.fileStorageService.copy({
|
||||
from: { folderPath: draftFolderPath },
|
||||
to: { folderPath: newFolderPath },
|
||||
});
|
||||
|
||||
return newVersion;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { fork } from 'child_process';
|
||||
import { promises as fs, existsSync } from 'fs';
|
||||
import { promises as fs } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { v4 } from 'uuid';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
import {
|
||||
ServerlessDriver,
|
||||
ServerlessExecuteError,
|
||||
@@ -12,35 +11,36 @@ import {
|
||||
} from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
|
||||
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content';
|
||||
import { BaseServerlessDriver } from 'src/engine/core-modules/serverless/drivers/base-serverless.driver';
|
||||
import { BUILD_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/build-file-name';
|
||||
import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
|
||||
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import {
|
||||
ServerlessFunctionException,
|
||||
ServerlessFunctionExceptionCode,
|
||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||
import { COMMON_LAYER_NAME } from 'src/engine/core-modules/serverless/drivers/constants/common-layer-name';
|
||||
import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies';
|
||||
import { SERVERLESS_TMPDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/serverless-tmpdir-folder';
|
||||
import { compileTypescript } from 'src/engine/core-modules/serverless/drivers/utils/compile-typescript';
|
||||
import { OUTDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/outdir-folder';
|
||||
import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name';
|
||||
|
||||
const LISTENER_FILE_NAME = 'listener.js';
|
||||
|
||||
export interface LocalDriverOptions {
|
||||
fileStorageService: FileStorageService;
|
||||
}
|
||||
|
||||
export class LocalDriver
|
||||
extends BaseServerlessDriver
|
||||
implements ServerlessDriver
|
||||
{
|
||||
export class LocalDriver implements ServerlessDriver {
|
||||
private readonly fileStorageService: FileStorageService;
|
||||
|
||||
constructor(options: LocalDriverOptions) {
|
||||
super();
|
||||
this.fileStorageService = options.fileStorageService;
|
||||
}
|
||||
|
||||
private getInMemoryServerlessFunctionFolderPath = (
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
version: string,
|
||||
) => {
|
||||
return join(SERVERLESS_TMPDIR_FOLDER, serverlessFunction.id, version);
|
||||
};
|
||||
|
||||
private getInMemoryLayerFolderPath = (version: number) => {
|
||||
return join(SERVERLESS_TMPDIR_FOLDER, COMMON_LAYER_NAME, `${version}`);
|
||||
};
|
||||
@@ -49,88 +49,54 @@ export class LocalDriver
|
||||
const inMemoryLastVersionLayerFolderPath =
|
||||
this.getInMemoryLayerFolderPath(version);
|
||||
|
||||
if (existsSync(inMemoryLastVersionLayerFolderPath)) {
|
||||
return;
|
||||
try {
|
||||
await fs.access(inMemoryLastVersionLayerFolderPath);
|
||||
} catch (e) {
|
||||
await copyAndBuildDependencies(inMemoryLastVersionLayerFolderPath);
|
||||
}
|
||||
|
||||
await copyAndBuildDependencies(inMemoryLastVersionLayerFolderPath);
|
||||
}
|
||||
|
||||
async delete() {}
|
||||
|
||||
async build(serverlessFunction: ServerlessFunctionEntity) {
|
||||
await this.createLayerIfNotExists(serverlessFunction.layerVersion);
|
||||
const javascriptCode = await this.getCompiledCode(
|
||||
serverlessFunction,
|
||||
this.fileStorageService,
|
||||
);
|
||||
async build(serverlessFunction: ServerlessFunctionEntity, version: string) {
|
||||
const computedVersion =
|
||||
version === 'latest' ? serverlessFunction.latestVersion : version;
|
||||
|
||||
const draftFolderPath = getServerlessFolder({
|
||||
serverlessFunction,
|
||||
version: 'draft',
|
||||
});
|
||||
|
||||
await this.fileStorageService.write({
|
||||
file: javascriptCode,
|
||||
name: BUILD_FILE_NAME,
|
||||
mimeType: undefined,
|
||||
folder: draftFolderPath,
|
||||
});
|
||||
}
|
||||
|
||||
async publish(serverlessFunction: ServerlessFunctionEntity) {
|
||||
await this.build(serverlessFunction);
|
||||
|
||||
return serverlessFunction.latestVersion
|
||||
? `${parseInt(serverlessFunction.latestVersion, 10) + 1}`
|
||||
: '1';
|
||||
}
|
||||
|
||||
async execute(
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
payload: object,
|
||||
version: string,
|
||||
): Promise<ServerlessExecuteResult> {
|
||||
await this.createLayerIfNotExists(serverlessFunction.layerVersion);
|
||||
|
||||
const startTime = Date.now();
|
||||
let fileContent = '';
|
||||
const inMemoryServerlessFunctionFolderPath =
|
||||
this.getInMemoryServerlessFunctionFolderPath(
|
||||
serverlessFunction,
|
||||
computedVersion,
|
||||
);
|
||||
|
||||
try {
|
||||
const fileStream = await this.fileStorageService.read({
|
||||
folderPath: getServerlessFolder({
|
||||
serverlessFunction,
|
||||
version,
|
||||
}),
|
||||
filename: BUILD_FILE_NAME,
|
||||
});
|
||||
const folderPath = getServerlessFolder({
|
||||
serverlessFunction,
|
||||
version,
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
await this.fileStorageService.download({
|
||||
from: { folderPath },
|
||||
to: { folderPath: inMemoryServerlessFunctionFolderPath },
|
||||
});
|
||||
|
||||
const tmpFolderPath = join(SERVERLESS_TMPDIR_FOLDER, v4());
|
||||
compileTypescript(inMemoryServerlessFunctionFolderPath);
|
||||
|
||||
const tmpFilePath = join(tmpFolderPath, 'index.js');
|
||||
|
||||
await fs.symlink(
|
||||
this.getInMemoryLayerFolderPath(serverlessFunction.layerVersion),
|
||||
tmpFolderPath,
|
||||
'dir',
|
||||
const envFileContent = await fs.readFile(
|
||||
join(inMemoryServerlessFunctionFolderPath, ENV_FILE_NAME),
|
||||
);
|
||||
|
||||
const modifiedContent = `
|
||||
const envVariables = dotenv.parse(envFileContent);
|
||||
|
||||
const listener = `
|
||||
const index_1 = require("./src/index");
|
||||
|
||||
process.env = ${JSON.stringify(envVariables)}
|
||||
|
||||
process.on('message', async (message) => {
|
||||
const { event, context } = message;
|
||||
try {
|
||||
const result = await handler(event, context);
|
||||
const result = await index_1.handler(event, context);
|
||||
process.send(result);
|
||||
} catch (error) {
|
||||
process.send({
|
||||
@@ -140,82 +106,158 @@ export class LocalDriver
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
${fileContent}
|
||||
`;
|
||||
|
||||
await fs.writeFile(tmpFilePath, modifiedContent);
|
||||
await fs.writeFile(
|
||||
join(
|
||||
inMemoryServerlessFunctionFolderPath,
|
||||
OUTDIR_FOLDER,
|
||||
LISTENER_FILE_NAME,
|
||||
),
|
||||
listener,
|
||||
);
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
const child = fork(tmpFilePath, { silent: true });
|
||||
try {
|
||||
await fs.symlink(
|
||||
join(
|
||||
this.getInMemoryLayerFolderPath(serverlessFunction.layerVersion),
|
||||
'node_modules',
|
||||
),
|
||||
join(
|
||||
inMemoryServerlessFunctionFolderPath,
|
||||
OUTDIR_FOLDER,
|
||||
'node_modules',
|
||||
),
|
||||
'dir',
|
||||
);
|
||||
} catch (err) {
|
||||
if (err.code !== 'EEXIST') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
child.on('message', (message: object | ServerlessExecuteError) => {
|
||||
const duration = Date.now() - startTime;
|
||||
async publish(serverlessFunction: ServerlessFunctionEntity) {
|
||||
const newVersion = serverlessFunction.latestVersion
|
||||
? `${parseInt(serverlessFunction.latestVersion, 10) + 1}`
|
||||
: '1';
|
||||
|
||||
const draftFolderPath = getServerlessFolder({
|
||||
serverlessFunction: serverlessFunction,
|
||||
version: 'draft',
|
||||
});
|
||||
const newFolderPath = getServerlessFolder({
|
||||
serverlessFunction: serverlessFunction,
|
||||
version: newVersion,
|
||||
});
|
||||
|
||||
await this.fileStorageService.copy({
|
||||
from: { folderPath: draftFolderPath },
|
||||
to: { folderPath: newFolderPath },
|
||||
});
|
||||
|
||||
await this.build(serverlessFunction, newVersion);
|
||||
|
||||
return newVersion;
|
||||
}
|
||||
|
||||
async execute(
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
payload: object,
|
||||
version: string,
|
||||
): Promise<ServerlessExecuteResult> {
|
||||
const startTime = Date.now();
|
||||
const computedVersion =
|
||||
version === 'latest' ? serverlessFunction.latestVersion : version;
|
||||
|
||||
const listenerFile = join(
|
||||
this.getInMemoryServerlessFunctionFolderPath(
|
||||
serverlessFunction,
|
||||
computedVersion,
|
||||
),
|
||||
OUTDIR_FOLDER,
|
||||
LISTENER_FILE_NAME,
|
||||
);
|
||||
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const child = fork(listenerFile, { silent: true });
|
||||
|
||||
child.on('message', (message: object | ServerlessExecuteError) => {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if ('errorType' in message) {
|
||||
resolve({
|
||||
data: null,
|
||||
duration,
|
||||
error: message,
|
||||
status: ServerlessFunctionExecutionStatus.ERROR,
|
||||
});
|
||||
} else {
|
||||
resolve({
|
||||
data: message,
|
||||
duration,
|
||||
status: ServerlessFunctionExecutionStatus.SUCCESS,
|
||||
});
|
||||
}
|
||||
child.kill();
|
||||
});
|
||||
|
||||
child.stderr?.on('data', (data) => {
|
||||
const stackTrace = data
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter((line: string) => line.trim() !== '');
|
||||
const errorTrace = stackTrace.filter((line: string) =>
|
||||
line.includes('Error: '),
|
||||
)?.[0];
|
||||
|
||||
let errorType = 'Unknown';
|
||||
let errorMessage = '';
|
||||
|
||||
if (errorTrace) {
|
||||
errorType = errorTrace.split(':')[0];
|
||||
errorMessage = errorTrace.split(': ')[1];
|
||||
}
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if ('errorType' in message) {
|
||||
resolve({
|
||||
data: null,
|
||||
duration,
|
||||
error: message,
|
||||
status: ServerlessFunctionExecutionStatus.ERROR,
|
||||
error: {
|
||||
errorType,
|
||||
errorMessage,
|
||||
stackTrace: stackTrace,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
resolve({
|
||||
data: message,
|
||||
duration,
|
||||
status: ServerlessFunctionExecutionStatus.SUCCESS,
|
||||
});
|
||||
}
|
||||
child.kill();
|
||||
fs.unlink(tmpFilePath).catch(console.error);
|
||||
});
|
||||
|
||||
child.stderr?.on('data', (data) => {
|
||||
const stackTrace = data
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter((line: string) => line.trim() !== '');
|
||||
const errorTrace = stackTrace.filter((line: string) =>
|
||||
line.includes('Error: '),
|
||||
)?.[0];
|
||||
|
||||
let errorType = 'Unknown';
|
||||
let errorMessage = '';
|
||||
|
||||
if (errorTrace) {
|
||||
errorType = errorTrace.split(':')[0];
|
||||
errorMessage = errorTrace.split(': ')[1];
|
||||
}
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
resolve({
|
||||
data: null,
|
||||
duration,
|
||||
status: ServerlessFunctionExecutionStatus.ERROR,
|
||||
error: {
|
||||
errorType,
|
||||
errorMessage,
|
||||
stackTrace: stackTrace,
|
||||
},
|
||||
child.kill();
|
||||
});
|
||||
child.kill();
|
||||
fs.unlink(tmpFilePath).catch(console.error);
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
reject(error);
|
||||
child.kill();
|
||||
fs.unlink(tmpFilePath).catch(console.error);
|
||||
});
|
||||
child.on('error', (error) => {
|
||||
reject(error);
|
||||
child.kill();
|
||||
});
|
||||
|
||||
child.on('exit', (code) => {
|
||||
if (code && code !== 0) {
|
||||
reject(new Error(`Child process exited with code ${code}`));
|
||||
fs.unlink(tmpFilePath).catch(console.error);
|
||||
}
|
||||
});
|
||||
child.on('exit', (code) => {
|
||||
if (code && code !== 0) {
|
||||
reject(new Error(`Child process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
child.send({ event: payload });
|
||||
});
|
||||
child.send({ event: payload });
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
duration: Date.now() - startTime,
|
||||
error: {
|
||||
errorType: 'UnhandledError',
|
||||
errorMessage: error.message || 'Unknown error',
|
||||
stackTrace: error.stack ? error.stack.split('\n') : [],
|
||||
},
|
||||
status: ServerlessFunctionExecutionStatus.ERROR,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import ts from 'typescript';
|
||||
import { join } from 'path';
|
||||
|
||||
export const compileTypescript = (typescriptCode: string): string => {
|
||||
import ts, { createProgram } from 'typescript';
|
||||
|
||||
import { OUTDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/outdir-folder';
|
||||
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
|
||||
|
||||
export const compileTypescript = (folderPath: string) => {
|
||||
const options: ts.CompilerOptions = {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2017,
|
||||
@@ -8,12 +13,9 @@ export const compileTypescript = (typescriptCode: string): string => {
|
||||
esModuleInterop: true,
|
||||
resolveJsonModule: true,
|
||||
allowSyntheticDefaultImports: true,
|
||||
outDir: join(folderPath, OUTDIR_FOLDER, 'src'),
|
||||
types: ['node'],
|
||||
};
|
||||
|
||||
const result = ts.transpileModule(typescriptCode, {
|
||||
compilerOptions: options,
|
||||
});
|
||||
|
||||
return result.outputText;
|
||||
createProgram([join(folderPath, 'src', INDEX_FILE_NAME)], options).emit();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import path, { join } from 'path';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
import { ASSET_PATH } from 'src/constants/assets-path';
|
||||
|
||||
type File = { name: string; path: string; content: Buffer };
|
||||
|
||||
const getAllFiles = async (
|
||||
rootDir: string,
|
||||
dir: string = rootDir,
|
||||
files: File[] = [],
|
||||
): Promise<File[]> => {
|
||||
const dirEntries = await fs.readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of dirEntries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
return getAllFiles(rootDir, fullPath, files);
|
||||
} else {
|
||||
files.push({
|
||||
path: path.relative(rootDir, dir),
|
||||
name: entry.name,
|
||||
content: await fs.readFile(fullPath),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
};
|
||||
|
||||
export const getBaseTypescriptProjectFiles = (async () => {
|
||||
const baseTypescriptProjectPath = join(
|
||||
ASSET_PATH,
|
||||
`engine/core-modules/serverless/drivers/constants/base-typescript-project`,
|
||||
);
|
||||
|
||||
return await getAllFiles(baseTypescriptProjectPath);
|
||||
})();
|
||||
@@ -1,12 +1,17 @@
|
||||
import path from 'path';
|
||||
import path, { join } from 'path';
|
||||
|
||||
import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version';
|
||||
import { ASSET_PATH } from 'src/constants/assets-path';
|
||||
|
||||
// Can only be used in src/engine/integrations/serverless/drivers/utils folder
|
||||
export const getLayerDependenciesDirName = (
|
||||
version: 'latest' | 'engine' | number,
|
||||
): string => {
|
||||
const formattedVersion = version === 'latest' ? LAST_LAYER_VERSION : version;
|
||||
|
||||
return path.resolve(__dirname, `../layers/${formattedVersion}`);
|
||||
const baseTypescriptProjectPath = join(
|
||||
ASSET_PATH,
|
||||
`engine/core-modules/serverless/drivers/layers/${formattedVersion}`,
|
||||
);
|
||||
|
||||
return path.resolve(baseTypescriptProjectPath);
|
||||
};
|
||||
|
||||
@@ -10,14 +10,12 @@ export const NODE_LAYER_SUBFOLDER = 'nodejs';
|
||||
const TEMPORARY_LAMBDA_FOLDER = 'lambda-build';
|
||||
const TEMPORARY_LAMBDA_SOURCE_FOLDER = 'src';
|
||||
const LAMBDA_ZIP_FILE_NAME = 'lambda.zip';
|
||||
const LAMBDA_ENTRY_FILE_NAME = 'index.js';
|
||||
|
||||
export class LambdaBuildDirectoryManager {
|
||||
private temporaryDir = join(
|
||||
SERVERLESS_TMPDIR_FOLDER,
|
||||
`${TEMPORARY_LAMBDA_FOLDER}-${v4()}`,
|
||||
);
|
||||
private lambdaHandler = `${LAMBDA_ENTRY_FILE_NAME.split('.')[0]}.handler`;
|
||||
|
||||
async init() {
|
||||
const sourceTemporaryDir = join(
|
||||
@@ -25,15 +23,12 @@ export class LambdaBuildDirectoryManager {
|
||||
TEMPORARY_LAMBDA_SOURCE_FOLDER,
|
||||
);
|
||||
const lambdaZipPath = join(this.temporaryDir, LAMBDA_ZIP_FILE_NAME);
|
||||
const javascriptFilePath = join(sourceTemporaryDir, LAMBDA_ENTRY_FILE_NAME);
|
||||
|
||||
await fs.mkdir(sourceTemporaryDir, { recursive: true });
|
||||
|
||||
return {
|
||||
sourceTemporaryDir,
|
||||
lambdaZipPath,
|
||||
javascriptFilePath,
|
||||
lambdaHandler: this.lambdaHandler,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
@InputType()
|
||||
export class CreateServerlessFunctionFromFileInput {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
import { CreateServerlessFunctionFromFileInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function-from-file.input';
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
@InputType()
|
||||
export class CreateServerlessFunctionInput extends CreateServerlessFunctionFromFileInput {
|
||||
export class CreateServerlessFunctionInput {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
code: string;
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@@ -50,11 +50,6 @@ export class ServerlessFunctionDTO {
|
||||
@Field({ nullable: true })
|
||||
description: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
sourceCodeHash: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
|
||||
import { IsNotEmpty, IsObject, IsString, IsUUID } from 'class-validator';
|
||||
import graphqlTypeJson from 'graphql-type-json';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@@ -21,7 +22,7 @@ export class UpdateServerlessFunctionInput {
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
code: string;
|
||||
@Field(() => graphqlTypeJson)
|
||||
@IsObject()
|
||||
code: JSON;
|
||||
}
|
||||
|
||||
@@ -29,9 +29,6 @@ export class ServerlessFunctionEntity {
|
||||
@Column({ nullable: true })
|
||||
latestVersion: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
sourceCodeHash: string;
|
||||
|
||||
@Column({ nullable: false, default: ServerlessFunctionRuntime.NODE18 })
|
||||
runtime: ServerlessFunctionRuntime;
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import graphqlTypeJson from 'graphql-type-json';
|
||||
import { FileUpload, GraphQLUpload } from 'graphql-upload';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
@@ -11,7 +10,6 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreateServerlessFunctionFromFileInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function-from-file.input';
|
||||
import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
|
||||
import { DeleteServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/delete-serverless-function.input';
|
||||
import { ExecuteServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/execute-serverless-function.input';
|
||||
@@ -63,7 +61,7 @@ export class ServerlessFunctionResolver {
|
||||
}
|
||||
}
|
||||
|
||||
@Query(() => String, { nullable: true })
|
||||
@Query(() => graphqlTypeJson, { nullable: true })
|
||||
async getServerlessFunctionSourceCode(
|
||||
@Args('input') input: GetServerlessFunctionSourceCodeInput,
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
@@ -130,28 +128,6 @@ export class ServerlessFunctionResolver {
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
},
|
||||
input.code,
|
||||
workspaceId,
|
||||
);
|
||||
} catch (error) {
|
||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@Mutation(() => ServerlessFunctionDTO)
|
||||
async createOneServerlessFunctionFromFile(
|
||||
@Args({ name: 'file', type: () => GraphQLUpload })
|
||||
file: FileUpload,
|
||||
@Args('input')
|
||||
input: CreateServerlessFunctionFromFileInput,
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
) {
|
||||
try {
|
||||
await this.checkFeatureFlag(workspaceId);
|
||||
|
||||
return await this.serverlessFunctionService.createOneServerlessFunction(
|
||||
input,
|
||||
file,
|
||||
workspaceId,
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { basename, dirname, join } from 'path';
|
||||
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { FileUpload } from 'graphql-upload';
|
||||
import { Repository } from 'typeorm';
|
||||
import deepEqual from 'deep-equal';
|
||||
|
||||
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
import { ServerlessExecuteResult } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
|
||||
@@ -12,10 +14,9 @@ import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.se
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content';
|
||||
import { SOURCE_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/source-file-name';
|
||||
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
|
||||
import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service';
|
||||
import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
|
||||
import { CreateServerlessFunctionFromFileInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function-from-file.input';
|
||||
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
|
||||
import {
|
||||
ServerlessFunctionEntity,
|
||||
@@ -25,10 +26,12 @@ import {
|
||||
ServerlessFunctionException,
|
||||
ServerlessFunctionExceptionCode,
|
||||
} 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 { isDefined } from 'src/utils/is-defined';
|
||||
import { getLastLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-layer-dependencies';
|
||||
import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version';
|
||||
import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
|
||||
import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files';
|
||||
import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name';
|
||||
|
||||
@Injectable()
|
||||
export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFunctionEntity> {
|
||||
@@ -47,7 +50,7 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
||||
workspaceId: string,
|
||||
id: string,
|
||||
version: string,
|
||||
) {
|
||||
): Promise<{ [filePath: string]: string } | undefined> {
|
||||
const serverlessFunction = await this.serverlessFunctionRepository.findOne({
|
||||
where: {
|
||||
id,
|
||||
@@ -68,12 +71,20 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
||||
version,
|
||||
});
|
||||
|
||||
const fileStream = await this.fileStorageService.read({
|
||||
folderPath,
|
||||
filename: SOURCE_FILE_NAME,
|
||||
const indexFileStream = await this.fileStorageService.read({
|
||||
folderPath: join(folderPath, 'src'),
|
||||
filename: INDEX_FILE_NAME,
|
||||
});
|
||||
|
||||
return await readFileContent(fileStream);
|
||||
const envFileStream = await this.fileStorageService.read({
|
||||
folderPath: folderPath,
|
||||
filename: ENV_FILE_NAME,
|
||||
});
|
||||
|
||||
return {
|
||||
'.env': await readFileContent(envFileStream),
|
||||
'src/index.ts': await readFileContent(indexFileStream),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.code === FileStorageExceptionCode.FILE_NOT_FOUND) {
|
||||
return;
|
||||
@@ -132,10 +143,7 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
||||
'draft',
|
||||
);
|
||||
|
||||
if (
|
||||
serverlessFunctionCreateHash(latestCode || '') ===
|
||||
serverlessFunctionCreateHash(draftCode || '')
|
||||
) {
|
||||
if (deepEqual(latestCode, draftCode)) {
|
||||
throw new Error(
|
||||
'Cannot publish a new version when code has not changed',
|
||||
);
|
||||
@@ -146,20 +154,6 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
||||
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,
|
||||
});
|
||||
@@ -213,9 +207,6 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
||||
name: serverlessFunctionInput.name,
|
||||
description: serverlessFunctionInput.description,
|
||||
syncStatus: ServerlessFunctionSyncStatus.NOT_READY,
|
||||
sourceCodeHash: serverlessFunctionCreateHash(
|
||||
serverlessFunctionInput.code,
|
||||
),
|
||||
});
|
||||
|
||||
const fileFolder = getServerlessFolder({
|
||||
@@ -223,12 +214,14 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
||||
version: 'draft',
|
||||
});
|
||||
|
||||
await this.fileStorageService.write({
|
||||
file: serverlessFunctionInput.code,
|
||||
name: SOURCE_FILE_NAME,
|
||||
mimeType: undefined,
|
||||
folder: fileFolder,
|
||||
});
|
||||
for (const key of Object.keys(serverlessFunctionInput.code)) {
|
||||
await this.fileStorageService.write({
|
||||
file: serverlessFunctionInput.code[key],
|
||||
name: basename(key),
|
||||
mimeType: undefined,
|
||||
folder: join(fileFolder, dirname(key)),
|
||||
});
|
||||
}
|
||||
|
||||
await this.serverlessService.build(existingServerlessFunction, 'draft');
|
||||
await super.updateOne(existingServerlessFunction.id, {
|
||||
@@ -259,22 +252,12 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
||||
}
|
||||
|
||||
async createOneServerlessFunction(
|
||||
serverlessFunctionInput: CreateServerlessFunctionFromFileInput,
|
||||
code: FileUpload | string,
|
||||
serverlessFunctionInput: CreateServerlessFunctionInput,
|
||||
workspaceId: string,
|
||||
) {
|
||||
let typescriptCode: string;
|
||||
|
||||
if (typeof code === 'string') {
|
||||
typescriptCode = code;
|
||||
} else {
|
||||
typescriptCode = await readFileContent(code.createReadStream());
|
||||
}
|
||||
|
||||
const createdServerlessFunction = await super.createOne({
|
||||
...serverlessFunctionInput,
|
||||
workspaceId,
|
||||
sourceCodeHash: serverlessFunctionCreateHash(typescriptCode),
|
||||
layerVersion: LAST_LAYER_VERSION,
|
||||
});
|
||||
|
||||
@@ -283,12 +266,14 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
||||
version: 'draft',
|
||||
});
|
||||
|
||||
await this.fileStorageService.write({
|
||||
file: typescriptCode,
|
||||
name: SOURCE_FILE_NAME,
|
||||
mimeType: undefined,
|
||||
folder: draftFileFolder,
|
||||
});
|
||||
for (const file of await getBaseTypescriptProjectFiles) {
|
||||
await this.fileStorageService.write({
|
||||
file: file.content,
|
||||
name: file.name,
|
||||
mimeType: undefined,
|
||||
folder: join(draftFileFolder, file.path),
|
||||
});
|
||||
}
|
||||
|
||||
await this.serverlessService.build(createdServerlessFunction, 'draft');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user