Add sync front component (#17748)

## Context
Allow twenty apps to sync front components
This commit is contained in:
Weiko
2026-02-09 00:00:23 +01:00
committed by GitHub
parent 40d7e740ef
commit 9aa63f7ddc
33 changed files with 643 additions and 97 deletions
@@ -20,6 +20,7 @@ export class ApplicationExceptionFilter implements ExceptionFilter {
case ApplicationExceptionCode.ENTITY_NOT_FOUND:
case ApplicationExceptionCode.APPLICATION_NOT_FOUND:
case ApplicationExceptionCode.LOGIC_FUNCTION_NOT_FOUND:
case ApplicationExceptionCode.FRONT_COMPONENT_NOT_FOUND:
throw new NotFoundError(exception);
case ApplicationExceptionCode.FORBIDDEN:
case ApplicationExceptionCode.INVALID_INPUT:
@@ -11,6 +11,7 @@ import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { FrontComponentModule } from 'src/engine/metadata-modules/front-component/front-component.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module';
@@ -21,8 +22,8 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/code-step-build.module';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/code-step/code-step-build.module';
@Module({
imports: [
@@ -44,6 +45,7 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
FileStorageModule,
WorkspaceCacheModule,
WorkspaceMigrationRunnerModule,
FrontComponentModule,
],
providers: [
ApplicationResolver,
@@ -8,6 +8,7 @@ export enum ApplicationExceptionCode {
OBJECT_NOT_FOUND = 'OBJECT_NOT_FOUND',
FIELD_NOT_FOUND = 'FIELD_NOT_FOUND',
LOGIC_FUNCTION_NOT_FOUND = 'LOGIC_FUNCTION_NOT_FOUND',
FRONT_COMPONENT_NOT_FOUND = 'FRONT_COMPONENT_NOT_FOUND',
ENTITY_NOT_FOUND = 'ENTITY_NOT_FOUND',
APPLICATION_NOT_FOUND = 'APPLICATION_NOT_FOUND',
FORBIDDEN = 'FORBIDDEN',
@@ -24,6 +25,8 @@ const getApplicationExceptionUserFriendlyMessage = (
return msg`Field not found.`;
case ApplicationExceptionCode.LOGIC_FUNCTION_NOT_FOUND:
return msg`Logic function not found.`;
case ApplicationExceptionCode.FRONT_COMPONENT_NOT_FOUND:
return msg`Front component not found.`;
case ApplicationExceptionCode.ENTITY_NOT_FOUND:
return msg`Entity not found.`;
case ApplicationExceptionCode.APPLICATION_NOT_FOUND:
@@ -4,6 +4,7 @@ import { parse } from 'path';
import {
FieldManifest,
FrontComponentManifest,
LogicFunctionManifest,
Manifest,
ObjectFieldManifest,
@@ -34,7 +35,9 @@ import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/typ
import { findFlatEntitiesByApplicationId } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entities-by-application-id.util';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { FrontComponentService } from 'src/engine/metadata-modules/front-component/front-component.service';
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/services/logic-function.service';
import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
@@ -64,6 +67,7 @@ export class ApplicationSyncService {
private readonly fieldPermissionService: FieldPermissionService,
private readonly permissionService: PermissionFlagService,
private readonly fileStorageService: FileStorageService,
private readonly frontComponentService: FrontComponentService,
) {}
public async synchronizeFromManifest({
@@ -108,6 +112,14 @@ export class ApplicationSyncService {
});
}
if ((manifest.frontComponents ?? []).length > 0) {
await this.syncFrontComponents({
frontComponentsToSync: manifest.frontComponents,
workspaceId,
ownerFlatApplication,
});
}
await this.syncRoles({
manifest,
workspaceId,
@@ -948,6 +960,103 @@ export class ApplicationSyncService {
}
}
private async syncFrontComponents({
frontComponentsToSync,
workspaceId,
ownerFlatApplication,
}: {
frontComponentsToSync: FrontComponentManifest[];
workspaceId: string;
ownerFlatApplication: FlatApplication;
}) {
const { flatFrontComponentMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatFrontComponentMaps'],
},
);
const applicationFrontComponents = Object.values(
flatFrontComponentMaps.byUniversalIdentifier,
).filter(
(frontComponent) =>
isDefined(frontComponent) &&
frontComponent.applicationId === ownerFlatApplication.id,
) as FlatFrontComponent[];
const frontComponentsToSyncUniversalIdentifiers = frontComponentsToSync.map(
(frontComponent) => frontComponent.universalIdentifier,
);
const applicationFrontComponentsUniversalIdentifiers =
applicationFrontComponents.map(
(frontComponent) => frontComponent.universalIdentifier,
);
const frontComponentsToDelete = applicationFrontComponents.filter(
(frontComponent) =>
isDefined(frontComponent.universalIdentifier) &&
!frontComponentsToSyncUniversalIdentifiers.includes(
frontComponent.universalIdentifier,
),
);
const frontComponentsToUpdate = applicationFrontComponents.filter(
(frontComponent) =>
isDefined(frontComponent.universalIdentifier) &&
frontComponentsToSyncUniversalIdentifiers.includes(
frontComponent.universalIdentifier,
),
);
const frontComponentsToCreate = frontComponentsToSync.filter(
(frontComponentToSync) =>
!applicationFrontComponentsUniversalIdentifiers.includes(
frontComponentToSync.universalIdentifier,
),
);
for (const frontComponentToDelete of frontComponentsToDelete) {
await this.frontComponentService.destroyOne({
id: frontComponentToDelete.id,
workspaceId,
isSystemBuild: true,
ownerFlatApplication,
});
}
for (const frontComponentToUpdate of frontComponentsToUpdate) {
const frontComponentToSync = frontComponentsToSync.find(
(frontComponent) =>
frontComponent.universalIdentifier ===
frontComponentToUpdate.universalIdentifier,
);
if (!frontComponentToSync) {
throw new ApplicationException(
`Failed to find frontComponent to sync with universalIdentifier ${frontComponentToUpdate.universalIdentifier}`,
ApplicationExceptionCode.FRONT_COMPONENT_NOT_FOUND,
);
}
await this.frontComponentService.updateOne({
id: frontComponentToUpdate.id,
update: frontComponentToSync,
workspaceId,
ownerFlatApplication,
});
}
for (const frontComponentToCreate of frontComponentsToCreate) {
await this.frontComponentService.createOne({
input: frontComponentToCreate,
workspaceId,
ownerFlatApplication,
});
}
}
public async uninstallApplication({
workspaceId,
applicationUniversalIdentifier,