6653 serverless functions store and use environment variables in serverless function scripts (#7390)

![image](https://github.com/user-attachments/assets/a15bd4c1-3db4-4466-b748-06bdf3874354)

![image](https://github.com/user-attachments/assets/71242dfb-956b-43ed-9704-87cb0dfbc98d)
This commit is contained in:
martmull
2024-10-03 13:56:17 +02:00
committed by GitHub
parent 3cd24d542b
commit 62fe1d0e88
39 changed files with 815 additions and 513 deletions

View File

@@ -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 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 \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 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 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 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 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 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 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 sourceCodeHash\n runtime\n syncStatus\n latestVersion\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc, "\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 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(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument, "\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument,
@@ -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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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

View File

@@ -1,9 +1,8 @@
import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope'; import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { Button } from '@/ui/input/button/components/Button'; 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 { CoreEditorHeader } from '@/ui/input/code-editor/components/CodeEditorHeader';
import { Section } from '@/ui/layout/section/components/Section'; import { Section } from '@/ui/layout/section/components/Section';
import { TabList } from '@/ui/layout/tab/components/TabList'; 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 { Key } from 'ts-key-enum';
import { H2Title, IconGitCommit, IconPlayerPlay, IconRestore } from 'twenty-ui'; import { H2Title, IconGitCommit, IconPlayerPlay, IconRestore } from 'twenty-ui';
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount'; 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)` const StyledTabList = styled(TabList)`
border-bottom: none; border-bottom: none;
`; `;
export const SettingsServerlessFunctionCodeEditorTab = ({ export const SettingsServerlessFunctionCodeEditorTab = ({
formValues, files,
handleExecute, handleExecute,
handlePublish, handlePublish,
handleReset, handleReset,
@@ -28,15 +30,19 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
onChange, onChange,
setIsCodeValid, setIsCodeValid,
}: { }: {
formValues: ServerlessFunctionFormValues; files: File[];
handleExecute: () => void; handleExecute: () => void;
handlePublish: () => void; handlePublish: () => void;
handleReset: () => void; handleReset: () => void;
resetDisabled: boolean; resetDisabled: boolean;
publishDisabled: boolean; publishDisabled: boolean;
onChange: (key: string) => (value: string) => void; onChange: (filePath: string, value: string) => void;
setIsCodeValid: (isCodeValid: boolean) => void; setIsCodeValid: (isCodeValid: boolean) => void;
}) => { }) => {
const { activeTabIdState } = useTabList(
SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID,
);
const activeTabId = useRecoilValue(activeTabIdState);
const TestButton = ( const TestButton = (
<Button <Button
title="Test" title="Test"
@@ -68,21 +74,15 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
/> />
); );
const TAB_LIST_COMPONENT_ID = 'serverless-function-editor';
const HeaderTabList = ( const HeaderTabList = (
<StyledTabList <StyledTabList
tabListId={TAB_LIST_COMPONENT_ID} tabListId={SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID}
tabs={[{ id: 'index.ts', title: 'index.ts' }]} 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(); const navigate = useNavigate();
useHotkeyScopeOnMount( useHotkeyScopeOnMount(
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionEditorTab, SettingsServerlessFunctionHotkeyScope.ServerlessFunctionEditorTab,
@@ -95,18 +95,25 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
}, },
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionEditorTab, SettingsServerlessFunctionHotkeyScope.ServerlessFunctionEditorTab,
); );
return ( return (
<Section> <Section>
<H2Title <H2Title
title="Code your function" title="Code your function"
description="Write your function (in typescript) below" description="Write your function (in typescript) below"
/> />
<CodeEditor <CoreEditorHeader
value={formValues.code} leftNodes={[HeaderTabList]}
onChange={onChange('code')} rightNodes={[ResetButton, PublishButton, TestButton]}
setIsCodeValid={setIsCodeValid}
header={Header}
/> />
{activeTabId && (
<CodeEditor
files={files}
currentFilePath={activeTabId}
onChange={(newCodeValue) => onChange(activeTabId, newCodeValue)}
setIsCodeValid={setIsCodeValid}
/>
)}
</Section> </Section>
); );
}; };

View File

@@ -44,28 +44,6 @@ export const SettingsServerlessFunctionTestTab = ({
settingsServerlessFunctionOutput.error || 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(); const navigate = useNavigate();
useHotkeyScopeOnMount( useHotkeyScopeOnMount(
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionTestTab, SettingsServerlessFunctionHotkeyScope.ServerlessFunctionTestTab,
@@ -86,20 +64,52 @@ export const SettingsServerlessFunctionTestTab = ({
description='Insert a JSON input, then press "Run" to test your function.' description='Insert a JSON input, then press "Run" to test your function.'
/> />
<StyledInputsContainer> <StyledInputsContainer>
<CodeEditor <div>
value={settingsServerlessFunctionInput} <CoreEditorHeader
height={200} title={'Input'}
onChange={setSettingsServerlessFunctionInput} rightNodes={[
language={'json'} <Button
header={InputHeader} title="Run Function"
/> variant="primary"
<CodeEditor accent="blue"
value={result} size="small"
height={settingsServerlessFunctionCodeEditorOutputParams.height} Icon={IconPlayerPlay}
language={settingsServerlessFunctionCodeEditorOutputParams.language} onClick={handleExecute}
options={{ readOnly: true, domReadOnly: true }} />,
header={OutputHeader} ]}
/> />
<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> </StyledInputsContainer>
</Section> </Section>
); );

View File

@@ -0,0 +1,2 @@
export const SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID =
'settings-serverless-function-editor-tab-list';

View File

@@ -5,7 +5,6 @@ export const SERVERLESS_FUNCTION_FRAGMENT = gql`
id id
name name
description description
sourceCodeHash
runtime runtime
syncStatus syncStatus
latestVersion latestVersion

View File

@@ -9,7 +9,7 @@ export type ServerlessFunctionNewFormValues = {
}; };
export type ServerlessFunctionFormValues = ServerlessFunctionNewFormValues & { export type ServerlessFunctionFormValues = ServerlessFunctionNewFormValues & {
code: string; code: { [filePath: string]: string } | undefined;
}; };
type SetServerlessFunctionFormValues = Dispatch< type SetServerlessFunctionFormValues = Dispatch<
@@ -26,7 +26,7 @@ export const useServerlessFunctionUpdateFormState = (
const [formValues, setFormValues] = useState<ServerlessFunctionFormValues>({ const [formValues, setFormValues] = useState<ServerlessFunctionFormValues>({
name: '', name: '',
description: '', description: '',
code: '', code: undefined,
}); });
const { serverlessFunction } = const { serverlessFunction } =
@@ -37,7 +37,7 @@ export const useServerlessFunctionUpdateFormState = (
version: 'draft', version: 'draft',
onCompleted: (data: FindOneServerlessFunctionSourceCodeQuery) => { onCompleted: (data: FindOneServerlessFunctionSourceCodeQuery) => {
const newState = { const newState = {
code: data?.getServerlessFunctionSourceCode || '', code: data?.getServerlessFunctionSourceCode || undefined,
name: serverlessFunction?.name || '', name: serverlessFunction?.name || '',
description: serverlessFunction?.description || '', description: serverlessFunction?.description || '',
}; };

View File

@@ -1,22 +1,13 @@
import Editor, { Monaco, EditorProps } from '@monaco-editor/react'; import Editor, { Monaco, EditorProps } from '@monaco-editor/react';
import dotenv from 'dotenv';
import { AutoTypings } from 'monaco-editor-auto-typings'; import { AutoTypings } from 'monaco-editor-auto-typings';
import { editor, MarkerSeverity } 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';
import { useEffect } from 'react';
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages'; import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
import { isDefined } from '~/utils/isDefined'; 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)` const StyledEditor = styled(Editor)`
border: 1px solid ${({ theme }) => theme.border.color.medium}; border: 1px solid ${({ theme }) => theme.border.color.medium};
border-top: none; border-top: none;
@@ -24,25 +15,34 @@ const StyledEditor = styled(Editor)`
${({ theme }) => theme.border.radius.sm}; ${({ theme }) => theme.border.radius.sm};
`; `;
export type File = {
language: string;
content: string;
path: string;
};
type CodeEditorProps = Omit<EditorProps, 'onChange'> & { type CodeEditorProps = Omit<EditorProps, 'onChange'> & {
header: React.ReactNode; currentFilePath: string;
files: File[];
onChange?: (value: string) => void; onChange?: (value: string) => void;
setIsCodeValid?: (isCodeValid: boolean) => void; setIsCodeValid?: (isCodeValid: boolean) => void;
}; };
export const CodeEditor = ({ export const CodeEditor = ({
value = DEFAULT_CODE, currentFilePath,
files,
onChange, onChange,
setIsCodeValid, setIsCodeValid,
language = 'typescript',
height = 450, height = 450,
options = undefined, options = undefined,
header,
}: CodeEditorProps) => { }: CodeEditorProps) => {
const theme = useTheme(); const theme = useTheme();
const { availablePackages } = useGetAvailablePackages(); const { availablePackages } = useGetAvailablePackages();
const currentFile = files.find((file) => file.path === currentFilePath);
const environmentVariablesFile = files.find((file) => file.path === '.env');
const handleEditorDidMount = async ( const handleEditorDidMount = async (
editor: editor.IStandaloneCodeEditor, editor: editor.IStandaloneCodeEditor,
monaco: Monaco, monaco: Monaco,
@@ -50,7 +50,57 @@ export const CodeEditor = ({
monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme)); monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme));
monaco.editor.setTheme('codeEditorTheme'); 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, { await AutoTypings.create(editor, {
monaco, monaco,
preloadPackages: true, preloadPackages: true,
@@ -71,43 +121,28 @@ export const CodeEditor = ({
setIsCodeValid?.(true); 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 ( return (
isDefined(currentFile) &&
isDefined(availablePackages) && ( isDefined(availablePackages) && (
<> <StyledEditor
{header} height={height}
<StyledEditor value={currentFile.content}
height={height} language={currentFile.language}
language={language} onMount={handleEditorDidMount}
value={value} onChange={(value?: string) => value && onChange?.(value)}
onMount={handleEditorDidMount} onValidate={handleEditorValidation}
onChange={(value?: string) => value && onChange?.(value)} options={{
onValidate={handleEditorValidation} ...options,
options={{ overviewRulerLanes: 0,
...options, scrollbar: {
overviewRulerLanes: 0, vertical: 'hidden',
scrollbar: { horizontal: 'hidden',
vertical: 'hidden', },
horizontal: 'hidden', minimap: {
}, enabled: false,
minimap: { },
enabled: false, }}
}, />
}}
/>
</>
) )
); );
}; };

View File

@@ -24,6 +24,7 @@ import { useRecoilValue, useSetRecoilState } from 'recoil';
import { IconCode, IconFunction, IconSettings, IconTestPipe } from 'twenty-ui'; import { IconCode, IconFunction, IconSettings, IconTestPipe } from 'twenty-ui';
import { usePreventOverlapCallback } from '~/hooks/usePreventOverlapCallback'; import { usePreventOverlapCallback } from '~/hooks/usePreventOverlapCallback';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
const TAB_LIST_COMPONENT_ID = 'serverless-function-detail'; 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 = const resetDisabled =
!isDefined(latestVersionCode) || latestVersionCode === formValues.code; !isDefined(latestVersionCode) ||
const publishDisabled = !isCodeValid || latestVersionCode === formValues.code; isDeeplyEqual(latestVersionCode, formValues.code);
const publishDisabled =
!isCodeValid || isDeeplyEqual(latestVersionCode, formValues.code);
const handleReset = async () => { const handleReset = async () => {
try { try {
const newState = { const newState = {
code: latestVersionCode || '', code: latestVersionCode || {},
}; };
setFormValues((prevState) => ({ setFormValues((prevState) => ({
...prevState, ...prevState,
@@ -166,18 +177,30 @@ export const SettingsServerlessFunctionDetail = () => {
{ id: 'settings', title: 'Settings', Icon: IconSettings }, { 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 = () => { const renderActiveTabContent = () => {
switch (activeTabId) { switch (activeTabId) {
case 'editor': case 'editor':
return ( return (
<SettingsServerlessFunctionCodeEditorTab <SettingsServerlessFunctionCodeEditorTab
formValues={formValues} files={files}
handleExecute={handleExecute} handleExecute={handleExecute}
handlePublish={handlePublish} handlePublish={handlePublish}
handleReset={handleReset} handleReset={handleReset}
resetDisabled={resetDisabled} resetDisabled={resetDisabled}
publishDisabled={publishDisabled} publishDisabled={publishDisabled}
onChange={onChange} onChange={onCodeChange}
setIsCodeValid={setIsCodeValid} setIsCodeValid={setIsCodeValid}
/> />
); );

View File

@@ -9,7 +9,6 @@ import { ServerlessFunctionNewFormValues } from '@/settings/serverless-functions
import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope'; import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { DEFAULT_CODE } from '@/ui/input/code-editor/components/CodeEditor';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useState } from 'react'; import { useState } from 'react';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
@@ -31,7 +30,6 @@ export const SettingsServerlessFunctionsNew = () => {
const newServerlessFunction = await createOneServerlessFunction({ const newServerlessFunction = await createOneServerlessFunction({
name: formValues.name, name: formValues.name,
description: formValues.description, description: formValues.description,
code: DEFAULT_CODE,
}); });
if (!isDefined(newServerlessFunction?.data)) { if (!isDefined(newServerlessFunction?.data)) {

View File

@@ -1,4 +1,3 @@
import { DEFAULT_CODE } from '@/ui/input/code-editor/components/CodeEditor';
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test'; import { within } from '@storybook/test';
import { graphql, http, HttpResponse } from 'msw'; import { graphql, http, HttpResponse } from 'msw';
@@ -38,7 +37,6 @@ const meta: Meta<PageDecoratorArgs> = {
description: '', description: '',
syncStatus: 'READY', syncStatus: 'READY',
runtime: 'nodejs18.x', runtime: 'nodejs18.x',
sourceCodeHash: '42d2734b3dc8a7b45a16803ed7f417bc',
updatedAt: '2024-02-24T10:23:10.673Z', updatedAt: '2024-02-24T10:23:10.673Z',
createdAt: '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) || '', () => { http.get(getImageAbsoluteURI(SOURCE_CODE_FULL_PATH) || '', () => {
return HttpResponse.text(DEFAULT_CODE); return HttpResponse.text('export const handler = () => {}');
}), }),
], ],
}, },

View File

@@ -6,17 +6,21 @@
"builder": "swc", "builder": "swc",
"typeCheck": true, "typeCheck": true,
"assets": [ "assets": [
{
"include": "**/serverless/drivers/constants/base-typescript-project/**",
"outDir": "dist/assets"
},
{ {
"include": "**/serverless/drivers/layers/*/package.json", "include": "**/serverless/drivers/layers/*/package.json",
"outDir": "dist/src" "outDir": "dist/assets"
}, },
{ {
"include": "**/serverless/drivers/layers/*/yarn.lock", "include": "**/serverless/drivers/layers/*/yarn.lock",
"outDir": "dist/src" "outDir": "dist/assets"
}, },
{ {
"include": "**/serverless/drivers/layers/engine/**", "include": "**/serverless/drivers/layers/engine/**",
"outDir": "dist/src" "outDir": "dist/assets"
} }
], ],
"watchAssets": true "watchAssets": true

View File

@@ -0,0 +1,3 @@
import path from 'path';
export const ASSET_PATH = path.resolve(__dirname, `../../assets`);

View File

@@ -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`,
);
}
}

View File

@@ -17,4 +17,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>;
download(params: {
from: { folderPath: string; filename?: string };
to: { folderPath: string; filename?: string };
}): Promise<void>;
} }

View File

@@ -21,10 +21,6 @@ export class LocalDriver implements StorageDriver {
} }
async createFolder(path: string) { async createFolder(path: string) {
if (existsSync(path)) {
return;
}
return fs.mkdir(path, { recursive: true }); return fs.mkdir(path, { recursive: true });
} }
@@ -122,21 +118,24 @@ export class LocalDriver implements StorageDriver {
} }
} }
async copy(params: { async copy(
from: { folderPath: string; filename?: string }; params: {
to: { folderPath: string; filename?: string }; from: { folderPath: string; filename?: string };
}): Promise<void> { to: { folderPath: string; filename?: string };
},
toInMemory = false,
): Promise<void> {
if (!params.from.filename && params.to.filename) { if (!params.from.filename && params.to.filename) {
throw new Error('Cannot copy folder to file'); throw new Error('Cannot copy folder to file');
} }
const fromPath = join( const fromPath = join(
`${this.options.storagePath}/`, this.options.storagePath,
params.from.folderPath, params.from.folderPath,
params.from.filename || '', params.from.filename || '',
); );
const toPath = join( const toPath = join(
`${this.options.storagePath}/`, toInMemory ? '' : this.options.storagePath,
params.to.folderPath, params.to.folderPath,
params.to.filename || '', params.to.filename || '',
); );
@@ -156,4 +155,11 @@ export class LocalDriver implements StorageDriver {
throw error; throw error;
} }
} }
async download(params: {
from: { folderPath: string; filename?: string };
to: { folderPath: string; filename?: string };
}): Promise<void> {
await this.copy(params, true);
}
} }

View File

@@ -1,4 +1,8 @@
import { Readable } from 'stream'; import { Readable } from 'stream';
import fs from 'fs';
import { mkdir } from 'fs/promises';
import { join } from 'path';
import { pipeline } from 'stream/promises';
import { import {
CopyObjectCommand, 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: { async copy(params: {
from: { folderPath: string; filename?: string }; from: { folderPath: string; filename?: string };
to: { 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) { 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) { for (const object of listedObjects.Contents) {
const match = object.Key?.match(/(.*)\/(.*)/); const folderAndFilePaths = this.extractFolderAndFilePaths(object.Key);
if (!isDefined(match)) { if (!isDefined(folderAndFilePaths)) {
continue; continue;
} }
const fromFolderPath = match[1];
const filename = match[2]; const { fromFolderPath, filename } = folderAndFilePaths;
const toFolderPath = fromFolderPath.replace( const toFolderPath = fromFolderPath.replace(
params.from.folderPath, params.from.folderPath,
params.to.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) { async checkBucketExists(args: HeadBucketCommandInput) {
try { try {
await this.s3Client.headBucket(args); await this.s3Client.headBucket(args);

View File

@@ -40,4 +40,11 @@ export class FileStorageService implements StorageDriver {
}): Promise<void> { }): Promise<void> {
return this.driver.copy(params); return this.driver.copy(params);
} }
download(params: {
from: { folderPath: string; filename?: string };
to: { folderPath: string; filename?: string };
}): Promise<void> {
return this.driver.download(params);
}
} }

View File

@@ -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);
}
}

View File

@@ -0,0 +1 @@
!base-typescript-project/**/.env

View File

@@ -0,0 +1,2 @@
# Add your environment variables here.
# Access them in your serverless function code using process.env.VARIABLE

View File

@@ -0,0 +1,7 @@
export const handler = async (
event: object,
context: object,
): Promise<object> => {
// Your code here
return {};
};

View File

@@ -0,0 +1 @@
export const ENV_FILE_NAME = '.env';

View File

@@ -0,0 +1 @@
export const INDEX_FILE_NAME = 'index.ts';

View File

@@ -0,0 +1 @@
export const OUTDIR_FOLDER = 'dist';

View File

@@ -1 +0,0 @@
export const SOURCE_FILE_NAME = 'source.ts';

View File

@@ -1,6 +1,7 @@
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
import { join } from 'path'; import { join } from 'path';
import dotenv from 'dotenv';
import { import {
CreateFunctionCommand, CreateFunctionCommand,
DeleteFunctionCommand, DeleteFunctionCommand,
@@ -18,6 +19,8 @@ import {
waitUntilFunctionUpdatedV2, waitUntilFunctionUpdatedV2,
ListLayerVersionsCommandInput, ListLayerVersionsCommandInput,
ListLayerVersionsCommand, ListLayerVersionsCommand,
UpdateFunctionConfigurationCommand,
UpdateFunctionConfigurationCommandInput,
} 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';
@@ -36,7 +39,6 @@ import {
NODE_LAYER_SUBFOLDER, NODE_LAYER_SUBFOLDER,
} from 'src/engine/core-modules/serverless/drivers/utils/lambda-build-directory-manager'; } 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 { 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 { 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 { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
import { import {
@@ -46,6 +48,11 @@ import {
import { isDefined } from 'src/utils/is-defined'; import { isDefined } from 'src/utils/is-defined';
import { COMMON_LAYER_NAME } from 'src/engine/core-modules/serverless/drivers/constants/common-layer-name'; import { COMMON_LAYER_NAME } from 'src/engine/core-modules/serverless/drivers/constants/common-layer-name';
import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies'; import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies';
import { 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 { export interface LambdaDriverOptions extends LambdaClientConfig {
fileStorageService: FileStorageService; fileStorageService: FileStorageService;
@@ -53,16 +60,12 @@ export interface LambdaDriverOptions extends LambdaClientConfig {
role: string; role: string;
} }
export class LambdaDriver export class LambdaDriver implements ServerlessDriver {
extends BaseServerlessDriver
implements ServerlessDriver
{
private readonly lambdaClient: Lambda; private readonly lambdaClient: Lambda;
private readonly lambdaRole: string; private readonly lambdaRole: string;
private readonly fileStorageService: FileStorageService; private readonly fileStorageService: FileStorageService;
constructor(options: LambdaDriverOptions) { constructor(options: LambdaDriverOptions) {
super();
const { region, role, ...lambdaOptions } = options; const { region, role, ...lambdaOptions } = options;
this.lambdaClient = new Lambda({ ...lambdaOptions, region }); this.lambdaClient = new Lambda({ ...lambdaOptions, region });
@@ -165,24 +168,50 @@ export class LambdaDriver
} }
} }
async build(serverlessFunction: ServerlessFunctionEntity) { private getInMemoryServerlessFunctionFolderPath = (
const javascriptCode = await this.getCompiledCode( 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, 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(); await createZipFile(
join(inMemoryServerlessFunctionFolderPath, OUTDIR_FOLDER),
const {
sourceTemporaryDir,
lambdaZipPath, 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( const functionExists = await this.checkFunctionExists(
serverlessFunction.id, serverlessFunction.id,
@@ -198,8 +227,11 @@ export class LambdaDriver
ZipFile: await fs.readFile(lambdaZipPath), ZipFile: await fs.readFile(lambdaZipPath),
}, },
FunctionName: serverlessFunction.id, FunctionName: serverlessFunction.id,
Handler: lambdaHandler, Handler: 'src/index.handler',
Layers: [layerArn], Layers: [layerArn],
Environment: {
Variables: envVariables,
},
Role: this.lambdaRole, Role: this.lambdaRole,
Runtime: serverlessFunction.runtime, Runtime: serverlessFunction.runtime,
Description: 'Lambda function to run user script', Description: 'Lambda function to run user script',
@@ -210,23 +242,37 @@ export class LambdaDriver
await this.lambdaClient.send(command); await this.lambdaClient.send(command);
} else { } else {
const params: UpdateFunctionCodeCommandInput = { const updateCodeParams: UpdateFunctionCodeCommandInput = {
ZipFile: await fs.readFile(lambdaZipPath), ZipFile: await fs.readFile(lambdaZipPath),
FunctionName: serverlessFunction.id, 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 this.waitFunctionUpdates(serverlessFunction.id, 10);
await lambdaBuildDirectoryManager.clean();
} }
async publish(serverlessFunction: ServerlessFunctionEntity) { async publish(serverlessFunction: ServerlessFunctionEntity) {
await this.build(serverlessFunction); await this.build(serverlessFunction, 'draft');
const params: PublishVersionCommandInput = { const params: PublishVersionCommandInput = {
FunctionName: serverlessFunction.id, FunctionName: serverlessFunction.id,
}; };
@@ -240,6 +286,20 @@ export class LambdaDriver
throw new Error('New published version is undefined'); 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; return newVersion;
} }

View File

@@ -1,10 +1,9 @@
import { fork } from 'child_process'; import { fork } from 'child_process';
import { promises as fs, existsSync } from 'fs'; import { promises as fs } from 'fs';
import { join } from 'path'; 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 { import {
ServerlessDriver, ServerlessDriver,
ServerlessExecuteError, ServerlessExecuteError,
@@ -12,35 +11,36 @@ import {
} from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface'; } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { 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 { 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 { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import {
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 { COMMON_LAYER_NAME } from 'src/engine/core-modules/serverless/drivers/constants/common-layer-name';
import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies'; import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies';
import { SERVERLESS_TMPDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/serverless-tmpdir-folder'; import { SERVERLESS_TMPDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/serverless-tmpdir-folder';
import { compileTypescript } from 'src/engine/core-modules/serverless/drivers/utils/compile-typescript';
import { 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 { export interface LocalDriverOptions {
fileStorageService: FileStorageService; fileStorageService: FileStorageService;
} }
export class LocalDriver export class LocalDriver implements ServerlessDriver {
extends BaseServerlessDriver
implements ServerlessDriver
{
private readonly fileStorageService: FileStorageService; private readonly fileStorageService: FileStorageService;
constructor(options: LocalDriverOptions) { constructor(options: LocalDriverOptions) {
super();
this.fileStorageService = options.fileStorageService; this.fileStorageService = options.fileStorageService;
} }
private getInMemoryServerlessFunctionFolderPath = (
serverlessFunction: ServerlessFunctionEntity,
version: string,
) => {
return join(SERVERLESS_TMPDIR_FOLDER, serverlessFunction.id, version);
};
private getInMemoryLayerFolderPath = (version: number) => { private getInMemoryLayerFolderPath = (version: number) => {
return join(SERVERLESS_TMPDIR_FOLDER, COMMON_LAYER_NAME, `${version}`); return join(SERVERLESS_TMPDIR_FOLDER, COMMON_LAYER_NAME, `${version}`);
}; };
@@ -49,88 +49,54 @@ export class LocalDriver
const inMemoryLastVersionLayerFolderPath = const inMemoryLastVersionLayerFolderPath =
this.getInMemoryLayerFolderPath(version); this.getInMemoryLayerFolderPath(version);
if (existsSync(inMemoryLastVersionLayerFolderPath)) { try {
return; await fs.access(inMemoryLastVersionLayerFolderPath);
} catch (e) {
await copyAndBuildDependencies(inMemoryLastVersionLayerFolderPath);
} }
await copyAndBuildDependencies(inMemoryLastVersionLayerFolderPath);
} }
async delete() {} async delete() {}
async build(serverlessFunction: ServerlessFunctionEntity) { async build(serverlessFunction: ServerlessFunctionEntity, version: string) {
await this.createLayerIfNotExists(serverlessFunction.layerVersion); const computedVersion =
const javascriptCode = await this.getCompiledCode( version === 'latest' ? serverlessFunction.latestVersion : version;
serverlessFunction,
this.fileStorageService,
);
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); await this.createLayerIfNotExists(serverlessFunction.layerVersion);
const startTime = Date.now(); const inMemoryServerlessFunctionFolderPath =
let fileContent = ''; this.getInMemoryServerlessFunctionFolderPath(
serverlessFunction,
computedVersion,
);
try { const folderPath = getServerlessFolder({
const fileStream = await this.fileStorageService.read({ serverlessFunction,
folderPath: getServerlessFolder({ version,
serverlessFunction, });
version,
}),
filename: BUILD_FILE_NAME,
});
fileContent = await readFileContent(fileStream); await this.fileStorageService.download({
} catch (error) { from: { folderPath },
if (error.code === FileStorageExceptionCode.FILE_NOT_FOUND) { to: { folderPath: inMemoryServerlessFunctionFolderPath },
throw new ServerlessFunctionException( });
`Function Version '${version}' does not exist`,
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
);
}
throw error;
}
const tmpFolderPath = join(SERVERLESS_TMPDIR_FOLDER, v4()); compileTypescript(inMemoryServerlessFunctionFolderPath);
const tmpFilePath = join(tmpFolderPath, 'index.js'); const envFileContent = await fs.readFile(
join(inMemoryServerlessFunctionFolderPath, ENV_FILE_NAME),
await fs.symlink(
this.getInMemoryLayerFolderPath(serverlessFunction.layerVersion),
tmpFolderPath,
'dir',
); );
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) => { process.on('message', async (message) => {
const { event, context } = message; const { event, context } = message;
try { try {
const result = await handler(event, context); const result = await index_1.handler(event, context);
process.send(result); process.send(result);
} catch (error) { } catch (error) {
process.send({ 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) => { try {
const child = fork(tmpFilePath, { silent: true }); 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) => { async publish(serverlessFunction: ServerlessFunctionEntity) {
const duration = Date.now() - startTime; 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({ resolve({
data: null, data: null,
duration, duration,
error: message,
status: ServerlessFunctionExecutionStatus.ERROR, status: ServerlessFunctionExecutionStatus.ERROR,
error: {
errorType,
errorMessage,
stackTrace: stackTrace,
},
}); });
} else { child.kill();
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();
fs.unlink(tmpFilePath).catch(console.error);
});
child.on('error', (error) => { child.on('error', (error) => {
reject(error); reject(error);
child.kill(); child.kill();
fs.unlink(tmpFilePath).catch(console.error); });
});
child.on('exit', (code) => { child.on('exit', (code) => {
if (code && code !== 0) { if (code && code !== 0) {
reject(new Error(`Child process exited with code ${code}`)); reject(new Error(`Child process exited with code ${code}`));
fs.unlink(tmpFilePath).catch(console.error); }
} });
});
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,
};
}
} }
} }

View File

@@ -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 = { const options: ts.CompilerOptions = {
module: ts.ModuleKind.CommonJS, module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2017, target: ts.ScriptTarget.ES2017,
@@ -8,12 +13,9 @@ export const compileTypescript = (typescriptCode: string): string => {
esModuleInterop: true, esModuleInterop: true,
resolveJsonModule: true, resolveJsonModule: true,
allowSyntheticDefaultImports: true, allowSyntheticDefaultImports: true,
outDir: join(folderPath, OUTDIR_FOLDER, 'src'),
types: ['node'], types: ['node'],
}; };
const result = ts.transpileModule(typescriptCode, { createProgram([join(folderPath, 'src', INDEX_FILE_NAME)], options).emit();
compilerOptions: options,
});
return result.outputText;
}; };

View File

@@ -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);
})();

View File

@@ -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 { 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 = ( export const getLayerDependenciesDirName = (
version: 'latest' | 'engine' | number, version: 'latest' | 'engine' | number,
): string => { ): string => {
const formattedVersion = version === 'latest' ? LAST_LAYER_VERSION : version; 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);
}; };

View File

@@ -10,14 +10,12 @@ export const NODE_LAYER_SUBFOLDER = 'nodejs';
const TEMPORARY_LAMBDA_FOLDER = 'lambda-build'; const TEMPORARY_LAMBDA_FOLDER = 'lambda-build';
const TEMPORARY_LAMBDA_SOURCE_FOLDER = 'src'; const TEMPORARY_LAMBDA_SOURCE_FOLDER = 'src';
const LAMBDA_ZIP_FILE_NAME = 'lambda.zip'; const LAMBDA_ZIP_FILE_NAME = 'lambda.zip';
const LAMBDA_ENTRY_FILE_NAME = 'index.js';
export class LambdaBuildDirectoryManager { export class LambdaBuildDirectoryManager {
private temporaryDir = join( private temporaryDir = join(
SERVERLESS_TMPDIR_FOLDER, SERVERLESS_TMPDIR_FOLDER,
`${TEMPORARY_LAMBDA_FOLDER}-${v4()}`, `${TEMPORARY_LAMBDA_FOLDER}-${v4()}`,
); );
private lambdaHandler = `${LAMBDA_ENTRY_FILE_NAME.split('.')[0]}.handler`;
async init() { async init() {
const sourceTemporaryDir = join( const sourceTemporaryDir = join(
@@ -25,15 +23,12 @@ export class LambdaBuildDirectoryManager {
TEMPORARY_LAMBDA_SOURCE_FOLDER, TEMPORARY_LAMBDA_SOURCE_FOLDER,
); );
const lambdaZipPath = join(this.temporaryDir, LAMBDA_ZIP_FILE_NAME); const lambdaZipPath = join(this.temporaryDir, LAMBDA_ZIP_FILE_NAME);
const javascriptFilePath = join(sourceTemporaryDir, LAMBDA_ENTRY_FILE_NAME);
await fs.mkdir(sourceTemporaryDir, { recursive: true }); await fs.mkdir(sourceTemporaryDir, { recursive: true });
return { return {
sourceTemporaryDir, sourceTemporaryDir,
lambdaZipPath, lambdaZipPath,
javascriptFilePath,
lambdaHandler: this.lambdaHandler,
}; };
} }

View File

@@ -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;
}

View File

@@ -1,13 +1,16 @@
import { Field, InputType } from '@nestjs/graphql'; import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsString } from 'class-validator'; import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { CreateServerlessFunctionFromFileInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function-from-file.input';
@InputType() @InputType()
export class CreateServerlessFunctionInput extends CreateServerlessFunctionFromFileInput { export class CreateServerlessFunctionInput {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@Field() @Field()
code: string; name: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
description?: string;
} }

View File

@@ -50,11 +50,6 @@ export class ServerlessFunctionDTO {
@Field({ nullable: true }) @Field({ nullable: true })
description: string; description: string;
@IsString()
@IsNotEmpty()
@Field()
sourceCodeHash: string;
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@Field() @Field()

View File

@@ -1,6 +1,7 @@
import { Field, InputType } from '@nestjs/graphql'; 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'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@@ -21,7 +22,7 @@ export class UpdateServerlessFunctionInput {
@Field({ nullable: true }) @Field({ nullable: true })
description?: string; description?: string;
@IsString() @Field(() => graphqlTypeJson)
@Field() @IsObject()
code: string; code: JSON;
} }

View File

@@ -29,9 +29,6 @@ export class ServerlessFunctionEntity {
@Column({ nullable: true }) @Column({ nullable: true })
latestVersion: string; latestVersion: string;
@Column({ nullable: false })
sourceCodeHash: string;
@Column({ nullable: false, default: ServerlessFunctionRuntime.NODE18 }) @Column({ nullable: false, default: ServerlessFunctionRuntime.NODE18 })
runtime: ServerlessFunctionRuntime; runtime: ServerlessFunctionRuntime;

View File

@@ -3,7 +3,6 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import graphqlTypeJson from 'graphql-type-json'; import graphqlTypeJson from 'graphql-type-json';
import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; 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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; 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 { 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 { 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'; 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( async getServerlessFunctionSourceCode(
@Args('input') input: GetServerlessFunctionSourceCodeInput, @Args('input') input: GetServerlessFunctionSourceCodeInput,
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
@@ -130,28 +128,6 @@ export class ServerlessFunctionResolver {
name: input.name, name: input.name,
description: input.description, 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, workspaceId,
); );
} catch (error) { } catch (error) {

View File

@@ -1,9 +1,11 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { basename, dirname, 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 { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import deepEqual from 'deep-equal';
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception'; 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'; 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 { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content'; 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 { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service';
import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
import { 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 { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
import { import {
ServerlessFunctionEntity, ServerlessFunctionEntity,
@@ -25,10 +26,12 @@ 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 { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils';
import { isDefined } from 'src/utils/is-defined'; import { isDefined } from 'src/utils/is-defined';
import { getLastLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-layer-dependencies'; 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 { 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() @Injectable()
export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFunctionEntity> { export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFunctionEntity> {
@@ -47,7 +50,7 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
workspaceId: string, workspaceId: string,
id: string, id: string,
version: string, version: string,
) { ): Promise<{ [filePath: string]: string } | undefined> {
const serverlessFunction = await this.serverlessFunctionRepository.findOne({ const serverlessFunction = await this.serverlessFunctionRepository.findOne({
where: { where: {
id, id,
@@ -68,12 +71,20 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
version, version,
}); });
const fileStream = await this.fileStorageService.read({ const indexFileStream = await this.fileStorageService.read({
folderPath, folderPath: join(folderPath, 'src'),
filename: SOURCE_FILE_NAME, 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) { } catch (error) {
if (error.code === FileStorageExceptionCode.FILE_NOT_FOUND) { if (error.code === FileStorageExceptionCode.FILE_NOT_FOUND) {
return; return;
@@ -132,10 +143,7 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
'draft', 'draft',
); );
if ( if (deepEqual(latestCode, draftCode)) {
serverlessFunctionCreateHash(latestCode || '') ===
serverlessFunctionCreateHash(draftCode || '')
) {
throw new Error( throw new Error(
'Cannot publish a new version when code has not changed', 'Cannot publish a new version when code has not changed',
); );
@@ -146,20 +154,6 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
existingServerlessFunction, 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, { await super.updateOne(existingServerlessFunction.id, {
latestVersion: newVersion, latestVersion: newVersion,
}); });
@@ -213,9 +207,6 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
name: serverlessFunctionInput.name, name: serverlessFunctionInput.name,
description: serverlessFunctionInput.description, description: serverlessFunctionInput.description,
syncStatus: ServerlessFunctionSyncStatus.NOT_READY, syncStatus: ServerlessFunctionSyncStatus.NOT_READY,
sourceCodeHash: serverlessFunctionCreateHash(
serverlessFunctionInput.code,
),
}); });
const fileFolder = getServerlessFolder({ const fileFolder = getServerlessFolder({
@@ -223,12 +214,14 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
version: 'draft', version: 'draft',
}); });
await this.fileStorageService.write({ for (const key of Object.keys(serverlessFunctionInput.code)) {
file: serverlessFunctionInput.code, await this.fileStorageService.write({
name: SOURCE_FILE_NAME, file: serverlessFunctionInput.code[key],
mimeType: undefined, name: basename(key),
folder: fileFolder, mimeType: undefined,
}); folder: join(fileFolder, dirname(key)),
});
}
await this.serverlessService.build(existingServerlessFunction, 'draft'); await this.serverlessService.build(existingServerlessFunction, 'draft');
await super.updateOne(existingServerlessFunction.id, { await super.updateOne(existingServerlessFunction.id, {
@@ -259,22 +252,12 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
} }
async createOneServerlessFunction( async createOneServerlessFunction(
serverlessFunctionInput: CreateServerlessFunctionFromFileInput, serverlessFunctionInput: CreateServerlessFunctionInput,
code: FileUpload | string,
workspaceId: string, workspaceId: string,
) { ) {
let typescriptCode: string;
if (typeof code === 'string') {
typescriptCode = code;
} else {
typescriptCode = await readFileContent(code.createReadStream());
}
const createdServerlessFunction = await super.createOne({ const createdServerlessFunction = await super.createOne({
...serverlessFunctionInput, ...serverlessFunctionInput,
workspaceId, workspaceId,
sourceCodeHash: serverlessFunctionCreateHash(typescriptCode),
layerVersion: LAST_LAYER_VERSION, layerVersion: LAST_LAYER_VERSION,
}); });
@@ -283,12 +266,14 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
version: 'draft', version: 'draft',
}); });
await this.fileStorageService.write({ for (const file of await getBaseTypescriptProjectFiles) {
file: typescriptCode, await this.fileStorageService.write({
name: SOURCE_FILE_NAME, file: file.content,
mimeType: undefined, name: file.name,
folder: draftFileFolder, mimeType: undefined,
}); folder: join(draftFileFolder, file.path),
});
}
await this.serverlessService.build(createdServerlessFunction, 'draft'); await this.serverlessService.build(createdServerlessFunction, 'draft');