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:
martmull
2026-05-06 10:23:53 +02:00
committed by GitHub
parent 2a97e77303
commit 617f571400
75 changed files with 1065 additions and 507 deletions
@@ -19,7 +19,7 @@ import {
import { ApplicationLogsService } from 'src/engine/core-modules/application-logs/application-logs.service';
import { parseApplicationLogLines } from 'src/engine/core-modules/application-logs/utils/parse-application-log-lines';
import { ApplicationRegistrationVariableEntity } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.entity';
import type { FlatApplicationVariable } from 'src/engine/core-modules/application/application-variable/types/flat-application-variable.type';
import type { FlatApplicationVariable } from 'src/engine/metadata-modules/flat-application-variable/types/flat-application-variable.type';
import { FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { LOGIC_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/logic-function/logic-function-executed';
@@ -205,8 +205,17 @@ export class LogicFunctionExecutorService {
);
}
const flatApplicationVariables =
applicationVariableMaps.byApplicationId[flatApplication.id] ?? [];
const flatApplicationVariableUniversalIdentifiers =
applicationVariableMaps.universalIdentifiersByApplicationId[
flatApplication.id
] ?? [];
const flatApplicationVariables = flatApplicationVariableUniversalIdentifiers
.map(
(universalIdentifier) =>
applicationVariableMaps.byUniversalIdentifier[universalIdentifier],
)
.filter(isDefined);
return { flatApplication, flatLogicFunction, flatApplicationVariables };
}
@@ -1,4 +1,4 @@
import { type FlatApplicationVariable } from 'src/engine/core-modules/application/application-variable/types/flat-application-variable.type';
import { type FlatApplicationVariable } from 'src/engine/metadata-modules/flat-application-variable/types/flat-application-variable.type';
import { type SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service';
import { buildEnvVar } from 'src/engine/core-modules/logic-function/logic-function-executor/utils/build-env-var';
@@ -28,6 +28,8 @@ describe('buildEnvVar', () => {
isSecret: false,
applicationId: 'app-1',
workspaceId: '00000000-0000-0000-0000-000000000000',
universalIdentifier: '00000000-0000-0000-0000-000000000000',
applicationUniversalIdentifier: '00000000-0000-0000-0000-000000000000',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
},
@@ -39,6 +41,8 @@ describe('buildEnvVar', () => {
isSecret: true,
applicationId: 'app-1',
workspaceId: '00000000-0000-0000-0000-000000000000',
universalIdentifier: '00000000-0000-0000-0000-000000000000',
applicationUniversalIdentifier: '00000000-0000-0000-0000-000000000000',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
},
@@ -50,6 +54,8 @@ describe('buildEnvVar', () => {
isSecret: false,
applicationId: 'app-1',
workspaceId: '00000000-0000-0000-0000-000000000000',
universalIdentifier: '00000000-0000-0000-0000-000000000000',
applicationUniversalIdentifier: '00000000-0000-0000-0000-000000000000',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
},
@@ -78,6 +84,8 @@ describe('buildEnvVar', () => {
isSecret: false,
applicationId: 'app-1',
workspaceId: '00000000-0000-0000-0000-000000000000',
universalIdentifier: '00000000-0000-0000-0000-000000000000',
applicationUniversalIdentifier: '00000000-0000-0000-0000-000000000000',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
},
@@ -89,6 +97,8 @@ describe('buildEnvVar', () => {
isSecret: false,
applicationId: 'app-1',
workspaceId: '00000000-0000-0000-0000-000000000000',
universalIdentifier: '00000000-0000-0000-0000-000000000000',
applicationUniversalIdentifier: '00000000-0000-0000-0000-000000000000',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
},
@@ -112,6 +122,8 @@ describe('buildEnvVar', () => {
isSecret: false,
applicationId: 'app-1',
workspaceId: '00000000-0000-0000-0000-000000000000',
universalIdentifier: '00000000-0000-0000-0000-000000000000',
applicationUniversalIdentifier: '00000000-0000-0000-0000-000000000000',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
},
@@ -1,4 +1,4 @@
import { type FlatApplicationVariable } from 'src/engine/core-modules/application/application-variable/types/flat-application-variable.type';
import { type FlatApplicationVariable } from 'src/engine/metadata-modules/flat-application-variable/types/flat-application-variable.type';
import { type SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service';
export const buildEnvVar = (