Migrate serverless function service to v2 (#17285)
# Introduction In this PR we're migrating the serverless function service that was using the SF repo directly to the v2 build and runner. The whole serverless engine now deals with flat entities only ## Resolvers Refactored the resolvers ( serverlessFunction, route, database and cron trigger) : - return types to `dto` - Standardized the flat to dto transpilation within the resolvers - Find and findMany passing by the cached data ## Services Refactored the services ( serverlessFunction, route, database and cron trigger) : - return type to be `flat` - always calling v2 and computing cache ## New additional caches - application variables ( cf https://github.com/twentyhq/core-team-issues/issues/2116 ) - serverless function layer ## What to test: - CRUD ( database trigger ✅ , route trigger, cron trigger, serverless function through workflows ✅ ) - Duplicating a workflow with a serverless function code node ✅ ## Concerns We need to implement the cron that will hard delete soft deleted s3 serverless functions, not in this PR though ( cf https://github.com/twentyhq/twenty/pull/17285#discussion_r2709168570 and https://github.com/twentyhq/core-team-issues/issues/2118 )
This commit is contained in:
+65
-26
@@ -1,9 +1,8 @@
|
||||
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
@@ -15,9 +14,15 @@ import { CreateCronTriggerInput } from 'src/engine/metadata-modules/cron-trigger
|
||||
import { CronTriggerIdInput } from 'src/engine/metadata-modules/cron-trigger/dtos/cron-trigger-id.input';
|
||||
import { CronTriggerDTO } from 'src/engine/metadata-modules/cron-trigger/dtos/cron-trigger.dto';
|
||||
import { UpdateCronTriggerInput } from 'src/engine/metadata-modules/cron-trigger/dtos/update-cron-trigger.input';
|
||||
import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
|
||||
import {
|
||||
CronTriggerException,
|
||||
CronTriggerExceptionCode,
|
||||
} from 'src/engine/metadata-modules/cron-trigger/exceptions/cron-trigger.exception';
|
||||
import { CronTriggerV2Service } from 'src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service';
|
||||
import { cronTriggerGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/cron-trigger/utils/cron-trigger-graphql-api-exception-handler.util';
|
||||
import { fromFlatCronTriggerToCronTriggerDto } from 'src/engine/metadata-modules/cron-trigger/utils/from-flat-cron-trigger-to-cron-trigger-dto.util';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
@@ -29,37 +34,59 @@ import { cronTriggerGraphQLApiExceptionHandler } from 'src/engine/metadata-modul
|
||||
export class CronTriggerResolver {
|
||||
constructor(
|
||||
private readonly cronTriggerV2Service: CronTriggerV2Service,
|
||||
@InjectRepository(CronTriggerEntity)
|
||||
private readonly cronTriggerRepository: Repository<CronTriggerEntity>,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
@Query(() => CronTriggerDTO)
|
||||
async findOneCronTrigger(
|
||||
@Args('input') { id }: CronTriggerIdInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<CronTriggerDTO> {
|
||||
try {
|
||||
return await this.cronTriggerRepository.findOneOrFail({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
const { flatCronTriggerMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatCronTriggerMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatCronTrigger = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: flatCronTriggerMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(flatCronTrigger)) {
|
||||
throw new CronTriggerException(
|
||||
`Cron trigger with id ${id} not found`,
|
||||
CronTriggerExceptionCode.CRON_TRIGGER_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return fromFlatCronTriggerToCronTriggerDto(flatCronTrigger);
|
||||
} catch (error) {
|
||||
cronTriggerGraphQLApiExceptionHandler(error);
|
||||
return cronTriggerGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@Query(() => [CronTriggerDTO])
|
||||
async findManyCronTriggers(
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<CronTriggerDTO[]> {
|
||||
try {
|
||||
return await this.cronTriggerRepository.find({
|
||||
where: { workspaceId },
|
||||
});
|
||||
const { flatCronTriggerMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatCronTriggerMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return Object.values(flatCronTriggerMaps.byId)
|
||||
.filter(isDefined)
|
||||
.map(fromFlatCronTriggerToCronTriggerDto);
|
||||
} catch (error) {
|
||||
cronTriggerGraphQLApiExceptionHandler(error);
|
||||
return cronTriggerGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,14 +94,16 @@ export class CronTriggerResolver {
|
||||
async deleteOneCronTrigger(
|
||||
@Args('input') input: CronTriggerIdInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<CronTriggerDTO> {
|
||||
try {
|
||||
return await this.cronTriggerV2Service.destroyOne({
|
||||
const flatCronTrigger = await this.cronTriggerV2Service.destroyOne({
|
||||
destroyCronTriggerInput: input,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return fromFlatCronTriggerToCronTriggerDto(flatCronTrigger);
|
||||
} catch (error) {
|
||||
cronTriggerGraphQLApiExceptionHandler(error);
|
||||
return cronTriggerGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,11 +112,16 @@ export class CronTriggerResolver {
|
||||
@Args('input')
|
||||
input: UpdateCronTriggerInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<CronTriggerDTO> {
|
||||
try {
|
||||
return await this.cronTriggerV2Service.updateOne(input, workspaceId);
|
||||
const flatCronTrigger = await this.cronTriggerV2Service.updateOne(
|
||||
input,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return fromFlatCronTriggerToCronTriggerDto(flatCronTrigger);
|
||||
} catch (error) {
|
||||
cronTriggerGraphQLApiExceptionHandler(error);
|
||||
return cronTriggerGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,11 +130,16 @@ export class CronTriggerResolver {
|
||||
@Args('input')
|
||||
input: CreateCronTriggerInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<CronTriggerDTO> {
|
||||
try {
|
||||
return await this.cronTriggerV2Service.createOne(input, workspaceId);
|
||||
const flatCronTrigger = await this.cronTriggerV2Service.createOne(
|
||||
input,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return fromFlatCronTriggerToCronTriggerDto(flatCronTrigger);
|
||||
} catch (error) {
|
||||
cronTriggerGraphQLApiExceptionHandler(error);
|
||||
return cronTriggerGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -10,7 +10,7 @@ import {
|
||||
CronTriggerException,
|
||||
CronTriggerExceptionCode,
|
||||
} from 'src/engine/metadata-modules/cron-trigger/exceptions/cron-trigger.exception';
|
||||
import { FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
|
||||
import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
|
||||
import { fromCreateCronTriggerInputToFlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/utils/from-create-cron-trigger-input-to-flat-cron-trigger.util';
|
||||
import { fromUpdateCronTriggerInputToFlatCronTriggerToUpdateOrThrow } from 'src/engine/metadata-modules/cron-trigger/utils/from-update-cron-trigger-input-to-flat-cron-trigger-to-update-or-throw.util';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
@@ -34,7 +34,7 @@ export class CronTriggerV2Service {
|
||||
* when interacting with another application than workspace custom one
|
||||
* */
|
||||
applicationId?: string,
|
||||
) {
|
||||
): Promise<FlatCronTrigger> {
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{
|
||||
@@ -90,7 +90,7 @@ export class CronTriggerV2Service {
|
||||
async updateOne(
|
||||
cronTriggerInput: UpdateCronTriggerInput,
|
||||
workspaceId: string,
|
||||
) {
|
||||
): Promise<FlatCronTrigger> {
|
||||
const { flatCronTriggerMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import {
|
||||
CronTriggerExceptionCode,
|
||||
} from 'src/engine/metadata-modules/cron-trigger/exceptions/cron-trigger.exception';
|
||||
|
||||
export const cronTriggerGraphQLApiExceptionHandler = (error: Error): void => {
|
||||
export const cronTriggerGraphQLApiExceptionHandler = (error: Error): never => {
|
||||
if (error instanceof CronTriggerException) {
|
||||
switch (error.code) {
|
||||
case CronTriggerExceptionCode.CRON_TRIGGER_NOT_FOUND:
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { type CronTriggerDTO } from 'src/engine/metadata-modules/cron-trigger/dtos/cron-trigger.dto';
|
||||
import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
|
||||
|
||||
export const fromFlatCronTriggerToCronTriggerDto = (
|
||||
flatCronTrigger: FlatCronTrigger,
|
||||
): CronTriggerDTO => ({
|
||||
id: flatCronTrigger.id,
|
||||
settings: flatCronTrigger.settings,
|
||||
createdAt: new Date(flatCronTrigger.createdAt),
|
||||
updatedAt: new Date(flatCronTrigger.updatedAt),
|
||||
});
|
||||
+71
-33
@@ -1,9 +1,8 @@
|
||||
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
@@ -15,9 +14,15 @@ import { CreateDatabaseEventTriggerInput } from 'src/engine/metadata-modules/dat
|
||||
import { DatabaseEventTriggerIdInput } from 'src/engine/metadata-modules/database-event-trigger/dtos/database-event-trigger-id.input';
|
||||
import { DatabaseEventTriggerDTO } from 'src/engine/metadata-modules/database-event-trigger/dtos/database-event-trigger.dto';
|
||||
import { UpdateDatabaseEventTriggerInput } from 'src/engine/metadata-modules/database-event-trigger/dtos/update-database-event-trigger.input';
|
||||
import { DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
|
||||
import {
|
||||
DatabaseEventTriggerException,
|
||||
DatabaseEventTriggerExceptionCode,
|
||||
} from 'src/engine/metadata-modules/database-event-trigger/exceptions/database-event-trigger.exception';
|
||||
import { DatabaseEventTriggerV2Service } from 'src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service';
|
||||
import { databaseEventTriggerGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/database-event-trigger/utils/database-event-trigger-graphql-api-exception-handler.utils';
|
||||
import { fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto } from 'src/engine/metadata-modules/database-event-trigger/utils/from-flat-database-event-trigger-to-database-event-trigger-dto.util';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
@@ -29,37 +34,61 @@ import { databaseEventTriggerGraphQLApiExceptionHandler } from 'src/engine/metad
|
||||
export class DatabaseEventTriggerResolver {
|
||||
constructor(
|
||||
private readonly databaseEventTriggerV2Service: DatabaseEventTriggerV2Service,
|
||||
@InjectRepository(DatabaseEventTriggerEntity)
|
||||
private readonly databaseEventTriggerRepository: Repository<DatabaseEventTriggerEntity>,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
@Query(() => DatabaseEventTriggerDTO)
|
||||
async findOneDatabaseEventTrigger(
|
||||
@Args('input') { id }: DatabaseEventTriggerIdInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<DatabaseEventTriggerDTO> {
|
||||
try {
|
||||
return await this.databaseEventTriggerRepository.findOneOrFail({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
const { flatDatabaseEventTriggerMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatDatabaseEventTriggerMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatDatabaseEventTrigger = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: flatDatabaseEventTriggerMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(flatDatabaseEventTrigger)) {
|
||||
throw new DatabaseEventTriggerException(
|
||||
`Database event trigger with id ${id} not found`,
|
||||
DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto(
|
||||
flatDatabaseEventTrigger,
|
||||
);
|
||||
} catch (error) {
|
||||
databaseEventTriggerGraphQLApiExceptionHandler(error);
|
||||
return databaseEventTriggerGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@Query(() => [DatabaseEventTriggerDTO])
|
||||
async findManyDatabaseEventTriggers(
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<DatabaseEventTriggerDTO[]> {
|
||||
try {
|
||||
return await this.databaseEventTriggerRepository.find({
|
||||
where: { workspaceId },
|
||||
});
|
||||
const { flatDatabaseEventTriggerMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatDatabaseEventTriggerMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return Object.values(flatDatabaseEventTriggerMaps.byId)
|
||||
.filter(isDefined)
|
||||
.map(fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto);
|
||||
} catch (error) {
|
||||
databaseEventTriggerGraphQLApiExceptionHandler(error);
|
||||
return databaseEventTriggerGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,14 +96,19 @@ export class DatabaseEventTriggerResolver {
|
||||
async deleteOneDatabaseEventTrigger(
|
||||
@Args('input') input: DatabaseEventTriggerIdInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<DatabaseEventTriggerDTO> {
|
||||
try {
|
||||
return await this.databaseEventTriggerV2Service.destroyOne({
|
||||
destroyDatabaseEventTriggerInput: input,
|
||||
workspaceId,
|
||||
});
|
||||
const flatDatabaseEventTrigger =
|
||||
await this.databaseEventTriggerV2Service.destroyOne({
|
||||
destroyDatabaseEventTriggerInput: input,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto(
|
||||
flatDatabaseEventTrigger,
|
||||
);
|
||||
} catch (error) {
|
||||
databaseEventTriggerGraphQLApiExceptionHandler(error);
|
||||
return databaseEventTriggerGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,14 +117,16 @@ export class DatabaseEventTriggerResolver {
|
||||
@Args('input')
|
||||
input: UpdateDatabaseEventTriggerInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<DatabaseEventTriggerDTO> {
|
||||
try {
|
||||
return await this.databaseEventTriggerV2Service.updateOne(
|
||||
input,
|
||||
workspaceId,
|
||||
const flatDatabaseEventTrigger =
|
||||
await this.databaseEventTriggerV2Service.updateOne(input, workspaceId);
|
||||
|
||||
return fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto(
|
||||
flatDatabaseEventTrigger,
|
||||
);
|
||||
} catch (error) {
|
||||
databaseEventTriggerGraphQLApiExceptionHandler(error);
|
||||
return databaseEventTriggerGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,14 +135,16 @@ export class DatabaseEventTriggerResolver {
|
||||
@Args('input')
|
||||
input: CreateDatabaseEventTriggerInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<DatabaseEventTriggerDTO> {
|
||||
try {
|
||||
return await this.databaseEventTriggerV2Service.createOne(
|
||||
input,
|
||||
workspaceId,
|
||||
const flatDatabaseEventTrigger =
|
||||
await this.databaseEventTriggerV2Service.createOne(input, workspaceId);
|
||||
|
||||
return fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto(
|
||||
flatDatabaseEventTrigger,
|
||||
);
|
||||
} catch (error) {
|
||||
databaseEventTriggerGraphQLApiExceptionHandler(error);
|
||||
return databaseEventTriggerGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -10,7 +10,7 @@ import {
|
||||
DatabaseEventTriggerException,
|
||||
DatabaseEventTriggerExceptionCode,
|
||||
} from 'src/engine/metadata-modules/database-event-trigger/exceptions/database-event-trigger.exception';
|
||||
import { FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
|
||||
import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
|
||||
import { fromCreateDatabaseEventTriggerInputToFlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/utils/from-create-database-event-trigger-input-to-flat-database-event-trigger.util';
|
||||
import { fromUpdateDatabaseEventTriggerInputToFlatDatabaseEventTriggerToUpdateOrThrow } from 'src/engine/metadata-modules/database-event-trigger/utils/from-update-database-event-trigger-input-to-flat-database-event-trigger-to-update-or-throw.util';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
@@ -34,7 +34,7 @@ export class DatabaseEventTriggerV2Service {
|
||||
* when interacting with another application than workspace custom one
|
||||
* */
|
||||
applicationId?: string,
|
||||
) {
|
||||
): Promise<FlatDatabaseEventTrigger> {
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{
|
||||
@@ -92,7 +92,7 @@ export class DatabaseEventTriggerV2Service {
|
||||
async updateOne(
|
||||
databaseEventTriggerInput: UpdateDatabaseEventTriggerInput,
|
||||
workspaceId: string,
|
||||
) {
|
||||
): Promise<FlatDatabaseEventTrigger> {
|
||||
const { flatDatabaseEventTriggerMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import {
|
||||
|
||||
export const databaseEventTriggerGraphQLApiExceptionHandler = (
|
||||
error: Error,
|
||||
): void => {
|
||||
): never => {
|
||||
if (error instanceof DatabaseEventTriggerException) {
|
||||
switch (error.code) {
|
||||
case DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_NOT_FOUND:
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { type DatabaseEventTriggerDTO } from 'src/engine/metadata-modules/database-event-trigger/dtos/database-event-trigger.dto';
|
||||
import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
|
||||
|
||||
export const fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto = (
|
||||
flatDatabaseEventTrigger: FlatDatabaseEventTrigger,
|
||||
): DatabaseEventTriggerDTO => ({
|
||||
id: flatDatabaseEventTrigger.id,
|
||||
settings: flatDatabaseEventTrigger.settings,
|
||||
createdAt: new Date(flatDatabaseEventTrigger.createdAt),
|
||||
updatedAt: new Date(flatDatabaseEventTrigger.updatedAt),
|
||||
});
|
||||
+4
-2
@@ -3,6 +3,7 @@ import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
import { FLAT_CRON_TRIGGER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/cron-trigger/constants/flat-cron-trigger-editable-properties.constant';
|
||||
import { FLAT_DATABASE_EVENT_TRIGGER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/database-event-trigger/constants/flat-database-event-trigger-editable-properties.constant';
|
||||
import { FLAT_AGENT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-agent/constants/flat-agent-editable-properties.constant';
|
||||
import { FLAT_COMMAND_MENU_ITEM_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-command-menu-item/constants/flat-command-menu-item-editable-properties.constant';
|
||||
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
|
||||
import { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant';
|
||||
import { FLAT_FRONT_COMPONENT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-front-component/constants/flat-front-component-editable-properties.constant';
|
||||
@@ -14,7 +15,6 @@ import { FLAT_ROLE_TARGET_EDITABLE_PROPERTIES } from 'src/engine/metadata-module
|
||||
import { FLAT_ROLE_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-role/constants/flat-role-editable-properties.constant';
|
||||
import { FLAT_ROW_LEVEL_PERMISSION_PREDICATE_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-row-level-permission-predicate-group/constants/flat-row-level-permission-predicate-group-editable-properties.constant';
|
||||
import { FLAT_ROW_LEVEL_PERMISSION_PREDICATE_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/constants/flat-row-level-permission-predicate-editable-properties.constant';
|
||||
import { FLAT_COMMAND_MENU_ITEM_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-command-menu-item/constants/flat-command-menu-item-editable-properties.constant';
|
||||
import { FLAT_SKILL_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-skill/constants/flat-skill-editable-properties.constant';
|
||||
import { FLAT_VIEW_FIELD_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-field/constants/flat-view-field-editable-properties.constant';
|
||||
import { FLAT_VIEW_FILTER_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-filter-group/constants/flat-view-filter-group-editable-properties.constant';
|
||||
@@ -81,8 +81,10 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = {
|
||||
(property) => property !== 'code',
|
||||
),
|
||||
'deletedAt',
|
||||
'latestVersion',
|
||||
'publishedVersions',
|
||||
],
|
||||
propertiesToStringify: ['toolInputSchema'],
|
||||
propertiesToStringify: ['toolInputSchema', 'publishedVersions'],
|
||||
},
|
||||
cronTrigger: {
|
||||
propertiesToCompare: [...FLAT_CRON_TRIGGER_EDITABLE_PROPERTIES],
|
||||
|
||||
+65
-26
@@ -1,9 +1,8 @@
|
||||
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
@@ -11,13 +10,19 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
import { CreateRouteTriggerInput } from 'src/engine/metadata-modules/route-trigger/dtos/create-route-trigger.input';
|
||||
import { RouteTriggerIdInput } from 'src/engine/metadata-modules/route-trigger/dtos/route-trigger-id.input';
|
||||
import { RouteTriggerDTO } from 'src/engine/metadata-modules/route-trigger/dtos/route-trigger.dto';
|
||||
import { UpdateRouteTriggerInput } from 'src/engine/metadata-modules/route-trigger/dtos/update-route-trigger.input';
|
||||
import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
|
||||
import {
|
||||
RouteTriggerException,
|
||||
RouteTriggerExceptionCode,
|
||||
} from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception';
|
||||
import { RouteTriggerV2Service } from 'src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service';
|
||||
import { fromFlatRouteTriggerToRouteTriggerDto } from 'src/engine/metadata-modules/route-trigger/utils/from-flat-route-trigger-to-route-trigger-dto.util';
|
||||
import { routeTriggerGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/route-trigger/utils/route-trigger-graphql-api-exception-handler.utils';
|
||||
|
||||
@UseGuards(
|
||||
@@ -33,37 +38,59 @@ import { routeTriggerGraphQLApiExceptionHandler } from 'src/engine/metadata-modu
|
||||
export class RouteTriggerResolver {
|
||||
constructor(
|
||||
private readonly routeV2Service: RouteTriggerV2Service,
|
||||
@InjectRepository(RouteTriggerEntity)
|
||||
private readonly routeTriggerRepository: Repository<RouteTriggerEntity>,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
@Query(() => RouteTriggerDTO)
|
||||
async findOneRouteTrigger(
|
||||
@Args('input') { id }: RouteTriggerIdInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<RouteTriggerDTO> {
|
||||
try {
|
||||
return await this.routeTriggerRepository.findOneOrFail({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
const { flatRouteTriggerMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatRouteTriggerMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatRouteTrigger = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: flatRouteTriggerMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(flatRouteTrigger)) {
|
||||
throw new RouteTriggerException(
|
||||
`Route trigger with id ${id} not found`,
|
||||
RouteTriggerExceptionCode.ROUTE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return fromFlatRouteTriggerToRouteTriggerDto(flatRouteTrigger);
|
||||
} catch (error) {
|
||||
routeTriggerGraphQLApiExceptionHandler(error);
|
||||
return routeTriggerGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@Query(() => [RouteTriggerDTO])
|
||||
async findManyRouteTriggers(
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<RouteTriggerDTO[]> {
|
||||
try {
|
||||
return await this.routeTriggerRepository.find({
|
||||
where: { workspaceId },
|
||||
});
|
||||
const { flatRouteTriggerMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatRouteTriggerMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return Object.values(flatRouteTriggerMaps.byId)
|
||||
.filter(isDefined)
|
||||
.map(fromFlatRouteTriggerToRouteTriggerDto);
|
||||
} catch (error) {
|
||||
routeTriggerGraphQLApiExceptionHandler(error);
|
||||
return routeTriggerGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,14 +98,16 @@ export class RouteTriggerResolver {
|
||||
async deleteOneRouteTrigger(
|
||||
@Args('input') input: RouteTriggerIdInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<RouteTriggerDTO> {
|
||||
try {
|
||||
return await this.routeV2Service.destroyOne({
|
||||
const flatRouteTrigger = await this.routeV2Service.destroyOne({
|
||||
destroyRouteTriggerInput: input,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return fromFlatRouteTriggerToRouteTriggerDto(flatRouteTrigger);
|
||||
} catch (error) {
|
||||
routeTriggerGraphQLApiExceptionHandler(error);
|
||||
return routeTriggerGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,11 +116,16 @@ export class RouteTriggerResolver {
|
||||
@Args('input')
|
||||
input: UpdateRouteTriggerInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<RouteTriggerDTO> {
|
||||
try {
|
||||
return await this.routeV2Service.updateOne(input, workspaceId);
|
||||
const flatRouteTrigger = await this.routeV2Service.updateOne(
|
||||
input,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return fromFlatRouteTriggerToRouteTriggerDto(flatRouteTrigger);
|
||||
} catch (error) {
|
||||
routeTriggerGraphQLApiExceptionHandler(error);
|
||||
return routeTriggerGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,11 +134,16 @@ export class RouteTriggerResolver {
|
||||
@Args('input')
|
||||
input: CreateRouteTriggerInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<RouteTriggerDTO> {
|
||||
try {
|
||||
return await this.routeV2Service.createOne(input, workspaceId);
|
||||
const flatRouteTrigger = await this.routeV2Service.createOne(
|
||||
input,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return fromFlatRouteTriggerToRouteTriggerDto(flatRouteTrigger);
|
||||
} catch (error) {
|
||||
routeTriggerGraphQLApiExceptionHandler(error);
|
||||
return routeTriggerGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -12,7 +12,7 @@ import {
|
||||
RouteTriggerException,
|
||||
RouteTriggerExceptionCode,
|
||||
} from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception';
|
||||
import { FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
|
||||
import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
|
||||
import { fromCreateRouteTriggerInputToFlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/utils/from-create-route-trigger-input-to-flat-route-trigger.util';
|
||||
import { fromUpdateRouteTriggerInputToFlatRouteTriggerToUpdateOrThrow } from 'src/engine/metadata-modules/route-trigger/utils/from-update-route-trigger-input-to-flat-route-trigger-to-update-or-throw.util';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
@@ -34,7 +34,7 @@ export class RouteTriggerV2Service {
|
||||
* when interacting with another application than workspace custom one
|
||||
* */
|
||||
applicationId?: string,
|
||||
) {
|
||||
): Promise<FlatRouteTrigger> {
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{
|
||||
@@ -89,7 +89,7 @@ export class RouteTriggerV2Service {
|
||||
async updateOne(
|
||||
routeTriggerInput: UpdateRouteTriggerInput,
|
||||
workspaceId: string,
|
||||
) {
|
||||
): Promise<FlatRouteTrigger> {
|
||||
const { flatRouteTriggerMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { type RouteTriggerDTO } from 'src/engine/metadata-modules/route-trigger/dtos/route-trigger.dto';
|
||||
import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
|
||||
|
||||
export const fromFlatRouteTriggerToRouteTriggerDto = (
|
||||
flatRouteTrigger: FlatRouteTrigger,
|
||||
): RouteTriggerDTO => ({
|
||||
id: flatRouteTrigger.id,
|
||||
path: flatRouteTrigger.path,
|
||||
isAuthRequired: flatRouteTrigger.isAuthRequired,
|
||||
httpMethod: flatRouteTrigger.httpMethod,
|
||||
forwardedRequestHeaders: flatRouteTrigger.forwardedRequestHeaders,
|
||||
createdAt: new Date(flatRouteTrigger.createdAt),
|
||||
updatedAt: new Date(flatRouteTrigger.updatedAt),
|
||||
});
|
||||
+1
-1
@@ -7,7 +7,7 @@ import {
|
||||
RouteTriggerExceptionCode,
|
||||
} from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception';
|
||||
|
||||
export const routeTriggerGraphQLApiExceptionHandler = (error: Error): void => {
|
||||
export const routeTriggerGraphQLApiExceptionHandler = (error: Error): never => {
|
||||
if (error instanceof RouteTriggerException) {
|
||||
switch (error.code) {
|
||||
case RouteTriggerExceptionCode.ROUTE_NOT_FOUND:
|
||||
|
||||
+12
-2
@@ -5,13 +5,23 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
|
||||
import { ServerlessFunctionLayerResolver } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.resolver';
|
||||
import { ServerlessFunctionLayerService } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service';
|
||||
import { WorkspaceServerlessFunctionLayerMapCacheService } from 'src/engine/metadata-modules/serverless-function-layer/services/workspace-serverless-function-layer-map-cache.service';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PermissionsModule,
|
||||
TypeOrmModule.forFeature([ServerlessFunctionLayerEntity]),
|
||||
WorkspaceCacheModule,
|
||||
],
|
||||
providers: [
|
||||
ServerlessFunctionLayerService,
|
||||
ServerlessFunctionLayerResolver,
|
||||
WorkspaceServerlessFunctionLayerMapCacheService,
|
||||
],
|
||||
exports: [
|
||||
ServerlessFunctionLayerService,
|
||||
WorkspaceServerlessFunctionLayerMapCacheService,
|
||||
],
|
||||
providers: [ServerlessFunctionLayerService, ServerlessFunctionLayerResolver],
|
||||
exports: [ServerlessFunctionLayerService],
|
||||
})
|
||||
export class ServerlessFunctionLayerModule {}
|
||||
|
||||
+22
-2
@@ -10,12 +10,14 @@ import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serve
|
||||
import { CreateServerlessFunctionLayerInput } from 'src/engine/metadata-modules/serverless-function-layer/dtos/create-serverless-function-layer.input';
|
||||
import { getLastCommonLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-common-layer-dependencies';
|
||||
import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
@Injectable()
|
||||
export class ServerlessFunctionLayerService {
|
||||
constructor(
|
||||
@InjectRepository(ServerlessFunctionLayerEntity)
|
||||
private readonly serverlessFunctionLayerRepository: Repository<ServerlessFunctionLayerEntity>,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {}
|
||||
|
||||
async create(
|
||||
@@ -32,12 +34,21 @@ export class ServerlessFunctionLayerService {
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return this.serverlessFunctionLayerRepository.save(serverlessFunctionLayer);
|
||||
const savedLayer = await this.serverlessFunctionLayerRepository.save(
|
||||
serverlessFunctionLayer,
|
||||
);
|
||||
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'serverlessFunctionLayerMaps',
|
||||
]);
|
||||
|
||||
return savedLayer;
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
data: QueryDeepPartialEntity<ServerlessFunctionLayerEntity>,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const checksum = data.yarnLock
|
||||
? serverlessFunctionCreateHash(data.yarnLock as string)
|
||||
@@ -45,7 +56,16 @@ export class ServerlessFunctionLayerService {
|
||||
|
||||
const updateData = { ...data, ...(checksum && { checksum }) };
|
||||
|
||||
return this.serverlessFunctionLayerRepository.update(id, updateData);
|
||||
const result = await this.serverlessFunctionLayerRepository.update(
|
||||
id,
|
||||
updateData,
|
||||
);
|
||||
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'serverlessFunctionLayerMaps',
|
||||
]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async createCommonLayerIfNotExist(workspaceId: string) {
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
|
||||
|
||||
import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
|
||||
import { type ServerlessFunctionLayerCacheMaps } from 'src/engine/metadata-modules/serverless-function-layer/types/serverless-function-layer-cache-maps.type';
|
||||
import { fromServerlessFunctionLayerEntityToFlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/utils/from-serverless-function-layer-entity-to-flat-serverless-function-layer.util';
|
||||
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
|
||||
|
||||
@Injectable()
|
||||
@WorkspaceCache('serverlessFunctionLayerMaps')
|
||||
export class WorkspaceServerlessFunctionLayerMapCacheService extends WorkspaceCacheProvider<ServerlessFunctionLayerCacheMaps> {
|
||||
constructor(
|
||||
@InjectRepository(ServerlessFunctionLayerEntity)
|
||||
private readonly serverlessFunctionLayerRepository: Repository<ServerlessFunctionLayerEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async computeForCache(
|
||||
workspaceId: string,
|
||||
): Promise<ServerlessFunctionLayerCacheMaps> {
|
||||
const serverlessFunctionLayerEntities =
|
||||
await this.serverlessFunctionLayerRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
const serverlessFunctionLayerMaps: ServerlessFunctionLayerCacheMaps = {
|
||||
byId: {},
|
||||
};
|
||||
|
||||
for (const entity of serverlessFunctionLayerEntities) {
|
||||
const flatServerlessFunctionLayer =
|
||||
fromServerlessFunctionLayerEntityToFlatServerlessFunctionLayer(entity);
|
||||
|
||||
serverlessFunctionLayerMaps.byId[flatServerlessFunctionLayer.id] =
|
||||
flatServerlessFunctionLayer;
|
||||
}
|
||||
|
||||
return serverlessFunctionLayerMaps;
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
|
||||
import { type ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
|
||||
|
||||
export type FlatServerlessFunctionLayer =
|
||||
FlatEntityFrom<ServerlessFunctionLayerEntity>;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type';
|
||||
|
||||
export type ServerlessFunctionLayerCacheMaps = {
|
||||
byId: Partial<Record<string, FlatServerlessFunctionLayer>>;
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { type ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
|
||||
import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type';
|
||||
|
||||
export const fromServerlessFunctionLayerEntityToFlatServerlessFunctionLayer = (
|
||||
entity: ServerlessFunctionLayerEntity,
|
||||
): FlatServerlessFunctionLayer => ({
|
||||
id: entity.id,
|
||||
packageJson: entity.packageJson,
|
||||
yarnLock: entity.yarnLock,
|
||||
checksum: entity.checksum,
|
||||
workspaceId: entity.workspaceId,
|
||||
createdAt: entity.createdAt.toISOString(),
|
||||
updatedAt: entity.updatedAt.toISOString(),
|
||||
serverlessFunctionIds: entity.serverlessFunctions?.map((sf) => sf.id) ?? [],
|
||||
});
|
||||
+2
@@ -24,6 +24,7 @@ import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverles
|
||||
import { ServerlessFunctionV2Service } from 'src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service';
|
||||
import { WorkspaceFlatServerlessFunctionMapCacheService } from 'src/engine/metadata-modules/serverless-function/services/workspace-flat-serverless-function-map-cache.service';
|
||||
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
|
||||
@Module({
|
||||
@@ -46,6 +47,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
WorkspaceMigrationModule,
|
||||
ServerlessFunctionLayerModule,
|
||||
SubscriptionsModule,
|
||||
WorkspaceCacheModule,
|
||||
TokenModule,
|
||||
],
|
||||
providers: [
|
||||
|
||||
+198
-50
@@ -1,11 +1,9 @@
|
||||
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import graphqlTypeJson from 'graphql-type-json';
|
||||
import { Repository } from 'typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
@@ -14,19 +12,22 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
|
||||
import { FeatureFlagGuard } from 'src/engine/guards/feature-flag.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
|
||||
import { ExecuteServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/execute-serverless-function.input';
|
||||
import { GetServerlessFunctionSourceCodeInput } from 'src/engine/metadata-modules/serverless-function/dtos/get-serverless-function-source-code.input';
|
||||
import { PublishServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/publish-serverless-function.input';
|
||||
import { ServerlessFunctionExecutionResultDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
||||
import { ServerlessFunctionIdInput } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-id.input';
|
||||
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
|
||||
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||
import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils';
|
||||
import { ServerlessFunctionLogsDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-logs.dto';
|
||||
import { ServerlessFunctionLogsInput } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-logs.input';
|
||||
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
|
||||
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
|
||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||
import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
|
||||
import { findFlatServerlessFunctionOrThrow } from 'src/engine/metadata-modules/serverless-function/utils/find-flat-serverless-function-or-throw.util';
|
||||
import { fromFlatServerlessFunctionToServerlessFunctionDto } from 'src/engine/metadata-modules/serverless-function/utils/from-flat-serverless-function-to-serverless-function-dto.util';
|
||||
import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils';
|
||||
import { SubscriptionChannel } from 'src/engine/subscriptions/enums/subscription-channel.enum';
|
||||
import { SubscriptionService } from 'src/engine/subscriptions/subscription.service';
|
||||
|
||||
@@ -41,40 +42,91 @@ import { SubscriptionService } from 'src/engine/subscriptions/subscription.servi
|
||||
export class ServerlessFunctionResolver {
|
||||
constructor(
|
||||
private readonly serverlessFunctionService: ServerlessFunctionService,
|
||||
@InjectRepository(ServerlessFunctionEntity)
|
||||
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
|
||||
private readonly subscriptionService: SubscriptionService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
@Query(() => ServerlessFunctionDTO)
|
||||
async findOneServerlessFunction(
|
||||
@Args('input') { id }: ServerlessFunctionIdInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<ServerlessFunctionDTO> {
|
||||
try {
|
||||
return await this.serverlessFunctionRepository.findOneOrFail({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['cronTriggers', 'databaseEventTriggers', 'routeTriggers'],
|
||||
const {
|
||||
flatServerlessFunctionMaps,
|
||||
flatCronTriggerMaps,
|
||||
flatDatabaseEventTriggerMaps,
|
||||
flatRouteTriggerMaps,
|
||||
} =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatServerlessFunctionMaps',
|
||||
'flatCronTriggerMaps',
|
||||
'flatDatabaseEventTriggerMaps',
|
||||
'flatRouteTriggerMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const flatServerlessFunction = findFlatServerlessFunctionOrThrow({
|
||||
id,
|
||||
flatServerlessFunctionMaps,
|
||||
});
|
||||
|
||||
return fromFlatServerlessFunctionToServerlessFunctionDto({
|
||||
flatServerlessFunction,
|
||||
flatCronTriggerMaps,
|
||||
flatDatabaseEventTriggerMaps,
|
||||
flatRouteTriggerMaps,
|
||||
});
|
||||
} catch (error) {
|
||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
return serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@Query(() => [ServerlessFunctionDTO])
|
||||
async findManyServerlessFunctions(
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<ServerlessFunctionDTO[]> {
|
||||
try {
|
||||
return this.serverlessFunctionRepository.find({
|
||||
where: { workspaceId },
|
||||
relations: ['cronTriggers', 'databaseEventTriggers', 'routeTriggers'],
|
||||
});
|
||||
const {
|
||||
flatServerlessFunctionMaps,
|
||||
flatCronTriggerMaps,
|
||||
flatDatabaseEventTriggerMaps,
|
||||
flatRouteTriggerMaps,
|
||||
} =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatServerlessFunctionMaps',
|
||||
'flatCronTriggerMaps',
|
||||
'flatDatabaseEventTriggerMaps',
|
||||
'flatRouteTriggerMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
return Object.values(flatServerlessFunctionMaps.byId)
|
||||
.filter(
|
||||
(
|
||||
flatServerlessFunction,
|
||||
): flatServerlessFunction is FlatServerlessFunction =>
|
||||
isDefined(flatServerlessFunction) &&
|
||||
!isDefined(flatServerlessFunction.deletedAt),
|
||||
)
|
||||
.map((flatServerlessFunction) =>
|
||||
fromFlatServerlessFunctionToServerlessFunctionDto({
|
||||
flatServerlessFunction,
|
||||
flatCronTriggerMaps,
|
||||
flatDatabaseEventTriggerMaps,
|
||||
flatRouteTriggerMaps,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
return serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +135,7 @@ export class ServerlessFunctionResolver {
|
||||
try {
|
||||
return await this.serverlessFunctionService.getAvailablePackages(id);
|
||||
} catch (error) {
|
||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
return serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +151,7 @@ export class ServerlessFunctionResolver {
|
||||
input.version,
|
||||
);
|
||||
} catch (error) {
|
||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
return serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,14 +160,38 @@ export class ServerlessFunctionResolver {
|
||||
async deleteOneServerlessFunction(
|
||||
@Args('input') input: ServerlessFunctionIdInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<ServerlessFunctionDTO> {
|
||||
try {
|
||||
return await this.serverlessFunctionService.deleteOneServerlessFunction({
|
||||
id: input.id,
|
||||
workspaceId,
|
||||
const flatServerlessFunction =
|
||||
await this.serverlessFunctionService.deleteOneServerlessFunction({
|
||||
id: input.id,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const {
|
||||
flatCronTriggerMaps,
|
||||
flatDatabaseEventTriggerMaps,
|
||||
flatRouteTriggerMaps,
|
||||
} =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatCronTriggerMaps',
|
||||
'flatDatabaseEventTriggerMaps',
|
||||
'flatRouteTriggerMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatServerlessFunctionToServerlessFunctionDto({
|
||||
flatServerlessFunction,
|
||||
flatCronTriggerMaps,
|
||||
flatDatabaseEventTriggerMaps,
|
||||
flatRouteTriggerMaps,
|
||||
});
|
||||
} catch (error) {
|
||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
return serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,14 +201,38 @@ export class ServerlessFunctionResolver {
|
||||
@Args('input')
|
||||
input: UpdateServerlessFunctionInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<ServerlessFunctionDTO> {
|
||||
try {
|
||||
return await this.serverlessFunctionService.updateOneServerlessFunction(
|
||||
input,
|
||||
workspaceId,
|
||||
);
|
||||
const flatServerlessFunction =
|
||||
await this.serverlessFunctionService.updateOneServerlessFunction(
|
||||
input,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const {
|
||||
flatCronTriggerMaps,
|
||||
flatDatabaseEventTriggerMaps,
|
||||
flatRouteTriggerMaps,
|
||||
} =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatCronTriggerMaps',
|
||||
'flatDatabaseEventTriggerMaps',
|
||||
'flatRouteTriggerMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatServerlessFunctionToServerlessFunctionDto({
|
||||
flatServerlessFunction,
|
||||
flatCronTriggerMaps,
|
||||
flatDatabaseEventTriggerMaps,
|
||||
flatRouteTriggerMaps,
|
||||
});
|
||||
} catch (error) {
|
||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
return serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,14 +242,38 @@ export class ServerlessFunctionResolver {
|
||||
@Args('input')
|
||||
input: CreateServerlessFunctionInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<ServerlessFunctionDTO> {
|
||||
try {
|
||||
return await this.serverlessFunctionService.createOneServerlessFunction(
|
||||
input,
|
||||
workspaceId,
|
||||
);
|
||||
const flatServerlessFunction =
|
||||
await this.serverlessFunctionService.createOneServerlessFunction(
|
||||
input,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const {
|
||||
flatCronTriggerMaps,
|
||||
flatDatabaseEventTriggerMaps,
|
||||
flatRouteTriggerMaps,
|
||||
} =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatCronTriggerMaps',
|
||||
'flatDatabaseEventTriggerMaps',
|
||||
'flatRouteTriggerMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatServerlessFunctionToServerlessFunctionDto({
|
||||
flatServerlessFunction,
|
||||
flatCronTriggerMaps,
|
||||
flatDatabaseEventTriggerMaps,
|
||||
flatRouteTriggerMaps,
|
||||
});
|
||||
} catch (error) {
|
||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
return serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +293,7 @@ export class ServerlessFunctionResolver {
|
||||
version,
|
||||
});
|
||||
} catch (error) {
|
||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
return serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,16 +302,40 @@ export class ServerlessFunctionResolver {
|
||||
async publishServerlessFunction(
|
||||
@Args('input') input: PublishServerlessFunctionInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<ServerlessFunctionDTO> {
|
||||
try {
|
||||
const { id } = input;
|
||||
|
||||
return await this.serverlessFunctionService.publishOneServerlessFunctionOrFail(
|
||||
id,
|
||||
workspaceId,
|
||||
);
|
||||
const flatServerlessFunction =
|
||||
await this.serverlessFunctionService.publishOneServerlessFunctionOrFail(
|
||||
id,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const {
|
||||
flatCronTriggerMaps,
|
||||
flatDatabaseEventTriggerMaps,
|
||||
flatRouteTriggerMaps,
|
||||
} =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatCronTriggerMaps',
|
||||
'flatDatabaseEventTriggerMaps',
|
||||
'flatRouteTriggerMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatServerlessFunctionToServerlessFunctionDto({
|
||||
flatServerlessFunction,
|
||||
flatCronTriggerMaps,
|
||||
flatDatabaseEventTriggerMaps,
|
||||
flatRouteTriggerMaps,
|
||||
});
|
||||
} catch (error) {
|
||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
return serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+431
-190
@@ -1,32 +1,31 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { join } from 'path';
|
||||
|
||||
import deepEqual from 'deep-equal';
|
||||
import {
|
||||
DEFAULT_API_KEY_NAME,
|
||||
DEFAULT_API_URL_NAME,
|
||||
} from 'twenty-shared/application';
|
||||
import { Sources } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Not, Repository } from 'typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
import { type ServerlessExecuteResult } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
||||
import { SERVERLESS_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/serverless-function/serverless-function-executed';
|
||||
import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { buildEnvVar } from 'src/engine/core-modules/serverless/drivers/utils/build-env-var';
|
||||
import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files';
|
||||
import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service';
|
||||
import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
|
||||
import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
|
||||
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { ServerlessFunctionLayerService } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service';
|
||||
import { DEFAULT_TOOL_INPUT_SCHEMA } from 'src/engine/metadata-modules/serverless-function/constants/default-tool-input-schema.constant';
|
||||
import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
|
||||
import { type UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
@@ -34,8 +33,15 @@ import {
|
||||
ServerlessFunctionException,
|
||||
ServerlessFunctionExceptionCode,
|
||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
|
||||
import { findFlatServerlessFunctionOrThrow } from 'src/engine/metadata-modules/serverless-function/utils/find-flat-serverless-function-or-throw.util';
|
||||
import { fromCreateServerlessFunctionInputToFlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util';
|
||||
import { fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOrThrow } from 'src/engine/metadata-modules/serverless-function/utils/from-update-serverless-function-input-to-flat-serverless-function-to-update-or-throw.util';
|
||||
import { SubscriptionChannel } from 'src/engine/subscriptions/enums/subscription-channel.enum';
|
||||
import { SubscriptionService } from 'src/engine/subscriptions/subscription.service';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
import {
|
||||
WorkflowVersionStepException,
|
||||
WorkflowVersionStepExceptionCode,
|
||||
@@ -57,33 +63,57 @@ export class ServerlessFunctionService {
|
||||
private readonly auditService: AuditService,
|
||||
private readonly applicationTokenService: ApplicationTokenService,
|
||||
private readonly subscriptionService: SubscriptionService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {}
|
||||
|
||||
async hasServerlessFunctionPublishedVersion(serverlessFunctionId: string) {
|
||||
return await this.serverlessFunctionRepository.exists({
|
||||
where: {
|
||||
id: serverlessFunctionId,
|
||||
latestVersion: Not(IsNull()),
|
||||
},
|
||||
async hasServerlessFunctionPublishedVersion(
|
||||
serverlessFunctionId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const { flatServerlessFunctionMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatServerlessFunctionMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatServerlessFunction = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: serverlessFunctionId,
|
||||
flatEntityMaps: flatServerlessFunctionMaps,
|
||||
});
|
||||
|
||||
return (
|
||||
isDefined(flatServerlessFunction) &&
|
||||
!isDefined(flatServerlessFunction.deletedAt) &&
|
||||
isDefined(flatServerlessFunction.latestVersion)
|
||||
);
|
||||
}
|
||||
|
||||
async getServerlessFunctionSourceCode(
|
||||
workspaceId: string,
|
||||
id: string,
|
||||
version: string,
|
||||
): Promise<Sources | undefined> {
|
||||
const serverlessFunction =
|
||||
await this.serverlessFunctionRepository.findOneOrFail({
|
||||
where: {
|
||||
id,
|
||||
) {
|
||||
const { flatServerlessFunctionMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatServerlessFunctionMaps'],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const flatServerlessFunction = findFlatServerlessFunctionOrThrow({
|
||||
id,
|
||||
flatServerlessFunctionMaps,
|
||||
});
|
||||
|
||||
try {
|
||||
const folderPath = getServerlessFolder({
|
||||
serverlessFunction,
|
||||
const folderPath = getServerlessFolderOrThrow({
|
||||
flatServerlessFunction,
|
||||
version,
|
||||
});
|
||||
|
||||
@@ -109,24 +139,43 @@ export class ServerlessFunctionService {
|
||||
}): Promise<ServerlessExecuteResult> {
|
||||
await this.throttleExecution(workspaceId);
|
||||
|
||||
const functionToExecute =
|
||||
await this.serverlessFunctionRepository.findOneOrFail({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
relations: [
|
||||
'serverlessFunctionLayer',
|
||||
'application.applicationVariables',
|
||||
],
|
||||
});
|
||||
const {
|
||||
flatServerlessFunctionMaps,
|
||||
flatApplicationMaps,
|
||||
applicationVariableMaps,
|
||||
serverlessFunctionLayerMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatServerlessFunctionMaps',
|
||||
'flatApplicationMaps',
|
||||
'applicationVariableMaps',
|
||||
'serverlessFunctionLayerMaps',
|
||||
]);
|
||||
|
||||
const applicationAccessToken = isDefined(functionToExecute.applicationId)
|
||||
const flatServerlessFunction = findFlatServerlessFunctionOrThrow({
|
||||
id,
|
||||
flatServerlessFunctionMaps,
|
||||
});
|
||||
|
||||
const flatServerlessFunctionLayer =
|
||||
serverlessFunctionLayerMaps.byId[
|
||||
flatServerlessFunction.serverlessFunctionLayerId
|
||||
];
|
||||
|
||||
if (!isDefined(flatServerlessFunctionLayer)) {
|
||||
throw new ServerlessFunctionException(
|
||||
`Serverless function layer with id ${flatServerlessFunction.serverlessFunctionLayerId} not found`,
|
||||
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const applicationAccessToken = isDefined(
|
||||
flatServerlessFunction.applicationId,
|
||||
)
|
||||
? await this.applicationTokenService.generateApplicationToken({
|
||||
workspaceId,
|
||||
applicationId: functionToExecute.applicationId,
|
||||
applicationId: flatServerlessFunction.applicationId,
|
||||
expiresInSeconds: Math.max(
|
||||
functionToExecute.timeoutSeconds,
|
||||
flatServerlessFunction.timeoutSeconds,
|
||||
MIN_TOKEN_EXPIRATION_IN_SECONDS,
|
||||
),
|
||||
})
|
||||
@@ -134,6 +183,14 @@ export class ServerlessFunctionService {
|
||||
|
||||
const baseUrl = cleanServerUrl(this.twentyConfigService.get('SERVER_URL'));
|
||||
|
||||
const flatApplicationVariables = isDefined(
|
||||
flatServerlessFunction.applicationId,
|
||||
)
|
||||
? (applicationVariableMaps.byApplicationId[
|
||||
flatServerlessFunction.applicationId
|
||||
] ?? [])
|
||||
: [];
|
||||
|
||||
const envVariables = {
|
||||
...(isDefined(baseUrl)
|
||||
? {
|
||||
@@ -145,18 +202,19 @@ export class ServerlessFunctionService {
|
||||
[DEFAULT_API_KEY_NAME]: applicationAccessToken.token,
|
||||
}
|
||||
: {}),
|
||||
...buildEnvVar(functionToExecute),
|
||||
...buildEnvVar(flatApplicationVariables),
|
||||
};
|
||||
|
||||
const resultServerlessFunction = await this.callWithTimeout({
|
||||
callback: () =>
|
||||
this.serverlessService.execute({
|
||||
serverlessFunction: functionToExecute,
|
||||
flatServerlessFunction,
|
||||
flatServerlessFunctionLayer,
|
||||
payload,
|
||||
version,
|
||||
env: envVariables,
|
||||
}),
|
||||
timeoutMs: functionToExecute.timeoutSeconds * 1000,
|
||||
timeoutMs: flatServerlessFunction.timeoutSeconds * 1000,
|
||||
});
|
||||
|
||||
if (this.twentyConfigService.get('SERVERLESS_LOGS_ENABLED')) {
|
||||
@@ -164,18 +222,24 @@ export class ServerlessFunctionService {
|
||||
console.log(resultServerlessFunction.logs);
|
||||
}
|
||||
|
||||
const applicationUniversalIdentifier = isDefined(
|
||||
flatServerlessFunction.applicationId,
|
||||
)
|
||||
? flatApplicationMaps.byId[flatServerlessFunction.applicationId]
|
||||
?.universalIdentifier
|
||||
: undefined;
|
||||
|
||||
await this.subscriptionService.publish({
|
||||
channel: SubscriptionChannel.SERVERLESS_FUNCTION_LOGS_CHANNEL,
|
||||
workspaceId,
|
||||
payload: {
|
||||
serverlessFunctionLogs: {
|
||||
logs: resultServerlessFunction.logs,
|
||||
id: functionToExecute.id,
|
||||
name: functionToExecute.name,
|
||||
universalIdentifier: functionToExecute.universalIdentifier,
|
||||
applicationId: functionToExecute.applicationId,
|
||||
applicationUniversalIdentifier:
|
||||
functionToExecute.application?.universalIdentifier,
|
||||
id: flatServerlessFunction.id,
|
||||
name: flatServerlessFunction.name,
|
||||
universalIdentifier: flatServerlessFunction.universalIdentifier,
|
||||
applicationId: flatServerlessFunction.applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -190,23 +254,31 @@ export class ServerlessFunctionService {
|
||||
...(resultServerlessFunction.error && {
|
||||
errorType: resultServerlessFunction.error.errorType,
|
||||
}),
|
||||
functionId: functionToExecute.id,
|
||||
functionName: functionToExecute.name,
|
||||
functionId: flatServerlessFunction.id,
|
||||
functionName: flatServerlessFunction.name,
|
||||
});
|
||||
|
||||
return resultServerlessFunction;
|
||||
}
|
||||
|
||||
async publishOneServerlessFunctionOrFail(id: string, workspaceId: string) {
|
||||
const existingServerlessFunction =
|
||||
await this.serverlessFunctionRepository.findOneOrFail({
|
||||
where: {
|
||||
id,
|
||||
async publishOneServerlessFunctionOrFail(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
): Promise<FlatServerlessFunction> {
|
||||
const { flatServerlessFunctionMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatServerlessFunctionMaps'],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
if (isDefined(existingServerlessFunction.latestVersion)) {
|
||||
const existingFlatServerlessFunction = findFlatServerlessFunctionOrThrow({
|
||||
id,
|
||||
flatServerlessFunctionMaps,
|
||||
});
|
||||
|
||||
if (isDefined(existingFlatServerlessFunction.latestVersion)) {
|
||||
const latestCode = await this.getServerlessFunctionSourceCode(
|
||||
workspaceId,
|
||||
id,
|
||||
@@ -219,21 +291,21 @@ export class ServerlessFunctionService {
|
||||
);
|
||||
|
||||
if (deepEqual(latestCode, draftCode)) {
|
||||
return existingServerlessFunction;
|
||||
return existingFlatServerlessFunction;
|
||||
}
|
||||
}
|
||||
|
||||
const newVersion = existingServerlessFunction.latestVersion
|
||||
? `${parseInt(existingServerlessFunction.latestVersion, 10) + 1}`
|
||||
const newVersion = existingFlatServerlessFunction.latestVersion
|
||||
? `${parseInt(existingFlatServerlessFunction.latestVersion, 10) + 1}`
|
||||
: '1';
|
||||
|
||||
const draftFolderPath = getServerlessFolder({
|
||||
serverlessFunction: existingServerlessFunction,
|
||||
const draftFolderPath = getServerlessFolderOrThrow({
|
||||
flatServerlessFunction: existingFlatServerlessFunction,
|
||||
version: 'draft',
|
||||
});
|
||||
|
||||
const newFolderPath = getServerlessFolder({
|
||||
serverlessFunction: existingServerlessFunction,
|
||||
const newFolderPath = getServerlessFolderOrThrow({
|
||||
flatServerlessFunction: existingFlatServerlessFunction,
|
||||
version: newVersion,
|
||||
});
|
||||
|
||||
@@ -243,38 +315,60 @@ export class ServerlessFunctionService {
|
||||
});
|
||||
|
||||
const newPublishedVersions = [
|
||||
...existingServerlessFunction.publishedVersions,
|
||||
...existingFlatServerlessFunction.publishedVersions,
|
||||
newVersion,
|
||||
];
|
||||
|
||||
await this.serverlessFunctionRepository.update(
|
||||
existingServerlessFunction.id,
|
||||
{
|
||||
latestVersion: newVersion,
|
||||
publishedVersions: newPublishedVersions,
|
||||
},
|
||||
);
|
||||
const updatedFlatServerlessFunction: FlatServerlessFunction = {
|
||||
...existingFlatServerlessFunction,
|
||||
latestVersion: newVersion,
|
||||
publishedVersions: newPublishedVersions,
|
||||
};
|
||||
|
||||
const publishedServerlessFunction =
|
||||
await this.serverlessFunctionRepository.findOneOrFail({
|
||||
where: {
|
||||
id,
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
serverlessFunction: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [updatedFlatServerlessFunction],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while publishing serverless function',
|
||||
);
|
||||
}
|
||||
|
||||
const { flatServerlessFunctionMaps: recomputedFlatServerlessFunctionMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatServerlessFunctionMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const publishedFlatServerlessFunction =
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: recomputedFlatServerlessFunctionMaps,
|
||||
});
|
||||
|
||||
// This check should never be thrown, but we encounter some issue with
|
||||
// publishing serverless function in self hosted instances
|
||||
// See https://github.com/twentyhq/twenty/issues/13058
|
||||
// TODO: remove this check when issue solved
|
||||
if (!isDefined(publishedServerlessFunction.latestVersion)) {
|
||||
if (!isDefined(publishedFlatServerlessFunction.latestVersion)) {
|
||||
throw new WorkflowVersionStepException(
|
||||
`Fail to publish serverlessFunction ${publishedServerlessFunction.id}.Received latest version ${publishedServerlessFunction.latestVersion}`,
|
||||
`Fail to publish serverlessFunction ${publishedFlatServerlessFunction.id}.Received latest version ${publishedFlatServerlessFunction.latestVersion}`,
|
||||
WorkflowVersionStepExceptionCode.CODE_STEP_FAILURE,
|
||||
);
|
||||
}
|
||||
|
||||
return publishedServerlessFunction;
|
||||
return publishedFlatServerlessFunction;
|
||||
}
|
||||
|
||||
async deleteOneServerlessFunction({
|
||||
@@ -285,73 +379,196 @@ export class ServerlessFunctionService {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
softDelete?: boolean;
|
||||
}) {
|
||||
const existingServerlessFunction =
|
||||
await this.serverlessFunctionRepository.findOneOrFail({
|
||||
where: {
|
||||
id,
|
||||
}): Promise<FlatServerlessFunction> {
|
||||
const { flatServerlessFunctionMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatServerlessFunctionMaps'],
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
);
|
||||
|
||||
if (softDelete) {
|
||||
await this.serverlessFunctionRepository.softDelete({ id });
|
||||
} else {
|
||||
await this.serverlessFunctionRepository.delete({ id });
|
||||
// We don't need to await this
|
||||
this.fileStorageService.delete({
|
||||
folderPath: getServerlessFolder({
|
||||
serverlessFunction: existingServerlessFunction,
|
||||
}),
|
||||
});
|
||||
const existingFlatServerlessFunction = flatServerlessFunctionMaps.byId[id];
|
||||
|
||||
if (!isDefined(existingFlatServerlessFunction)) {
|
||||
throw new ServerlessFunctionException(
|
||||
'Serverless function to delete not found',
|
||||
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
// We don't need to await this
|
||||
this.serverlessService.delete(existingServerlessFunction);
|
||||
if (softDelete) {
|
||||
const updatedFlatServerlessFunctionWithDeletedAt: FlatServerlessFunction =
|
||||
{
|
||||
...existingFlatServerlessFunction,
|
||||
deletedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return existingServerlessFunction;
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
serverlessFunction: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [
|
||||
updatedFlatServerlessFunctionWithDeletedAt,
|
||||
],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while deleting serverless function',
|
||||
);
|
||||
}
|
||||
|
||||
return updatedFlatServerlessFunctionWithDeletedAt;
|
||||
} else {
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
serverlessFunction: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [existingFlatServerlessFunction],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while destroying serverless function',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return existingFlatServerlessFunction;
|
||||
}
|
||||
|
||||
async restoreOneServerlessFunction(id: string) {
|
||||
await this.serverlessFunctionRepository.restore({ id });
|
||||
async restoreOneServerlessFunction(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
): Promise<FlatServerlessFunction> {
|
||||
const { flatServerlessFunctionMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatServerlessFunctionMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const existingFlatServerlessFunction = flatServerlessFunctionMaps.byId[id];
|
||||
|
||||
if (!isDefined(existingFlatServerlessFunction)) {
|
||||
throw new ServerlessFunctionException(
|
||||
'Serverless function to restore not found',
|
||||
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const restoredFlatServerlessFunction: FlatServerlessFunction = {
|
||||
...existingFlatServerlessFunction,
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
serverlessFunction: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [restoredFlatServerlessFunction],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while restoring serverless function',
|
||||
);
|
||||
}
|
||||
|
||||
const { flatServerlessFunctionMaps: recomputedFlatServerlessFunctionMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatServerlessFunctionMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: recomputedFlatServerlessFunctionMaps,
|
||||
});
|
||||
}
|
||||
|
||||
async updateOneServerlessFunction(
|
||||
serverlessFunctionInput: UpdateServerlessFunctionInput,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const existingServerlessFunction =
|
||||
await this.serverlessFunctionRepository.findOneOrFail({
|
||||
where: {
|
||||
id: serverlessFunctionInput.id,
|
||||
): Promise<FlatServerlessFunction> {
|
||||
const { flatServerlessFunctionMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatServerlessFunctionMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const updatedFlatServerlessFunction =
|
||||
fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOrThrow({
|
||||
flatServerlessFunctionMaps,
|
||||
updateServerlessFunctionInput: serverlessFunctionInput,
|
||||
});
|
||||
|
||||
await this.serverlessFunctionRepository.update(
|
||||
existingServerlessFunction.id,
|
||||
{
|
||||
name: serverlessFunctionInput.update.name,
|
||||
description: serverlessFunctionInput.update.description,
|
||||
timeoutSeconds: serverlessFunctionInput.update.timeoutSeconds,
|
||||
toolInputSchema: serverlessFunctionInput.update.toolInputSchema,
|
||||
isTool: serverlessFunctionInput.update.isTool,
|
||||
},
|
||||
);
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
serverlessFunction: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [updatedFlatServerlessFunction],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
const fileFolder = getServerlessFolder({
|
||||
serverlessFunction: existingServerlessFunction,
|
||||
version: 'draft',
|
||||
});
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while updating serverless function',
|
||||
);
|
||||
}
|
||||
|
||||
await this.fileStorageService.writeFolder(
|
||||
serverlessFunctionInput.update.code,
|
||||
fileFolder,
|
||||
);
|
||||
const { flatServerlessFunctionMaps: recomputedFlatServerlessFunctionMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatServerlessFunctionMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return this.serverlessFunctionRepository.findOneBy({
|
||||
id: existingServerlessFunction.id,
|
||||
return findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: updatedFlatServerlessFunction.id,
|
||||
flatEntityMaps: recomputedFlatServerlessFunctionMaps,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -390,7 +607,7 @@ export class ServerlessFunctionService {
|
||||
serverlessFunctionLayerId?: string;
|
||||
},
|
||||
workspaceId: string,
|
||||
) {
|
||||
): Promise<FlatServerlessFunction> {
|
||||
let serverlessFunctionToCreateLayerId =
|
||||
serverlessFunctionInput.serverlessFunctionLayerId;
|
||||
|
||||
@@ -403,40 +620,58 @@ export class ServerlessFunctionService {
|
||||
serverlessFunctionToCreateLayerId = commonServerlessFunctionLayerId;
|
||||
}
|
||||
|
||||
const createServerlessFunctionInput: CreateServerlessFunctionInput = {
|
||||
...serverlessFunctionInput,
|
||||
serverlessFunctionLayerId: serverlessFunctionToCreateLayerId,
|
||||
};
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
// If no toolInputSchema is provided, use the default schema
|
||||
// (because the default template will be used for the code)
|
||||
const toolInputSchema = isDefined(serverlessFunctionInput.toolInputSchema)
|
||||
? serverlessFunctionInput.toolInputSchema
|
||||
: DEFAULT_TOOL_INPUT_SCHEMA;
|
||||
|
||||
const serverlessFunctionToCreate = this.serverlessFunctionRepository.create(
|
||||
{ ...createServerlessFunctionInput, workspaceId, toolInputSchema },
|
||||
);
|
||||
|
||||
const createdServerlessFunction =
|
||||
await this.serverlessFunctionRepository.save(serverlessFunctionToCreate);
|
||||
|
||||
const draftFileFolder = getServerlessFolder({
|
||||
serverlessFunction: createdServerlessFunction,
|
||||
version: 'draft',
|
||||
});
|
||||
|
||||
for (const file of await getBaseTypescriptProjectFiles) {
|
||||
await this.fileStorageService.write({
|
||||
file: file.content,
|
||||
name: file.name,
|
||||
mimeType: undefined,
|
||||
folder: join(draftFileFolder, file.path),
|
||||
const flatServerlessFunctionToCreate =
|
||||
fromCreateServerlessFunctionInputToFlatServerlessFunction({
|
||||
createServerlessFunctionInput: {
|
||||
...serverlessFunctionInput,
|
||||
serverlessFunctionLayerId: serverlessFunctionToCreateLayerId,
|
||||
},
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId:
|
||||
serverlessFunctionInput.applicationId ??
|
||||
workspaceCustomFlatApplication.id,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
serverlessFunction: {
|
||||
flatEntityToCreate: [flatServerlessFunctionToCreate],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while creating serverless function',
|
||||
);
|
||||
}
|
||||
|
||||
return this.serverlessFunctionRepository.findOneBy({
|
||||
id: createdServerlessFunction.id,
|
||||
const { flatServerlessFunctionMaps: recomputedFlatServerlessFunctionMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatServerlessFunctionMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: flatServerlessFunctionToCreate.id,
|
||||
flatEntityMaps: recomputedFlatServerlessFunctionMaps,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -453,24 +688,29 @@ export class ServerlessFunctionService {
|
||||
return;
|
||||
}
|
||||
|
||||
const serverlessFunction =
|
||||
await this.serverlessFunctionRepository.findOneOrFail({
|
||||
where: {
|
||||
id,
|
||||
const { flatServerlessFunctionMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatServerlessFunctionMaps'],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const flatServerlessFunction = findFlatServerlessFunctionOrThrow({
|
||||
id,
|
||||
flatServerlessFunctionMaps,
|
||||
});
|
||||
|
||||
await this.fileStorageService.copy({
|
||||
from: {
|
||||
folderPath: getServerlessFolder({
|
||||
serverlessFunction: serverlessFunction,
|
||||
folderPath: getServerlessFolderOrThrow({
|
||||
flatServerlessFunction,
|
||||
version,
|
||||
}),
|
||||
},
|
||||
to: {
|
||||
folderPath: getServerlessFolder({
|
||||
serverlessFunction: serverlessFunction,
|
||||
folderPath: getServerlessFolderOrThrow({
|
||||
flatServerlessFunction,
|
||||
version: 'draft',
|
||||
}),
|
||||
},
|
||||
@@ -485,50 +725,51 @@ export class ServerlessFunctionService {
|
||||
id: string;
|
||||
version: string;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
const serverlessFunctionToDuplicate =
|
||||
await this.serverlessFunctionRepository.findOneOrFail({
|
||||
where: {
|
||||
id,
|
||||
}): Promise<FlatServerlessFunction> {
|
||||
const { flatServerlessFunctionMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatServerlessFunctionMaps'],
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const newServerlessFunction = await this.createOneServerlessFunction(
|
||||
const flatServerlessFunctionToDuplicate = findFlatServerlessFunctionOrThrow(
|
||||
{
|
||||
name: serverlessFunctionToDuplicate.name,
|
||||
description: serverlessFunctionToDuplicate.description ?? undefined,
|
||||
timeoutSeconds: serverlessFunctionToDuplicate.timeoutSeconds,
|
||||
applicationId: serverlessFunctionToDuplicate.applicationId ?? undefined,
|
||||
id,
|
||||
flatServerlessFunctionMaps,
|
||||
},
|
||||
);
|
||||
|
||||
const newFlatServerlessFunction = await this.createOneServerlessFunction(
|
||||
{
|
||||
name: flatServerlessFunctionToDuplicate.name,
|
||||
description: flatServerlessFunctionToDuplicate.description ?? undefined,
|
||||
timeoutSeconds: flatServerlessFunctionToDuplicate.timeoutSeconds,
|
||||
applicationId:
|
||||
flatServerlessFunctionToDuplicate.applicationId ?? undefined,
|
||||
serverlessFunctionLayerId:
|
||||
serverlessFunctionToDuplicate.serverlessFunctionLayerId,
|
||||
flatServerlessFunctionToDuplicate.serverlessFunctionLayerId,
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!isDefined(newServerlessFunction)) {
|
||||
throw new ServerlessFunctionException(
|
||||
'Failed to create new serverless function',
|
||||
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_CREATE_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
await this.fileStorageService.copy({
|
||||
from: {
|
||||
folderPath: getServerlessFolder({
|
||||
serverlessFunction: serverlessFunctionToDuplicate,
|
||||
folderPath: getServerlessFolderOrThrow({
|
||||
flatServerlessFunction: flatServerlessFunctionToDuplicate,
|
||||
version,
|
||||
}),
|
||||
},
|
||||
to: {
|
||||
folderPath: getServerlessFolder({
|
||||
serverlessFunction: newServerlessFunction,
|
||||
folderPath: getServerlessFolderOrThrow({
|
||||
flatServerlessFunction: newFlatServerlessFunction,
|
||||
version: 'draft',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
return newServerlessFunction;
|
||||
return newFlatServerlessFunction;
|
||||
}
|
||||
|
||||
private async throttleExecution(workspaceId: string) {
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/fl
|
||||
import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
|
||||
import { fromServerlessFunctionEntityToFlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/utils/from-serverless-function-entity-to-flat-serverless-function.type';
|
||||
import { fromServerlessFunctionEntityToFlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/utils/from-serverless-function-entity-to-flat-serverless-function.util';
|
||||
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
|
||||
import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
|
||||
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import {
|
||||
ServerlessFunctionException,
|
||||
ServerlessFunctionExceptionCode,
|
||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||
|
||||
export const findFlatServerlessFunctionOrThrow = ({
|
||||
flatServerlessFunctionMaps,
|
||||
id,
|
||||
}: {
|
||||
flatServerlessFunctionMaps: MetadataFlatEntityMaps<'serverlessFunction'>;
|
||||
id: string;
|
||||
}) => {
|
||||
const flatServerlessFunction = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: flatServerlessFunctionMaps,
|
||||
});
|
||||
|
||||
if (
|
||||
!isDefined(flatServerlessFunction) ||
|
||||
isDefined(flatServerlessFunction.deletedAt)
|
||||
) {
|
||||
throw new ServerlessFunctionException(
|
||||
`Serverless function with id ${id} not found`,
|
||||
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return flatServerlessFunction;
|
||||
};
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { v4 } from 'uuid';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { DEFAULT_TOOL_INPUT_SCHEMA } from 'src/engine/metadata-modules/serverless-function/constants/default-tool-input-schema.constant';
|
||||
import { type CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
|
||||
@@ -39,7 +39,7 @@ export const fromCreateServerlessFunctionInputToFlatServerlessFunction = ({
|
||||
handlerName:
|
||||
rawCreateServerlessFunctionInput.handlerName ?? DEFAULT_HANDLER_NAME,
|
||||
universalIdentifier:
|
||||
rawCreateServerlessFunctionInput.universalIdentifier ?? id,
|
||||
rawCreateServerlessFunctionInput.universalIdentifier ?? v4(),
|
||||
createdAt: currentDate.toISOString(),
|
||||
updatedAt: currentDate.toISOString(),
|
||||
deletedAt: null,
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
|
||||
import { fromFlatCronTriggerToCronTriggerDto } from 'src/engine/metadata-modules/cron-trigger/utils/from-flat-cron-trigger-to-cron-trigger-dto.util';
|
||||
import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
|
||||
import { fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto } from 'src/engine/metadata-modules/database-event-trigger/utils/from-flat-database-event-trigger-to-database-event-trigger-dto.util';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
|
||||
import { fromFlatRouteTriggerToRouteTriggerDto } from 'src/engine/metadata-modules/route-trigger/utils/from-flat-route-trigger-to-route-trigger-dto.util';
|
||||
import { type ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
|
||||
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
|
||||
|
||||
export const fromFlatServerlessFunctionToServerlessFunctionDto = ({
|
||||
flatServerlessFunction,
|
||||
flatCronTriggerMaps,
|
||||
flatDatabaseEventTriggerMaps,
|
||||
flatRouteTriggerMaps,
|
||||
}: {
|
||||
flatServerlessFunction: FlatServerlessFunction;
|
||||
flatCronTriggerMaps: FlatEntityMaps<FlatCronTrigger>;
|
||||
flatDatabaseEventTriggerMaps: FlatEntityMaps<FlatDatabaseEventTrigger>;
|
||||
flatRouteTriggerMaps: FlatEntityMaps<FlatRouteTrigger>;
|
||||
}): ServerlessFunctionDTO => {
|
||||
const cronTriggers = flatServerlessFunction.cronTriggerIds
|
||||
.map((id) => flatCronTriggerMaps.byId[id])
|
||||
.filter(isDefined)
|
||||
.map(fromFlatCronTriggerToCronTriggerDto);
|
||||
|
||||
const databaseEventTriggers = flatServerlessFunction.databaseEventTriggerIds
|
||||
.map((id) => flatDatabaseEventTriggerMaps.byId[id])
|
||||
.filter(isDefined)
|
||||
.map(fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto);
|
||||
|
||||
const routeTriggers = flatServerlessFunction.routeTriggerIds
|
||||
.map((id) => flatRouteTriggerMaps.byId[id])
|
||||
.filter(isDefined)
|
||||
.map(fromFlatRouteTriggerToRouteTriggerDto);
|
||||
|
||||
return {
|
||||
id: flatServerlessFunction.id,
|
||||
name: flatServerlessFunction.name,
|
||||
description: flatServerlessFunction.description ?? undefined,
|
||||
runtime: flatServerlessFunction.runtime,
|
||||
timeoutSeconds: flatServerlessFunction.timeoutSeconds,
|
||||
latestVersion: flatServerlessFunction.latestVersion ?? undefined,
|
||||
handlerPath: flatServerlessFunction.handlerPath,
|
||||
handlerName: flatServerlessFunction.handlerName,
|
||||
publishedVersions: flatServerlessFunction.publishedVersions,
|
||||
toolInputSchema: flatServerlessFunction.toolInputSchema ?? undefined,
|
||||
isTool: flatServerlessFunction.isTool,
|
||||
applicationId: flatServerlessFunction.applicationId ?? undefined,
|
||||
workspaceId: flatServerlessFunction.workspaceId,
|
||||
createdAt: new Date(flatServerlessFunction.createdAt),
|
||||
updatedAt: new Date(flatServerlessFunction.updatedAt),
|
||||
cronTriggers,
|
||||
databaseEventTriggers,
|
||||
routeTriggers,
|
||||
};
|
||||
};
|
||||
+7
-16
@@ -1,18 +1,13 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
extractAndSanitizeObjectStringFields,
|
||||
isDefined,
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-maps.type';
|
||||
import { FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-editable-properties.constant';
|
||||
import { type UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
|
||||
import {
|
||||
ServerlessFunctionException,
|
||||
ServerlessFunctionExceptionCode,
|
||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
|
||||
import { findFlatServerlessFunctionOrThrow } from 'src/engine/metadata-modules/serverless-function/utils/find-flat-serverless-function-or-throw.util';
|
||||
import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils';
|
||||
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
|
||||
|
||||
@@ -22,7 +17,7 @@ export const fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOr
|
||||
flatServerlessFunctionMaps,
|
||||
}: {
|
||||
updateServerlessFunctionInput: UpdateServerlessFunctionInput;
|
||||
flatServerlessFunctionMaps: FlatEntityMaps<FlatServerlessFunction>;
|
||||
flatServerlessFunctionMaps: MetadataFlatEntityMaps<'serverlessFunction'>;
|
||||
}): FlatServerlessFunction => {
|
||||
const { id: serverlessFunctionToUpdateId } =
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
@@ -31,14 +26,10 @@ export const fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOr
|
||||
);
|
||||
|
||||
const existingFlatServerlessFunctionToUpdate =
|
||||
flatServerlessFunctionMaps.byId[serverlessFunctionToUpdateId];
|
||||
|
||||
if (!isDefined(existingFlatServerlessFunctionToUpdate)) {
|
||||
throw new ServerlessFunctionException(
|
||||
t`Serverless function to update not found`,
|
||||
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
findFlatServerlessFunctionOrThrow({
|
||||
id: serverlessFunctionToUpdateId,
|
||||
flatServerlessFunctionMaps,
|
||||
});
|
||||
const updatedEditableFieldProperties = {
|
||||
...extractAndSanitizeObjectStringFields(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user