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
@@ -171,6 +171,7 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({
case 'objectPermission':
case 'fieldPermission':
case 'webhook':
case 'applicationVariable':
case 'connectionProvider': {
if (!isDefined(idByUniversalIdentifier)) {
return action;
@@ -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,
@@ -3,6 +3,7 @@
exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should match snapshot 1`] = `
{
"agent": [],
"applicationVariable": [],
"commandMenuItem": [],
"connectionProvider": [],
"fieldMetadata": [
@@ -42,6 +42,7 @@ export const ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = {
objectPermission: {},
fieldPermission: {},
frontComponent: {},
applicationVariable: {},
connectionProvider: {},
} as const satisfies {
[P in AllMetadataName]: Partial<{
@@ -0,0 +1,7 @@
import { type ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity';
import { type UniversalFlatEntityFrom } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
export type UniversalFlatApplicationVariable = UniversalFlatEntityFrom<
ApplicationVariableEntity,
'applicationVariable'
>;
@@ -0,0 +1,24 @@
import { type BaseFlatCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-create-workspace-migration-action.type';
import { type BaseFlatDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-delete-workspace-migration-action.type';
import { type BaseFlatUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-update-workspace-migration-action.type';
import { type BaseUniversalCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-create-workspace-migration-action.type';
import { type BaseUniversalDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-delete-workspace-migration-action.type';
import { type BaseUniversalUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-update-workspace-migration-action.type';
export type FlatCreateApplicationVariableAction =
BaseFlatCreateWorkspaceMigrationAction<'applicationVariable'>;
export type UniversalCreateApplicationVariableAction =
BaseUniversalCreateWorkspaceMigrationAction<'applicationVariable'>;
export type FlatUpdateApplicationVariableAction =
BaseFlatUpdateWorkspaceMigrationAction<'applicationVariable'>;
export type UniversalUpdateApplicationVariableAction =
BaseUniversalUpdateWorkspaceMigrationAction<'applicationVariable'>;
export type UniversalDeleteApplicationVariableAction =
BaseUniversalDeleteWorkspaceMigrationAction<'applicationVariable'>;
export type FlatDeleteApplicationVariableAction =
BaseFlatDeleteWorkspaceMigrationAction<'applicationVariable'>;
@@ -0,0 +1,121 @@
import { Injectable } from '@nestjs/common';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { UniversalUpdateApplicationVariableAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/application-variable/types/workspace-migration-application-variable-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/universal-flat-entity-update-validation-args.type';
import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
import { UniversalFlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-result.type';
import { FlatApplicationVariableValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service';
@Injectable()
export class WorkspaceMigrationApplicationVariableActionsBuilderService extends WorkspaceEntityMigrationBuilderService<
typeof ALL_METADATA_NAME.applicationVariable
> {
constructor(
private readonly flatApplicationVariableValidatorService: FlatApplicationVariableValidatorService,
) {
super(ALL_METADATA_NAME.applicationVariable);
}
protected validateFlatEntityCreation(
args: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.applicationVariable
>,
): UniversalFlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.applicationVariable,
'create'
> {
const validationResult =
this.flatApplicationVariableValidatorService.validateFlatApplicationVariableCreation(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatApplicationVariableToValidate } = args;
return {
status: 'success',
action: {
type: 'create',
metadataName: 'applicationVariable',
flatEntity: flatApplicationVariableToValidate,
},
};
}
protected validateFlatEntityDeletion(
args: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.applicationVariable
>,
): UniversalFlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.applicationVariable,
'delete'
> {
const validationResult =
this.flatApplicationVariableValidatorService.validateFlatApplicationVariableDeletion(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatApplicationVariableToValidate } = args;
return {
status: 'success',
action: {
type: 'delete',
metadataName: 'applicationVariable',
universalIdentifier:
flatApplicationVariableToValidate.universalIdentifier,
},
};
}
protected validateFlatEntityUpdate(
args: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.applicationVariable
>,
): UniversalFlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.applicationVariable,
'update'
> {
const validationResult =
this.flatApplicationVariableValidatorService.validateFlatApplicationVariableUpdate(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { universalIdentifier, flatEntityUpdate } = args;
const updateAction: UniversalUpdateApplicationVariableAction = {
type: 'update',
metadataName: 'applicationVariable',
universalIdentifier,
update: flatEntityUpdate,
};
return {
status: 'success',
action: updateAction,
};
}
}
@@ -0,0 +1,127 @@
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 { ApplicationVariableEntityExceptionCode } from 'src/engine/core-modules/application/application-variable/application-variable.exception';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
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/universal-flat-entity-update-validation-args.type';
import { type UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
@Injectable()
export class FlatApplicationVariableValidatorService {
public validateFlatApplicationVariableCreation({
flatEntityToValidate: flatApplicationVariable,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatApplicationVariableMaps: optimisticFlatApplicationVariableMaps,
},
}: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.applicationVariable
>): FailedFlatEntityValidation<'applicationVariable', 'create'> {
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier: flatApplicationVariable.universalIdentifier,
key: flatApplicationVariable.key,
},
metadataName: 'applicationVariable',
type: 'create',
});
if (!isNonEmptyString(flatApplicationVariable.key)) {
validationResult.errors.push({
code: ApplicationVariableEntityExceptionCode.INVALID_APPLICATION_VARIABLE_INPUT,
message: t`Application variable key is required`,
userFriendlyMessage: msg`Application variable key is required`,
});
}
const existingVariableWithSameKey = Object.values(
optimisticFlatApplicationVariableMaps.byUniversalIdentifier,
).find(
(variable) =>
isDefined(variable) &&
variable.key === flatApplicationVariable.key &&
variable.universalIdentifier !==
flatApplicationVariable.universalIdentifier,
);
if (isDefined(existingVariableWithSameKey)) {
validationResult.errors.push({
code: ApplicationVariableEntityExceptionCode.INVALID_APPLICATION_VARIABLE_INPUT,
message: t`Application variable key must be unique`,
userFriendlyMessage: msg`Application variable key must be unique`,
});
}
return validationResult;
}
public validateFlatApplicationVariableDeletion({
flatEntityToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatApplicationVariableMaps: optimisticFlatApplicationVariableMaps,
},
}: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.applicationVariable
>): FailedFlatEntityValidation<'applicationVariable', 'delete'> {
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier: flatEntityToValidate.universalIdentifier,
key: flatEntityToValidate.key,
},
metadataName: 'applicationVariable',
type: 'delete',
});
const existingVariable = findFlatEntityByUniversalIdentifier({
universalIdentifier: flatEntityToValidate.universalIdentifier,
flatEntityMaps: optimisticFlatApplicationVariableMaps,
});
if (!isDefined(existingVariable)) {
validationResult.errors.push({
code: ApplicationVariableEntityExceptionCode.APPLICATION_VARIABLE_NOT_FOUND,
message: t`Application variable not found`,
userFriendlyMessage: msg`Application variable not found`,
});
}
return validationResult;
}
public validateFlatApplicationVariableUpdate({
universalIdentifier,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatApplicationVariableMaps: optimisticFlatApplicationVariableMaps,
},
}: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.applicationVariable
>): FailedFlatEntityValidation<'applicationVariable', 'update'> {
const fromFlatApplicationVariable = findFlatEntityByUniversalIdentifier({
universalIdentifier,
flatEntityMaps: optimisticFlatApplicationVariableMaps,
});
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier,
},
metadataName: 'applicationVariable',
type: 'update',
});
if (!isDefined(fromFlatApplicationVariable)) {
validationResult.errors.push({
code: ApplicationVariableEntityExceptionCode.APPLICATION_VARIABLE_NOT_FOUND,
message: t`Application variable not found`,
userFriendlyMessage: msg`Application variable not found`,
});
}
return validationResult;
}
}
@@ -29,6 +29,7 @@ import { FlatViewFilterValidatorService } from 'src/engine/workspace-manager/wor
import { FlatViewGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-group-validator.service';
import { FlatViewSortValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-sort-validator.service';
import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-validator.service';
import { FlatApplicationVariableValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service';
import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-webhook-validator.service';
import { FlatConnectionProviderValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-connection-provider-validator.service';
@@ -64,6 +65,7 @@ import { FlatConnectionProviderValidatorService } from 'src/engine/workspace-man
FlatRowLevelPermissionPredicateGroupValidatorService,
FlatFrontComponentValidatorService,
FlatWebhookValidatorService,
FlatApplicationVariableValidatorService,
FlatConnectionProviderValidatorService,
],
exports: [
@@ -95,6 +97,7 @@ import { FlatConnectionProviderValidatorService } from 'src/engine/workspace-man
FlatRowLevelPermissionPredicateGroupValidatorService,
FlatFrontComponentValidatorService,
FlatWebhookValidatorService,
FlatApplicationVariableValidatorService,
FlatConnectionProviderValidatorService,
],
})
@@ -28,6 +28,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';
import { WorkspaceMigrationConnectionProviderActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/connection-provider/workspace-migration-connection-provider-actions-builder.service';
import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module';
@@ -63,6 +64,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService,
WorkspaceMigrationFrontComponentActionsBuilderService,
WorkspaceMigrationWebhookActionsBuilderService,
WorkspaceMigrationApplicationVariableActionsBuilderService,
WorkspaceMigrationConnectionProviderActionsBuilderService,
],
exports: [
@@ -94,6 +96,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
FlatFieldMetadataTypeValidatorService,
WorkspaceMigrationFrontComponentActionsBuilderService,
WorkspaceMigrationWebhookActionsBuilderService,
WorkspaceMigrationApplicationVariableActionsBuilderService,
WorkspaceMigrationConnectionProviderActionsBuilderService,
],
})
@@ -0,0 +1,65 @@
import { Injectable } from '@nestjs/common';
import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import {
FlatCreateApplicationVariableAction,
UniversalCreateApplicationVariableAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/application-variable/types/workspace-migration-application-variable-action.type';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@Injectable()
export class CreateApplicationVariableActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create',
'applicationVariable',
) {
constructor() {
super();
}
override async transpileUniversalActionToFlatAction({
action,
flatApplication,
workspaceId,
}: WorkspaceMigrationActionRunnerArgs<UniversalCreateApplicationVariableAction>): Promise<FlatCreateApplicationVariableAction> {
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'applicationVariable',
});
return {
...action,
flatEntity: {
...action.flatEntity,
applicationId: flatApplication.id,
id: action.id ?? v4(),
workspaceId,
...emptyUniversalForeignKeyAggregators,
},
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerContext<FlatCreateApplicationVariableAction>,
): Promise<void> {
const { flatAction, queryRunner } = context;
const { flatEntity } = flatAction;
await this.insertFlatEntitiesInRepository({
queryRunner,
flatEntities: [flatEntity],
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerContext<FlatCreateApplicationVariableAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,51 @@
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 { ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity';
import {
FlatDeleteApplicationVariableAction,
UniversalDeleteApplicationVariableAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/application-variable/types/workspace-migration-application-variable-action.type';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@Injectable()
export class DeleteApplicationVariableActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete',
'applicationVariable',
) {
constructor() {
super();
}
override async transpileUniversalActionToFlatAction(
context: WorkspaceMigrationActionRunnerArgs<UniversalDeleteApplicationVariableAction>,
): Promise<FlatDeleteApplicationVariableAction> {
return this.transpileUniversalDeleteActionToFlatDeleteAction(context);
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerContext<FlatDeleteApplicationVariableAction>,
): Promise<void> {
const { flatAction, queryRunner, workspaceId } = context;
const applicationVariableRepository =
queryRunner.manager.getRepository<ApplicationVariableEntity>(
ApplicationVariableEntity,
);
await applicationVariableRepository.delete({
id: flatAction.entityId,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerContext<FlatDeleteApplicationVariableAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,98 @@
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 { ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity';
import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-update-relation-identifiers-to-ids.util';
import {
FlatUpdateApplicationVariableAction,
UniversalUpdateApplicationVariableAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/application-variable/types/workspace-migration-application-variable-action.type';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@Injectable()
export class UpdateApplicationVariableActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'update',
'applicationVariable',
) {
constructor(
private readonly secretEncryptionService: SecretEncryptionService,
) {
super();
}
override async transpileUniversalActionToFlatAction(
context: WorkspaceMigrationActionRunnerArgs<UniversalUpdateApplicationVariableAction>,
): Promise<FlatUpdateApplicationVariableAction> {
const { action, allFlatEntityMaps } = context;
const flatApplicationVariable = findFlatEntityByUniversalIdentifierOrThrow({
flatEntityMaps: allFlatEntityMaps.flatApplicationVariableMaps,
universalIdentifier: action.universalIdentifier,
});
const update = resolveUniversalUpdateRelationIdentifiersToIds({
metadataName: 'applicationVariable',
universalUpdate: action.update,
allFlatEntityMaps,
});
return {
type: 'update',
metadataName: 'applicationVariable',
entityId: flatApplicationVariable.id,
update,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerContext<FlatUpdateApplicationVariableAction>,
): Promise<void> {
const { flatAction, queryRunner, workspaceId } = context;
const { entityId, update } = flatAction;
const applicationVariableRepository =
queryRunner.manager.getRepository<ApplicationVariableEntity>(
ApplicationVariableEntity,
);
const existing = await applicationVariableRepository.findOne({
where: { id: entityId, workspaceId },
});
if (
update.isSecret !== undefined &&
update.isSecret &&
existing &&
!existing.isSecret
) {
(update as Record<string, unknown>).value =
this.secretEncryptionService.encrypt(existing.value);
}
if (
update.isSecret !== undefined &&
!update.isSecret &&
existing &&
existing.isSecret
) {
(update as Record<string, unknown>).value =
this.secretEncryptionService.decrypt(existing.value);
}
await applicationVariableRepository.update(
{ id: entityId, workspaceId },
update,
);
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerContext<FlatUpdateApplicationVariableAction>,
): Promise<void> {
return;
}
}
@@ -15,8 +15,8 @@ import {
import { fromUniversalFlatFieldMetadataToFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/utils/from-universal-flat-field-metadata-to-flat-field-metadata.util';
import { fromUniversalFlatObjectMetadataToFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/utils/from-universal-flat-object-metadata-to-flat-object-metadata.util';
import {
WorkspaceMigrationActionRunnerContext,
type WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
import { flatEntityToScalarFlatEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/flat-entity-to-scalar-flat-entity.util';
import { generateColumnDefinitions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/generate-column-definitions.util';
@@ -41,7 +41,7 @@ export class CreateObjectActionHandlerService extends WorkspaceMigrationRunnerAc
override async transpileUniversalActionToFlatAction(
context: WorkspaceMigrationActionRunnerArgs<UniversalCreateObjectAction>,
): Promise<FlatCreateObjectAction> {
const { action, workspaceId, allFlatEntityMaps } = context;
const { action, allFlatEntityMaps } = context;
const { fieldIdByUniversalIdentifier, id: providedObjectId } = action;
const allFieldIdToBeCreatedInActionByUniversalIdentifierMap = new Map<
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
import { CreateAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/agent/services/create-agent-action-handler.service';
import { DeleteAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/agent/services/delete-agent-action-handler.service';
@@ -81,6 +82,9 @@ import { UpdateViewSortActionHandlerService } from 'src/engine/workspace-manager
import { CreateViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/create-view-action-handler.service';
import { DeleteViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/delete-view-action-handler.service';
import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/update-view-action-handler.service';
import { CreateApplicationVariableActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/application-variable/services/create-application-variable-action-handler.service';
import { DeleteApplicationVariableActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/application-variable/services/delete-application-variable-action-handler.service';
import { UpdateApplicationVariableActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/application-variable/services/update-application-variable-action-handler.service';
import { CreateWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/create-webhook-action-handler.service';
import { DeleteWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/delete-webhook-action-handler.service';
import { UpdateWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/update-webhook-action-handler.service';
@@ -92,6 +96,7 @@ import { UpdateConnectionProviderActionHandlerService } from 'src/engine/workspa
imports: [
TypeOrmModule.forFeature([ApplicationEntity]),
WorkspaceSchemaManagerModule,
SecretEncryptionModule,
],
providers: [
CreateFieldActionHandlerService,
@@ -201,6 +206,11 @@ import { UpdateConnectionProviderActionHandlerService } from 'src/engine/workspa
CreateWebhookActionHandlerService,
UpdateWebhookActionHandlerService,
DeleteWebhookActionHandlerService,
CreateApplicationVariableActionHandlerService,
UpdateApplicationVariableActionHandlerService,
DeleteApplicationVariableActionHandlerService,
CreateConnectionProviderActionHandlerService,
UpdateConnectionProviderActionHandlerService,
DeleteConnectionProviderActionHandlerService,
@@ -28,5 +28,6 @@ export const METADATA_EVENTS_TO_EMIT = {
logicFunction: true,
viewSort: true,
webhook: false,
applicationVariable: false,
connectionProvider: true,
} as const satisfies { [P in AllMetadataName]: boolean };
@@ -69,6 +69,9 @@ const deriveAllMetadataEventsFromCreateAction = (
return [objectEvent, ...fieldEvents];
}
case 'applicationVariable': {
return [];
}
case 'view':
case 'viewField':
case 'viewGroup':
@@ -31,6 +31,9 @@ const deriveAllMetadataEventsFromDeleteAction = ({
allFlatEntityMaps,
}: DeriveMetadataEventsFromDeleteActionArgs): MetadataEvent[] => {
switch (flatAction.metadataName) {
case 'applicationVariable': {
return [];
}
case 'fieldMetadata':
case 'objectMetadata':
case 'view':
@@ -110,6 +110,9 @@ const deriveAllMetadataEventsFromUpdateAction = ({
return [deleteIndexMetadataEvent, createIndexMetadataEvent];
}
case 'applicationVariable': {
return [];
}
case 'fieldMetadata':
case 'objectMetadata':
case 'view':
@@ -26,5 +26,9 @@ export const flatEntityToScalarFlatEntity = <T extends AllMetadataName>({
result.applicationId = flatEntityRecord.applicationId;
result.universalIdentifier = flatEntityRecord.universalIdentifier;
if (metadataName === 'applicationVariable' && result.isSecret === true) {
result.value = '********';
}
return result as ScalarFlatEntity<MetadataEntity<T>>;
};
@@ -72,6 +72,7 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = ({
case 'objectPermission':
case 'fieldPermission':
case 'webhook':
case 'applicationVariable':
case 'connectionProvider': {
addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow({
flatEntity: flatAction.flatEntity,
@@ -44,6 +44,7 @@ export const optimisticallyApplyDeleteActionOnAllFlatEntityMaps = ({
case 'objectPermission':
case 'fieldPermission':
case 'webhook':
case 'applicationVariable':
case 'connectionProvider': {
const flatEntityToDelete = findFlatEntityByIdInFlatEntityMapsOrThrow<
MetadataFlatEntity<typeof flatAction.metadataName>
@@ -65,6 +65,7 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = ({
case 'objectPermission':
case 'fieldPermission':
case 'webhook':
case 'applicationVariable':
case 'connectionProvider': {
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(
flatAction.metadataName,