mirror of
https://github.com/lingble/twenty.git
synced 2025-11-01 05:07:56 +00:00
6653 serverless functions store and use environment variables in serverless function scripts (#7390)
 
This commit is contained in:
@@ -25,15 +25,15 @@ const documents = {
|
|||||||
"\n \n query GetManyRemoteTables($input: FindManyRemoteTablesInput!) {\n findDistantTablesWithStatus(input: $input) {\n ...RemoteTableFields\n }\n }\n": types.GetManyRemoteTablesDocument,
|
"\n \n query 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
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export const SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID =
|
||||||
|
'settings-serverless-function-editor-tab-list';
|
||||||
@@ -5,7 +5,6 @@ export const SERVERLESS_FUNCTION_FRAGMENT = gql`
|
|||||||
id
|
id
|
||||||
name
|
name
|
||||||
description
|
description
|
||||||
sourceCodeHash
|
|
||||||
runtime
|
runtime
|
||||||
syncStatus
|
syncStatus
|
||||||
latestVersion
|
latestVersion
|
||||||
|
|||||||
@@ -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 || '',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
}}
|
||||||
},
|
/>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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 = () => {}');
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
3
packages/twenty-server/src/constants/assets-path.ts
Normal file
3
packages/twenty-server/src/constants/assets-path.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export const ASSET_PATH = path.resolve(__dirname, `../../assets`);
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class RemoveServerlessSourceCodeHashColumn1726240847733
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'RemoveServerlessSourceCodeHashColumn1726240847733';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."serverlessFunction" DROP COLUMN "sourceCodeHash"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."serverlessFunction" ADD "sourceCodeHash" character varying NOT NULL`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,4 +17,8 @@ export interface StorageDriver {
|
|||||||
from: { folderPath: string; filename?: string };
|
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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
|
||||||
import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content';
|
|
||||||
import { SOURCE_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/source-file-name';
|
|
||||||
import { compileTypescript } from 'src/engine/core-modules/serverless/drivers/utils/compile-typescript';
|
|
||||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
|
||||||
import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
|
|
||||||
|
|
||||||
export class BaseServerlessDriver {
|
|
||||||
async getCompiledCode(
|
|
||||||
serverlessFunction: ServerlessFunctionEntity,
|
|
||||||
fileStorageService: FileStorageService,
|
|
||||||
) {
|
|
||||||
const folderPath = getServerlessFolder({
|
|
||||||
serverlessFunction,
|
|
||||||
version: 'draft',
|
|
||||||
});
|
|
||||||
const fileStream = await fileStorageService.read({
|
|
||||||
folderPath,
|
|
||||||
filename: SOURCE_FILE_NAME,
|
|
||||||
});
|
|
||||||
const typescriptCode = await readFileContent(fileStream);
|
|
||||||
|
|
||||||
return compileTypescript(typescriptCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/.gitignore
vendored
Normal file
1
packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
!base-typescript-project/**/.env
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Add your environment variables here.
|
||||||
|
# Access them in your serverless function code using process.env.VARIABLE
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export const handler = async (
|
||||||
|
event: object,
|
||||||
|
context: object,
|
||||||
|
): Promise<object> => {
|
||||||
|
// Your code here
|
||||||
|
return {};
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const ENV_FILE_NAME = '.env';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const INDEX_FILE_NAME = 'index.ts';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const OUTDIR_FOLDER = 'dist';
|
||||||
@@ -1 +0,0 @@
|
|||||||
export const SOURCE_FILE_NAME = 'source.ts';
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as fs from 'fs/promises';
|
import * 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import path, { join } from 'path';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
|
||||||
|
import { ASSET_PATH } from 'src/constants/assets-path';
|
||||||
|
|
||||||
|
type File = { name: string; path: string; content: Buffer };
|
||||||
|
|
||||||
|
const getAllFiles = async (
|
||||||
|
rootDir: string,
|
||||||
|
dir: string = rootDir,
|
||||||
|
files: File[] = [],
|
||||||
|
): Promise<File[]> => {
|
||||||
|
const dirEntries = await fs.readdir(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of dirEntries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
return getAllFiles(rootDir, fullPath, files);
|
||||||
|
} else {
|
||||||
|
files.push({
|
||||||
|
path: path.relative(rootDir, dir),
|
||||||
|
name: entry.name,
|
||||||
|
content: await fs.readFile(fullPath),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBaseTypescriptProjectFiles = (async () => {
|
||||||
|
const baseTypescriptProjectPath = join(
|
||||||
|
ASSET_PATH,
|
||||||
|
`engine/core-modules/serverless/drivers/constants/base-typescript-project`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return await getAllFiles(baseTypescriptProjectPath);
|
||||||
|
})();
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
import path from 'path';
|
import path, { join } from 'path';
|
||||||
|
|
||||||
import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version';
|
import { 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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql';
|
|
||||||
|
|
||||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
|
||||||
|
|
||||||
@InputType()
|
|
||||||
export class CreateServerlessFunctionFromFileInput {
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@Field()
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
|
||||||
@Field({ nullable: true })
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql';
|
import { 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user