20215 convert application variable to a syncable entity (#20269)
## Summary - Converts applicationVariable from a bespoke sync path to a proper SyncableEntity, unifying it with the workspace migration pipeline used by all other manifest-managed entities (agent, skill, frontComponent, webhook, etc.) - Removes the upsertManyApplicationVariableEntities method and its direct-DB-mutation approach in favor of the standard validate → build → run action handler pipeline - Adds universalIdentifier, deletedAt columns and makes applicationId NOT NULL via an instance command migration ## Motivation Before this change, applicationVariable was the only manifest-managed entity that bypassed ApplicationManifestMigrationService.syncMetadataFromManifest(). It used a bespoke service method called directly from syncApplication(), creating two mental models, two validation styles, and two cache invalidation patterns. Now there's one unified pipeline for all manifest entities. ## What changed ### Entity refactor: - ApplicationVariableEntity now extends SyncableEntity (gains universalIdentifier, non-nullable applicationId with CASCADE, soft-delete via deletedAt) ### New flat entity layer (flat-application-variable/): - Type, maps type, editable properties constant, entity-to-flat converter, cache service, module ### New migration pipeline wiring: - Manifest converter (fromApplicationVariableManifestToUniversalFlatApplicationVariable) - Validator service (FlatApplicationVariableValidatorService) - Builder service (WorkspaceMigrationApplicationVariableActionsBuilderService) - Create/Update/Delete action handlers with secret encryption hooks - Registered in orchestrator, builder module, runner module, and all type registries ### Removed bespoke path: - Deleted upsertManyApplicationVariableEntities from ApplicationVariableEntityService - Removed its call from ApplicationSyncService.syncApplication() - Kept update() (operator-set value at runtime) and getDisplayValue() (runtime display) ### Database migration: - Instance command to add columns, backfill universalIdentifier, enforce NOT NULL constraints, and update indexes ## Test plan - npx nx typecheck twenty-server passes (0 errors) - Unit tests pass (application-variable.service.spec.ts, build-env-var.spec.ts) - Install an app with applicationVariables in its manifest → variables appear with correct universalIdentifier - Update app manifest (add/remove/modify a variable) → migration pipeline handles diff correctly - Operator-set value via update endpoint persists correctly with encryption - Uninstall app → variables cascade-deleted - app dev --once on example app syncs without errors
This commit is contained in:
+1
@@ -171,6 +171,7 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({
|
||||
case 'objectPermission':
|
||||
case 'fieldPermission':
|
||||
case 'webhook':
|
||||
case 'applicationVariable':
|
||||
case 'connectionProvider': {
|
||||
if (!isDefined(idByUniversalIdentifier)) {
|
||||
return action;
|
||||
|
||||
+37
@@ -41,6 +41,7 @@ import { WorkspaceMigrationViewFilterActionsBuilderService } from 'src/engine/wo
|
||||
import { WorkspaceMigrationViewGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-group/workspace-migration-view-group-actions-builder.service';
|
||||
import { WorkspaceMigrationViewSortActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/workspace-migration-view-sort-actions.builder.service';
|
||||
import { WorkspaceMigrationViewActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view/workspace-migration-view-actions-builder.service';
|
||||
import { WorkspaceMigrationApplicationVariableActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/application-variable/workspace-migration-application-variable-actions-builder.service';
|
||||
import { WorkspaceMigrationWebhookActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/workspace-migration-webhook-actions-builder.service';
|
||||
|
||||
@Injectable()
|
||||
@@ -73,6 +74,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
private readonly workspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService: WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService,
|
||||
private readonly workspaceMigrationFrontComponentActionsBuilderService: WorkspaceMigrationFrontComponentActionsBuilderService,
|
||||
private readonly workspaceMigrationWebhookActionsBuilderService: WorkspaceMigrationWebhookActionsBuilderService,
|
||||
private readonly workspaceMigrationApplicationVariableActionsBuilderService: WorkspaceMigrationApplicationVariableActionsBuilderService,
|
||||
private readonly workspaceMigrationConnectionProviderActionsBuilderService: WorkspaceMigrationConnectionProviderActionsBuilderService,
|
||||
) {}
|
||||
|
||||
@@ -125,6 +127,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
const orchestratorActionsReport = structuredClone({
|
||||
...createEmptyOrchestratorActionsReport(),
|
||||
});
|
||||
|
||||
const orchestratorFailureReport = structuredClone(
|
||||
EMPTY_ORCHESTRATOR_FAILURE_REPORT(),
|
||||
);
|
||||
@@ -166,6 +169,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
flatPageLayoutTabMaps,
|
||||
flatFrontComponentMaps,
|
||||
flatWebhookMaps,
|
||||
flatApplicationVariableMaps,
|
||||
flatConnectionProviderMaps,
|
||||
} = fromToAllFlatEntityMaps;
|
||||
|
||||
@@ -826,6 +830,34 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(flatApplicationVariableMaps)) {
|
||||
const {
|
||||
from: fromFlatApplicationVariableMaps,
|
||||
to: toFlatApplicationVariableMaps,
|
||||
} = flatApplicationVariableMaps;
|
||||
|
||||
const applicationVariableResult =
|
||||
await this.workspaceMigrationApplicationVariableActionsBuilderService.validateAndBuild(
|
||||
{
|
||||
additionalCacheDataMaps,
|
||||
from: fromFlatApplicationVariableMaps,
|
||||
to: toFlatApplicationVariableMaps,
|
||||
buildOptions,
|
||||
dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps,
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
if (applicationVariableResult.status === 'fail') {
|
||||
orchestratorFailureReport.applicationVariable.push(
|
||||
...applicationVariableResult.errors,
|
||||
);
|
||||
} else {
|
||||
orchestratorActionsReport.applicationVariable =
|
||||
applicationVariableResult.actions;
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(flatConnectionProviderMaps)) {
|
||||
const {
|
||||
from: fromFlatConnectionProviderMaps,
|
||||
@@ -1031,6 +1063,11 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
...aggregatedOrchestratorActionsReport.webhook.update,
|
||||
///
|
||||
|
||||
// Application Variables
|
||||
...aggregatedOrchestratorActionsReport.applicationVariable.delete,
|
||||
...aggregatedOrchestratorActionsReport.applicationVariable.create,
|
||||
...aggregatedOrchestratorActionsReport.applicationVariable.update,
|
||||
|
||||
// Connection providers
|
||||
...aggregatedOrchestratorActionsReport.connectionProvider.delete,
|
||||
...aggregatedOrchestratorActionsReport.connectionProvider.create,
|
||||
|
||||
Reference in New Issue
Block a user