Files
twenty/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service.ts
T
Paul Rastoin c8f541618d Refactor validate build and run for configuration to be less verbose and more reliable (#16343)
# Introduction
Refactored the api `validateBuildAndRunWorkspaceMigration` to be require
less configuration but to infer required args dynamically depending on
provided metadata maps to compare

## `inferDeletionFromMissingEntities`
Is not dynamically computed avoiding any miss configuration issue and
any missleading devxp

## Maps computation

Making only one call to redis to build both dependency and to be
compared entity maps. It does not matter to avoid passing a about to
compared flat entity maps to could also be a depedency, it's handled
directly in the builder setup optimistic cache logic

Please note that the flat maps used for the service input transpilation
might differ from the one that we will dynamically compute and inject in
the builder. Leading to do 2 redis calls but also race condition prone
validation error
We prefer that this occurs at the builder rather than at the runner
level as the pg instance is not cache and reflect the real state of a
given workspace
In a nutshell, there's a possible race condition between cache
invalidation and computation in both service input transpilers and
builder but we're totally ok with that
2025-12-05 15:41:56 +00:00

193 lines
7.0 KiB
TypeScript

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 { 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 { UpdateRouteTriggerInput } from 'src/engine/metadata-modules/route-trigger/dtos/update-route-trigger.input';
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 { 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 { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class RouteTriggerV2Service {
constructor(
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly applicationService: ApplicationService,
) {}
async createOne(
routeTriggerInput: CreateRouteTriggerInput,
workspaceId: string,
/**
* @deprecated do not use call validateBuildAndRunWorkspaceMigration contextually
* when interacting with another application than workspace custom one
* */
applicationId?: string,
) {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
const flatRouteTriggerToCreate =
fromCreateRouteTriggerInputToFlatRouteTrigger({
createRouteTriggerInput: routeTriggerInput,
workspaceId,
workspaceCustomApplicationId:
applicationId ?? workspaceCustomFlatApplication.id,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
routeTrigger: {
flatEntityToCreate: [flatRouteTriggerToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while creating route',
);
}
const { flatRouteTriggerMaps: recomputedExistingFlatRouteMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRouteTriggerMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatRouteTriggerToCreate.id,
flatEntityMaps: recomputedExistingFlatRouteMaps,
});
}
async updateOne(
routeTriggerInput: UpdateRouteTriggerInput,
workspaceId: string,
) {
const { flatRouteTriggerMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRouteTriggerMaps'],
},
);
const optimisticallyUpdatedFlatRouteTrigger =
fromUpdateRouteTriggerInputToFlatRouteTriggerToUpdateOrThrow({
flatRouteTriggerMaps,
updateRouteTriggerInput: routeTriggerInput,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
routeTrigger: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [optimisticallyUpdatedFlatRouteTrigger],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while updating route',
);
}
const { flatRouteTriggerMaps: recomputedExistingFlatRouteMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRouteTriggerMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: optimisticallyUpdatedFlatRouteTrigger.id,
flatEntityMaps: recomputedExistingFlatRouteMaps,
});
}
async destroyOne({
destroyRouteTriggerInput,
workspaceId,
}: {
destroyRouteTriggerInput: RouteTriggerIdInput;
workspaceId: string;
}): Promise<FlatRouteTrigger> {
const { flatRouteTriggerMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRouteTriggerMaps'],
},
);
const existingFlatRoute =
flatRouteTriggerMaps.byId[destroyRouteTriggerInput.id];
if (!isDefined(existingFlatRoute)) {
throw new RouteTriggerException(
'Route to destroy not found',
RouteTriggerExceptionCode.ROUTE_NOT_FOUND,
);
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
routeTrigger: {
flatEntityToCreate: [],
flatEntityToDelete: [existingFlatRoute],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while destroying route',
);
}
return existingFlatRoute;
}
}