Front Extensibility: Introduce Front Component Entity (#17175)

As part of the extensibility effort, we are introducing a new engine
entity called "Front Component". This represents a dynamic react
component that will be rendered in CommandMenu actions or in PageLayout
widgets

This PR introduce the entity and all the necessary boilerplate to make
it syncable and cachable in the engine
This commit is contained in:
Charles Bochet
2026-01-16 17:23:46 +01:00
committed by GitHub
parent fe0d84c97e
commit 5fc4e810f7
68 changed files with 2042 additions and 5 deletions
@@ -5,6 +5,7 @@ import { FLAT_DATABASE_EVENT_TRIGGER_EDITABLE_PROPERTIES } from 'src/engine/meta
import { FLAT_AGENT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-agent/constants/flat-agent-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';
import { FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-editable-properties.constant';
import { FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-page-layout-tab/constants/flat-page-layout-tab-editable-properties.constant';
import { FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-page-layout-widget/constants/flat-page-layout-widget-editable-properties.constant';
@@ -162,6 +163,10 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = {
],
propertiesToStringify: [],
},
frontComponent: {
propertiesToCompare: [...FLAT_FRONT_COMPONENT_EDITABLE_PROPERTIES],
propertiesToStringify: [],
},
} as const satisfies {
[P in AllMetadataName]: OneFlatEntityConfiguration<P>;
};
@@ -436,6 +436,13 @@ export const ALL_METADATA_RELATIONS = {
},
},
},
frontComponent: {
manyToOne: {
workspace: null,
application: null,
},
oneToMany: {},
},
} as const satisfies MetadataRelationsProperties;
// Note: satisfies with complex mapped types involving nested generics doesn't always catch missing required keys
@@ -82,4 +82,5 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
role: true,
objectMetadata: true,
},
frontComponent: {},
} as const satisfies MetadataRequiredForValidation;
@@ -1,4 +1,6 @@
import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
import { type FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
import { type CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
import { type DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
@@ -145,6 +147,11 @@ import {
type DeleteViewAction,
type UpdateViewAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view/types/workspace-migration-view-action.type';
import {
type CreateFrontComponentAction,
type DeleteFrontComponentAction,
type UpdateFrontComponentAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/types/workspace-migration-front-component-action.type';
export type AllFlatEntityTypesByMetadataName = {
fieldMetadata: {
@@ -336,4 +343,13 @@ export type AllFlatEntityTypesByMetadataName = {
flatEntity: FlatPageLayoutTab;
entity: PageLayoutTabEntity;
};
frontComponent: {
actions: {
create: CreateFrontComponentAction;
update: UpdateFrontComponentAction;
delete: DeleteFrontComponentAction;
};
flatEntity: FlatFrontComponent;
entity: FrontComponentEntity;
};
};
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getMetadataRelatedMetadataNames should return related metadata names for agent 1`] = `[]`;
@@ -24,6 +24,8 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for frontComponent 1`] = `[]`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for index 1`] = `
[
"objectMetadata",
@@ -0,0 +1,5 @@
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
export const FLAT_FRONT_COMPONENT_EDITABLE_PROPERTIES = [
'name',
] as const satisfies (keyof FlatFrontComponent)[];
@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { WorkspaceFlatFrontComponentMapCacheService } from 'src/engine/metadata-modules/flat-front-component/services/workspace-flat-front-component-map-cache.service';
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
@Module({
imports: [
TypeOrmModule.forFeature([FrontComponentEntity]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [WorkspaceFlatFrontComponentMapCacheService],
exports: [WorkspaceFlatFrontComponentMapCacheService],
})
export class FlatFrontComponentModule {}
@@ -0,0 +1,44 @@
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 { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatFrontComponentMaps } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component-maps.type';
import { fromFrontComponentEntityToFlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/utils/from-front-component-entity-to-flat-front-component.util';
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@WorkspaceCache('flatFrontComponentMaps')
export class WorkspaceFlatFrontComponentMapCacheService extends WorkspaceCacheProvider<FlatFrontComponentMaps> {
constructor(
@InjectRepository(FrontComponentEntity)
private readonly frontComponentRepository: Repository<FrontComponentEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatFrontComponentMaps> {
const frontComponents = await this.frontComponentRepository.find({
where: { workspaceId },
});
const flatFrontComponentMaps = createEmptyFlatEntityMaps();
for (const frontComponentEntity of frontComponents) {
const flatFrontComponent =
fromFrontComponentEntityToFlatFrontComponent(frontComponentEntity);
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatFrontComponent,
flatEntityMapsToMutate: flatFrontComponentMaps,
});
}
return flatFrontComponentMaps;
}
}
@@ -0,0 +1,4 @@
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
export type FlatFrontComponentMaps = FlatEntityMaps<FlatFrontComponent>;
@@ -0,0 +1,4 @@
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
import { type FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
export type FlatFrontComponent = FlatEntityFrom<FrontComponentEntity>;
@@ -0,0 +1,34 @@
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
import { type CreateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/create-front-component.input';
export const fromCreateFrontComponentInputToFlatFrontComponentToCreate = ({
createFrontComponentInput,
workspaceId,
applicationId,
}: {
createFrontComponentInput: CreateFrontComponentInput;
workspaceId: string;
applicationId: string;
}): FlatFrontComponent => {
const now = new Date().toISOString();
const { name } = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
createFrontComponentInput,
['name'],
);
const id = createFrontComponentInput.id ?? v4();
return {
id,
name,
workspaceId,
createdAt: now,
updatedAt: now,
universalIdentifier: id,
applicationId,
};
};
@@ -0,0 +1,31 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/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 { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
import {
FrontComponentException,
FrontComponentExceptionCode,
} from 'src/engine/metadata-modules/front-component/front-component.exception';
export const fromDeleteFrontComponentInputToFlatFrontComponentOrThrow = ({
flatFrontComponentMaps,
frontComponentId,
}: {
flatFrontComponentMaps: FlatEntityMaps<FlatFrontComponent>;
frontComponentId: string;
}): FlatFrontComponent => {
const existingFlatFrontComponent = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: frontComponentId,
flatEntityMaps: flatFrontComponentMaps,
});
if (!isDefined(existingFlatFrontComponent)) {
throw new FrontComponentException(
'Front component not found',
FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND,
);
}
return existingFlatFrontComponent;
};
@@ -0,0 +1,13 @@
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
import { type FrontComponentDTO } from 'src/engine/metadata-modules/front-component/dtos/front-component.dto';
export const fromFlatFrontComponentToFrontComponentDto = (
flatFrontComponent: FlatFrontComponent,
): FrontComponentDTO => ({
id: flatFrontComponent.id,
name: flatFrontComponent.name,
workspaceId: flatFrontComponent.workspaceId,
applicationId: flatFrontComponent.applicationId,
createdAt: new Date(flatFrontComponent.createdAt),
updatedAt: new Date(flatFrontComponent.updatedAt),
});
@@ -0,0 +1,16 @@
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
import { type FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
export const fromFrontComponentEntityToFlatFrontComponent = (
frontComponentEntity: FrontComponentEntity,
): FlatFrontComponent => {
return {
id: frontComponentEntity.id,
name: frontComponentEntity.name,
workspaceId: frontComponentEntity.workspaceId,
universalIdentifier: frontComponentEntity.universalIdentifier,
applicationId: frontComponentEntity.applicationId,
createdAt: frontComponentEntity.createdAt.toISOString(),
updatedAt: frontComponentEntity.updatedAt.toISOString(),
};
};
@@ -0,0 +1,42 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/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 { FLAT_FRONT_COMPONENT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-front-component/constants/flat-front-component-editable-properties.constant';
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
import { type UpdateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/update-front-component.input';
import {
FrontComponentException,
FrontComponentExceptionCode,
} from 'src/engine/metadata-modules/front-component/front-component.exception';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
export const fromUpdateFrontComponentInputToFlatFrontComponentToUpdateOrThrow =
({
flatFrontComponentMaps,
updateFrontComponentInput,
}: {
flatFrontComponentMaps: FlatEntityMaps<FlatFrontComponent>;
updateFrontComponentInput: UpdateFrontComponentInput;
}): FlatFrontComponent => {
const existingFlatFrontComponent = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: updateFrontComponentInput.id,
flatEntityMaps: flatFrontComponentMaps,
});
if (!isDefined(existingFlatFrontComponent)) {
throw new FrontComponentException(
'Front component not found',
FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND,
);
}
return {
...mergeUpdateInExistingRecord({
existing: existingFlatFrontComponent,
properties: [...FLAT_FRONT_COMPONENT_EDITABLE_PROPERTIES],
update: updateFrontComponentInput.update,
}),
updatedAt: new Date().toISOString(),
};
};
@@ -24,7 +24,7 @@ export const fromCreateSkillInputToFlatSkillToCreate = ({
// Content is markdown - only trim, don't collapse whitespace (preserve newlines)
const content = createSkillInput.content.trim();
const id = v4();
const id = createSkillInput.id ?? v4();
return {
id,
@@ -0,0 +1,18 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class CreateFrontComponentInput {
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
id?: string;
@IsString()
@IsNotEmpty()
@Field()
name: string;
}
@@ -0,0 +1,31 @@
import { Field, HideField, ObjectType } from '@nestjs/graphql';
import { IsDateString, IsNotEmpty, IsString, IsUUID } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ObjectType('FrontComponent')
export class FrontComponentDTO {
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType)
id: string;
@IsString()
@Field()
name: string;
@HideField()
workspaceId: string;
@Field(() => UUIDScalarType)
applicationId: string;
@IsDateString()
@Field()
createdAt: Date;
@IsDateString()
@Field()
updatedAt: Date;
}
@@ -0,0 +1,37 @@
import { Field, InputType } from '@nestjs/graphql';
import { Type } from 'class-transformer';
import {
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
ValidateNested,
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class UpdateFrontComponentInputUpdates {
@IsOptional()
@IsString()
@Field({ nullable: true })
name?: string;
}
@InputType()
export class UpdateFrontComponentInput {
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType, {
description: 'The id of the front component to update',
})
id: string;
@Type(() => UpdateFrontComponentInputUpdates)
@ValidateNested()
@Field(() => UpdateFrontComponentInputUpdates, {
description: 'The front component fields to update',
})
update: UpdateFrontComponentInputUpdates;
}
@@ -0,0 +1,27 @@
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { SyncableEntityRequired } from 'src/engine/workspace-manager/types/syncable-entity-required.interface';
@Entity('frontComponent')
export class FrontComponentEntity
extends SyncableEntityRequired
implements Required<FrontComponentEntity>
{
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false })
name: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
}
@@ -0,0 +1,40 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
export enum FrontComponentExceptionCode {
FRONT_COMPONENT_NOT_FOUND = 'FRONT_COMPONENT_NOT_FOUND',
FRONT_COMPONENT_ALREADY_EXISTS = 'FRONT_COMPONENT_ALREADY_EXISTS',
INVALID_FRONT_COMPONENT_INPUT = 'INVALID_FRONT_COMPONENT_INPUT',
}
const getFrontComponentExceptionUserFriendlyMessage = (
code: FrontComponentExceptionCode,
) => {
switch (code) {
case FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND:
return msg`Front component not found.`;
case FrontComponentExceptionCode.FRONT_COMPONENT_ALREADY_EXISTS:
return msg`A front component with this name already exists.`;
case FrontComponentExceptionCode.INVALID_FRONT_COMPONENT_INPUT:
return msg`Invalid front component input.`;
default:
assertUnreachable(code);
}
};
export class FrontComponentException extends CustomException<FrontComponentExceptionCode> {
constructor(
message: string,
code: FrontComponentExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ??
getFrontComponentExceptionUserFriendlyMessage(code),
});
}
}
@@ -0,0 +1,29 @@
import { Module } from '@nestjs/common';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { FlatFrontComponentModule } from 'src/engine/metadata-modules/flat-front-component/flat-front-component.module';
import { FrontComponentResolver } from 'src/engine/metadata-modules/front-component/front-component.resolver';
import { FrontComponentService } from 'src/engine/metadata-modules/front-component/front-component.service';
import { FrontComponentGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/front-component/interceptors/front-component-graphql-api-exception.interceptor';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@Module({
imports: [
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationModule,
ApplicationModule,
PermissionsModule,
FlatFrontComponentModule,
],
providers: [
FrontComponentService,
FrontComponentResolver,
FrontComponentGraphqlApiExceptionInterceptor,
WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor,
],
exports: [FrontComponentService],
})
export class FrontComponentModule {}
@@ -0,0 +1,71 @@
import { UseGuards, UseInterceptors } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/create-front-component.input';
import { FrontComponentDTO } from 'src/engine/metadata-modules/front-component/dtos/front-component.dto';
import { UpdateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/update-front-component.input';
import { FrontComponentService } from 'src/engine/metadata-modules/front-component/front-component.service';
import { FrontComponentGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/front-component/interceptors/front-component-graphql-api-exception.interceptor';
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
@UseGuards(WorkspaceAuthGuard)
@UseInterceptors(
WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor,
FrontComponentGraphqlApiExceptionInterceptor,
)
@Resolver(() => FrontComponentDTO)
export class FrontComponentResolver {
constructor(private readonly frontComponentService: FrontComponentService) {}
@Query(() => [FrontComponentDTO])
@UseGuards(NoPermissionGuard)
async frontComponents(
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<FrontComponentDTO[]> {
return await this.frontComponentService.findAll(workspace.id);
}
@Query(() => FrontComponentDTO, { nullable: true })
@UseGuards(NoPermissionGuard)
async frontComponent(
@Args('id', { type: () => UUIDScalarType }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<FrontComponentDTO | null> {
return await this.frontComponentService.findById(id, workspace.id);
}
@Mutation(() => FrontComponentDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
async createFrontComponent(
@Args('input') input: CreateFrontComponentInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<FrontComponentDTO> {
return await this.frontComponentService.create(input, workspace.id);
}
@Mutation(() => FrontComponentDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
async updateFrontComponent(
@Args('input') input: UpdateFrontComponentInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<FrontComponentDTO> {
return await this.frontComponentService.update(input, workspace.id);
}
@Mutation(() => FrontComponentDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
async deleteFrontComponent(
@Args('id', { type: () => UUIDScalarType }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<FrontComponentDTO> {
return await this.frontComponentService.delete(id, workspace.id);
}
}
@@ -0,0 +1,237 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/application.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 { fromCreateFrontComponentInputToFlatFrontComponentToCreate } from 'src/engine/metadata-modules/flat-front-component/utils/from-create-front-component-input-to-flat-front-component-to-create.util';
import { fromDeleteFrontComponentInputToFlatFrontComponentOrThrow } from 'src/engine/metadata-modules/flat-front-component/utils/from-delete-front-component-input-to-flat-front-component-or-throw.util';
import { fromFlatFrontComponentToFrontComponentDto } from 'src/engine/metadata-modules/flat-front-component/utils/from-flat-front-component-to-front-component-dto.util';
import { fromUpdateFrontComponentInputToFlatFrontComponentToUpdateOrThrow } from 'src/engine/metadata-modules/flat-front-component/utils/from-update-front-component-input-to-flat-front-component-to-update-or-throw.util';
import { type CreateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/create-front-component.input';
import { type FrontComponentDTO } from 'src/engine/metadata-modules/front-component/dtos/front-component.dto';
import { type UpdateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/update-front-component.input';
import {
FrontComponentException,
FrontComponentExceptionCode,
} from 'src/engine/metadata-modules/front-component/front-component.exception';
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';
@Injectable()
export class FrontComponentService {
constructor(
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly applicationService: ApplicationService,
) {}
async findAll(workspaceId: string): Promise<FrontComponentDTO[]> {
const { flatFrontComponentMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatFrontComponentMaps'],
},
);
return Object.values(flatFrontComponentMaps.byId)
.filter(isDefined)
.sort((a, b) => a.name.localeCompare(b.name))
.map(fromFlatFrontComponentToFrontComponentDto);
}
async findById(
id: string,
workspaceId: string,
): Promise<FrontComponentDTO | null> {
const { flatFrontComponentMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatFrontComponentMaps'],
},
);
const flatFrontComponent = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatFrontComponentMaps,
});
if (!isDefined(flatFrontComponent)) {
return null;
}
return fromFlatFrontComponentToFrontComponentDto(flatFrontComponent);
}
async create(
input: CreateFrontComponentInput,
workspaceId: string,
): Promise<FrontComponentDTO> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const flatFrontComponentToCreate =
fromCreateFrontComponentInputToFlatFrontComponentToCreate({
createFrontComponentInput: input,
workspaceId,
applicationId: workspaceCustomFlatApplication.id,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
frontComponent: {
flatEntityToCreate: [flatFrontComponentToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating front component',
);
}
const { flatFrontComponentMaps: recomputedFlatFrontComponentMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatFrontComponentMaps'],
},
);
return fromFlatFrontComponentToFrontComponentDto(
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatFrontComponentToCreate.id,
flatEntityMaps: recomputedFlatFrontComponentMaps,
}),
);
}
async update(
input: UpdateFrontComponentInput,
workspaceId: string,
): Promise<FrontComponentDTO> {
const { flatFrontComponentMaps: existingFlatFrontComponentMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatFrontComponentMaps'],
},
);
const flatFrontComponentToUpdate =
fromUpdateFrontComponentInputToFlatFrontComponentToUpdateOrThrow({
flatFrontComponentMaps: existingFlatFrontComponentMaps,
updateFrontComponentInput: input,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
frontComponent: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [flatFrontComponentToUpdate],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating front component',
);
}
const { flatFrontComponentMaps: recomputedFlatFrontComponentMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatFrontComponentMaps'],
},
);
return fromFlatFrontComponentToFrontComponentDto(
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: input.id,
flatEntityMaps: recomputedFlatFrontComponentMaps,
}),
);
}
async delete(id: string, workspaceId: string): Promise<FrontComponentDTO> {
const { flatFrontComponentMaps: existingFlatFrontComponentMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatFrontComponentMaps'],
},
);
const flatFrontComponentToDelete =
fromDeleteFrontComponentInputToFlatFrontComponentOrThrow({
flatFrontComponentMaps: existingFlatFrontComponentMaps,
frontComponentId: id,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
frontComponent: {
flatEntityToCreate: [],
flatEntityToDelete: [flatFrontComponentToDelete],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting front component',
);
}
return fromFlatFrontComponentToFrontComponentDto(
flatFrontComponentToDelete,
);
}
async findByIdOrThrow(
id: string,
workspaceId: string,
): Promise<FrontComponentDTO> {
const frontComponent = await this.findById(id, workspaceId);
if (!isDefined(frontComponent)) {
throw new FrontComponentException(
'Front component not found',
FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND,
);
}
return frontComponent;
}
}
@@ -0,0 +1,24 @@
import {
type CallHandler,
type ExecutionContext,
Injectable,
type NestInterceptor,
} from '@nestjs/common';
import { type Observable, catchError } from 'rxjs';
import { frontComponentGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/front-component/utils/front-component-graphql-api-exception-handler.util';
@Injectable()
export class FrontComponentGraphqlApiExceptionInterceptor
implements NestInterceptor
{
intercept(
_context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
return next
.handle()
.pipe(catchError(frontComponentGraphqlApiExceptionHandler));
}
}
@@ -0,0 +1,29 @@
import { assertUnreachable } from 'twenty-shared/utils';
import {
ConflictError,
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
FrontComponentException,
FrontComponentExceptionCode,
} from 'src/engine/metadata-modules/front-component/front-component.exception';
export const frontComponentGraphqlApiExceptionHandler = (error: Error) => {
if (error instanceof FrontComponentException) {
switch (error.code) {
case FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND:
throw new NotFoundError(error);
case FrontComponentExceptionCode.INVALID_FRONT_COMPONENT_INPUT:
throw new UserInputError(error);
case FrontComponentExceptionCode.FRONT_COMPONENT_ALREADY_EXISTS:
throw new ConflictError(error);
default: {
return assertUnreachable(error.code);
}
}
}
throw error;
};
@@ -7,6 +7,7 @@ import { CronTriggerModule } from 'src/engine/metadata-modules/cron-trigger/cron
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { DatabaseEventTriggerModule } from 'src/engine/metadata-modules/database-event-trigger/database-event-trigger.module';
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { FrontComponentModule } from 'src/engine/metadata-modules/front-component/front-component.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
@@ -22,6 +23,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
imports: [
DataSourceModule,
FieldMetadataModule,
FrontComponentModule,
ObjectMetadataModule,
SearchFieldMetadataModule,
ServerlessFunctionModule,
@@ -42,6 +44,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
exports: [
DataSourceModule,
FieldMetadataModule,
FrontComponentModule,
ObjectMetadataModule,
SearchFieldMetadataModule,
ServerlessFunctionModule,
@@ -1,9 +1,16 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class CreateSkillInput {
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
id?: string;
@IsString()
@IsNotEmpty()
@Field()
@@ -40,6 +40,7 @@ export const WORKSPACE_CACHE_KEYS_V2 = {
'flat-maps:row-level-permission-predicate',
flatRowLevelPermissionPredicateGroupMaps:
'flat-maps:row-level-permission-predicate-group',
flatFrontComponentMaps: 'flat-maps:front-component',
} as const satisfies Record<WorkspaceCacheKeyName, string>;
export type AdditionalCacheDataMaps = {
@@ -18,6 +18,7 @@ import { WorkspaceMigrationAgentActionsBuilderService } from 'src/engine/workspa
import { WorkspaceMigrationCronTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/cron-trigger/workspace-migration-cron-trigger-action-builder.service';
import { WorkspaceMigrationDatabaseEventTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/database-event-trigger/workspace-migration-database-event-trigger-actions-builder.service';
import { WorkspaceMigrationFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/workspace-migration-field-actions-builder.service';
import { WorkspaceMigrationFrontComponentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/workspace-migration-front-component-actions-builder.service';
import { WorkspaceMigrationIndexActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/index/workspace-migration-index-actions-builder.service';
import { WorkspaceMigrationObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/workspace-migration-object-actions-builder.service';
import { WorkspaceMigrationPageLayoutTabActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-tab/workspace-migration-page-layout-tab-actions-builder.service';
@@ -60,6 +61,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
private readonly workspaceMigrationPageLayoutTabActionsBuilderService: WorkspaceMigrationPageLayoutTabActionsBuilderService,
private readonly workspaceMigrationRowLevelPermissionPredicateActionsBuilderService: WorkspaceMigrationRowLevelPermissionPredicateActionsBuilderService,
private readonly workspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService: WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService,
private readonly workspaceMigrationFrontComponentActionsBuilderService: WorkspaceMigrationFrontComponentActionsBuilderService,
) {}
private setupOptimisticCache({
@@ -159,6 +161,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
flatPageLayoutMaps,
flatPageLayoutWidgetMaps,
flatPageLayoutTabMaps,
flatFrontComponentMaps,
} = fromToAllFlatEntityMaps;
if (isDefined(flatObjectMetadataMaps)) {
@@ -928,6 +931,39 @@ export class WorkspaceMigrationBuildOrchestratorService {
}
}
if (isDefined(flatFrontComponentMaps)) {
const { from: fromFlatFrontComponentMaps, to: toFlatFrontComponentMaps } =
flatFrontComponentMaps;
const frontComponentResult =
await this.workspaceMigrationFrontComponentActionsBuilderService.validateAndBuild(
{
additionalCacheDataMaps,
from: fromFlatFrontComponentMaps,
to: toFlatFrontComponentMaps,
buildOptions,
dependencyOptimisticFlatEntityMaps: undefined,
workspaceId,
},
);
this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation(
{
allFlatEntityMaps: optimisticAllFlatEntityMaps,
flatEntityMapsAndRelatedFlatEntityMaps:
frontComponentResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
},
);
if (frontComponentResult.status === 'fail') {
orchestratorFailureReport.frontComponent.push(
...frontComponentResult.errors,
);
} else {
orchestratorActionsReport.frontComponent = frontComponentResult.actions;
}
}
const allErrors = Object.values(orchestratorFailureReport);
if (allErrors.some((report) => report.length > 0)) {
@@ -1069,6 +1105,12 @@ export class WorkspaceMigrationBuildOrchestratorService {
...aggregatedOrchestratorActionsReport.rowLevelPermissionPredicate
.update,
///
// Front components
...aggregatedOrchestratorActionsReport.frontComponent.delete,
...aggregatedOrchestratorActionsReport.frontComponent.create,
...aggregatedOrchestratorActionsReport.frontComponent.update,
///
],
workspaceId,
},
@@ -0,0 +1,12 @@
import { type BaseCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-create-workspace-migration-action.type';
import { type BaseDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-delete-workspace-migration-action.type';
import { type BaseUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-update-workspace-migration-action.type';
export type CreateFrontComponentAction =
BaseCreateWorkspaceMigrationAction<'frontComponent'>;
export type UpdateFrontComponentAction =
BaseUpdateWorkspaceMigrationAction<'frontComponent'>;
export type DeleteFrontComponentAction =
BaseDeleteWorkspaceMigrationAction<'frontComponent'>;
@@ -0,0 +1,116 @@
import { Injectable } from '@nestjs/common';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { UpdateFrontComponentAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/types/workspace-migration-front-component-action.type';
import { WorkspaceEntityMigrationBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-update-validation-args.type';
import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-validation-args.type';
import { FlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-validation-result.type';
import { FlatFrontComponentValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-front-component-validator.service';
@Injectable()
export class WorkspaceMigrationFrontComponentActionsBuilderService extends WorkspaceEntityMigrationBuilderService<
typeof ALL_METADATA_NAME.frontComponent
> {
constructor(
private readonly flatFrontComponentValidatorService: FlatFrontComponentValidatorService,
) {
super(ALL_METADATA_NAME.frontComponent);
}
protected validateFlatEntityCreation(
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.frontComponent>,
): FlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.frontComponent,
'create'
> {
const validationResult =
this.flatFrontComponentValidatorService.validateFlatFrontComponentCreation(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatFrontComponentToValidate } = args;
return {
status: 'success',
action: {
type: 'create',
metadataName: 'frontComponent',
flatEntity: flatFrontComponentToValidate,
},
};
}
protected validateFlatEntityDeletion(
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.frontComponent>,
): FlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.frontComponent,
'delete'
> {
const validationResult =
this.flatFrontComponentValidatorService.validateFlatFrontComponentDeletion(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatFrontComponentToValidate } = args;
return {
status: 'success',
action: {
type: 'delete',
metadataName: 'frontComponent',
entityId: flatFrontComponentToValidate.id,
},
};
}
protected validateFlatEntityUpdate(
args: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.frontComponent
>,
): FlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.frontComponent,
'update'
> {
const validationResult =
this.flatFrontComponentValidatorService.validateFlatFrontComponentUpdate(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityId, flatEntityUpdates } = args;
const updateFrontComponentAction: UpdateFrontComponentAction = {
type: 'update',
metadataName: 'frontComponent',
entityId: flatEntityId,
updates: flatEntityUpdates,
};
return {
status: 'success',
action: updateFrontComponentAction,
};
}
}
@@ -0,0 +1,113 @@
import { Injectable } from '@nestjs/common';
import { msg, t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { FrontComponentExceptionCode } from 'src/engine/metadata-modules/front-component/front-component.exception';
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util';
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-update-validation-args.type';
import { type FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-validation-args.type';
@Injectable()
export class FlatFrontComponentValidatorService {
public validateFlatFrontComponentCreation({
flatEntityToValidate: flatFrontComponent,
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.frontComponent
>): FailedFlatEntityValidation<'frontComponent', 'create'> {
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
id: flatFrontComponent.id,
universalIdentifier: flatFrontComponent.universalIdentifier,
name: flatFrontComponent.name,
},
metadataName: 'frontComponent',
type: 'create',
});
if (!isNonEmptyString(flatFrontComponent.name)) {
validationResult.errors.push({
code: FrontComponentExceptionCode.INVALID_FRONT_COMPONENT_INPUT,
message: t`Front component name is required`,
userFriendlyMessage: msg`Front component name is required`,
});
}
return validationResult;
}
public validateFlatFrontComponentDeletion({
flatEntityToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatFrontComponentMaps: optimisticFlatFrontComponentMaps,
},
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.frontComponent
>): FailedFlatEntityValidation<'frontComponent', 'delete'> {
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
id: flatEntityToValidate.id,
universalIdentifier: flatEntityToValidate.universalIdentifier,
name: flatEntityToValidate.name,
},
metadataName: 'frontComponent',
type: 'delete',
});
const existingFrontComponent = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatEntityToValidate.id,
flatEntityMaps: optimisticFlatFrontComponentMaps,
});
if (!isDefined(existingFrontComponent)) {
validationResult.errors.push({
code: FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND,
message: t`Front component not found`,
userFriendlyMessage: msg`Front component not found`,
});
return validationResult;
}
return validationResult;
}
public validateFlatFrontComponentUpdate({
flatEntityId,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatFrontComponentMaps: optimisticFlatFrontComponentMaps,
},
}: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.frontComponent
>): FailedFlatEntityValidation<'frontComponent', 'update'> {
const fromFlatFrontComponent = findFlatEntityByIdInFlatEntityMaps({
flatEntityId,
flatEntityMaps: optimisticFlatFrontComponentMaps,
});
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
id: flatEntityId,
universalIdentifier: fromFlatFrontComponent?.universalIdentifier,
},
metadataName: 'frontComponent',
type: 'update',
});
if (!isDefined(fromFlatFrontComponent)) {
validationResult.errors.push({
code: FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND,
message: t`Front component not found`,
userFriendlyMessage: msg`Front component not found`,
});
return validationResult;
}
return validationResult;
}
}
@@ -19,6 +19,7 @@ import { FlatRowLevelPermissionPredicateGroupValidatorService } from 'src/engine
import { FlatRowLevelPermissionPredicateValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-row-level-permission-predicate-validator.service';
import { FlatServerlessFunctionValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-serverless-function-validator.service';
import { FlatSkillValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-skill-validator.service';
import { FlatFrontComponentValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-front-component-validator.service';
import { FlatViewFieldValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-field-validator.service';
import { FlatViewFilterGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-group-validator.service';
import { FlatViewFilterValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service';
@@ -51,6 +52,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
FlatPageLayoutTabValidatorService,
FlatRowLevelPermissionPredicateValidatorService,
FlatRowLevelPermissionPredicateGroupValidatorService,
FlatFrontComponentValidatorService,
],
exports: [
FlatViewValidatorService,
@@ -75,6 +77,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
FlatPageLayoutTabValidatorService,
FlatRowLevelPermissionPredicateValidatorService,
FlatRowLevelPermissionPredicateGroupValidatorService,
FlatFrontComponentValidatorService,
],
})
export class WorkspaceMigrationBuilderValidatorsModule {}
@@ -18,6 +18,7 @@ import { WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService
import { WorkspaceMigrationRowLevelPermissionPredicateActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/row-level-permission-predicate/workspace-migration-row-level-permission-predicate-actions-builder.service';
import { WorkspaceMigrationServerlessFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/serverless-function/workspace-migration-serverless-function-actions-builder.service';
import { WorkspaceMigrationSkillActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/skill/workspace-migration-skill-actions-builder.service';
import { WorkspaceMigrationFrontComponentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/workspace-migration-front-component-actions-builder.service';
import { WorkspaceMigrationViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/workspace-migration-view-field-actions-builder.service';
import { WorkspaceMigrationViewFilterGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter-group/workspace-migration-view-filter-group-actions-builder.service';
import { WorkspaceMigrationViewFilterActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter/workspace-migration-view-filter-actions-builder.service';
@@ -50,6 +51,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationPageLayoutTabActionsBuilderService,
WorkspaceMigrationRowLevelPermissionPredicateActionsBuilderService,
WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService,
WorkspaceMigrationFrontComponentActionsBuilderService,
],
exports: [
WorkspaceMigrationViewActionsBuilderService,
@@ -74,6 +76,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationRowLevelPermissionPredicateActionsBuilderService,
WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService,
FlatFieldMetadataTypeValidatorService,
WorkspaceMigrationFrontComponentActionsBuilderService,
],
})
export class WorkspaceMigrationBuilderModule {}
@@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
import { CreateFrontComponentAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/types/workspace-migration-front-component-action.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@Injectable()
export class CreateFrontComponentActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create',
'frontComponent',
) {
constructor() {
super();
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<CreateFrontComponentAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { flatEntity } = action;
const frontComponentRepository =
queryRunner.manager.getRepository<FrontComponentEntity>(
FrontComponentEntity,
);
await frontComponentRepository.insert({
...flatEntity,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<CreateFrontComponentAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
import { DeleteFrontComponentAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/types/workspace-migration-front-component-action.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@Injectable()
export class DeleteFrontComponentActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete',
'frontComponent',
) {
constructor() {
super();
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<DeleteFrontComponentAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { entityId } = action;
const frontComponentRepository =
queryRunner.manager.getRepository<FrontComponentEntity>(
FrontComponentEntity,
);
await frontComponentRepository.delete({ id: entityId, workspaceId });
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<DeleteFrontComponentAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,39 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
import { UpdateFrontComponentAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/types/workspace-migration-front-component-action.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
@Injectable()
export class UpdateFrontComponentActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'update',
'frontComponent',
) {
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<UpdateFrontComponentAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { entityId, updates } = action;
const frontComponentRepository =
queryRunner.manager.getRepository<FrontComponentEntity>(
FrontComponentEntity,
);
await frontComponentRepository.update(
{ id: entityId, workspaceId },
fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
updates,
}),
);
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<UpdateFrontComponentAction>,
): Promise<void> {
return;
}
}
@@ -48,6 +48,9 @@ import { DeleteServerlessFunctionActionHandlerService } from 'src/engine/workspa
import { UpdateServerlessFunctionActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/update-serverless-function-action-handler.service';
import { CreateSkillActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/skill/services/create-skill-action-handler.service';
import { DeleteSkillActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/skill/services/delete-skill-action-handler.service';
import { CreateFrontComponentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/create-front-component-action-handler.service';
import { UpdateFrontComponentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/update-front-component-action-handler.service';
import { DeleteFrontComponentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/delete-front-component-action-handler.service';
import { UpdateSkillActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/skill/services/update-skill-action-handler.service';
import { CreateViewFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field/services/create-view-field-action-handler.service';
import { DeleteViewFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field/services/delete-view-field-action-handler.service';
@@ -151,6 +154,10 @@ import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/wor
CreateRowLevelPermissionPredicateGroupActionHandlerService,
UpdateRowLevelPermissionPredicateGroupActionHandlerService,
DeleteRowLevelPermissionPredicateGroupActionHandlerService,
CreateFrontComponentActionHandlerService,
UpdateFrontComponentActionHandlerService,
DeleteFrontComponentActionHandlerService,
],
})
export class WorkspaceSchemaMigrationRunnerActionHandlersModule {}
@@ -68,6 +68,7 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = <
case 'skill':
case 'pageLayout':
case 'pageLayoutWidget':
case 'frontComponent':
case 'pageLayoutTab': {
addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow({
flatEntity: action.flatEntity,
@@ -45,6 +45,7 @@ export const optimisticallyApplyDeleteActionOnAllFlatEntityMaps = <
case 'skill':
case 'pageLayout':
case 'pageLayoutWidget':
case 'frontComponent':
case 'pageLayoutTab': {
const flatEntityToDelete = findFlatEntityByIdInFlatEntityMapsOrThrow<
MetadataFlatEntity<typeof action.metadataName>
@@ -67,7 +67,8 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = <
case 'skill':
case 'pageLayout':
case 'pageLayoutWidget':
case 'pageLayoutTab': {
case 'pageLayoutTab':
case 'frontComponent': {
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(
action.metadataName,
);