feat: implement skills system for AI agents (#16865)

## Summary
This PR introduces a Skills system for AI agents, inspired by the [Agent
Skills specification](https://agentskills.io/specification).

## Changes

### Backend
- **SkillEntity**: New database entity with migration for storing skills
- **V2 Sync Mechanism**: Implemented FlatSkill, builders, validators,
and action handlers following the v2 flat entity pattern
- **Standard Skills**: Pre-defined skills (workflow-building,
data-manipulation, dashboard-building, metadata-building, research,
code-interpreter, xlsx, pdf, docx, pptx)
- **GraphQL API**: CRUD operations for skills with proper guards and
permissions
- **Workspace Cache**: Integrated skills into the workspace cache system

### Frontend  
- **Skills Table**: Searchable table in AI settings showing all skills
- **Skill Form**: Create/edit page with Label (primary), Description,
and Content (markdown editor)
- **API Name**: Following existing patterns, name is derived from label
with advanced settings toggle for custom API names
- **Standard vs Custom**: Standard skills are read-only, custom skills
can be edited/deleted

## Key Design Decisions
- Skills are stored in the database (Salesforce-like approach) rather
than files
- Name is derived from Label by default (isLabelSyncedWithName pattern)
- Skills reference functions/files via @ mentions in markdown content
rather than explicit relations
- Standard skills are synced from code, custom skills are created via UI

## Screenshots
Skills table and form UI follow existing settings patterns.

## Testing
- [x] Lint passes
- [x] Typecheck passes
- [ ] CI tests
This commit is contained in:
Félix Malfait
2026-01-02 15:22:01 +01:00
committed by GitHub
parent 2a3fd788ae
commit 21ff42074d
146 changed files with 6964 additions and 1282 deletions
@@ -0,0 +1,37 @@
export const STANDARD_SKILL = {
'workflow-building': {
universalIdentifier: '20202020-6155-838a-b64e-44a791fbdc13',
},
'data-manipulation': {
universalIdentifier: '20202020-e225-f5c7-3d56-45feaa36f2e6',
},
'dashboard-building': {
universalIdentifier: '20202020-398f-0d7a-82db-4f43bc7e7044',
},
'metadata-building': {
universalIdentifier: '20202020-c66a-5fed-4a74-46e0b42a6332',
},
research: {
universalIdentifier: '20202020-db75-4fca-6813-4c7db0f964a0',
},
'code-interpreter': {
universalIdentifier: '20202020-5eb9-e775-cf4e-4f22be7be362',
},
xlsx: {
universalIdentifier: '20202020-2c7f-5b77-dfa4-494b84752ab7',
},
pdf: {
universalIdentifier: '20202020-c3d1-e0c9-2f93-45648b8bbd26',
},
docx: {
universalIdentifier: '20202020-6f15-2432-0537-4e23a2efd1cb',
},
pptx: {
universalIdentifier: '20202020-c81b-baf8-5255-4c34bd0eac9b',
},
} as const satisfies Record<
string,
{
universalIdentifier: string;
}
>;
@@ -10,4 +10,5 @@ export const TWENTY_STANDARD_ALL_METADATA_NAME = [
'view',
'role',
'agent',
'skill',
] as const satisfies AllMetadataName[];
@@ -0,0 +1,3 @@
import type { STANDARD_SKILL } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-skill.constant';
export type AllStandardSkillName = keyof typeof STANDARD_SKILL;
@@ -0,0 +1,25 @@
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { type CreateStandardSkillArgs } from 'src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-skill-flat-metadata.util';
import { STANDARD_FLAT_SKILL_METADATA_BUILDERS_BY_SKILL_NAME } from 'src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util';
export const buildStandardFlatSkillMetadataMaps = (
args: Omit<CreateStandardSkillArgs, 'context'>,
): FlatEntityMaps<FlatSkill> => {
const allSkillMetadatas: FlatSkill[] = Object.values(
STANDARD_FLAT_SKILL_METADATA_BUILDERS_BY_SKILL_NAME,
).map((builder) => builder(args));
let flatSkillMetadataMaps = createEmptyFlatEntityMaps();
for (const skillMetadata of allSkillMetadatas) {
flatSkillMetadataMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: skillMetadata,
flatEntityMaps: flatSkillMetadataMaps,
});
}
return flatSkillMetadataMaps;
};
@@ -0,0 +1,46 @@
import { v4 } from 'uuid';
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
import { STANDARD_SKILL } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-skill.constant';
import { type AllStandardSkillName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-skill-name.type';
import { type StandardBuilderArgs } from 'src/engine/workspace-manager/twenty-standard-application/types/metadata-standard-buillder-args.type';
export type CreateStandardSkillContext = {
skillName: AllStandardSkillName;
name: string;
label: string;
icon: string | null;
description: string | null;
content: string;
isCustom: boolean;
};
export type CreateStandardSkillArgs = StandardBuilderArgs<'skill'> & {
context: CreateStandardSkillContext;
};
export const createStandardSkillFlatMetadata = ({
context: { skillName, name, label, icon, description, content, isCustom },
workspaceId,
twentyStandardApplicationId,
now,
}: CreateStandardSkillArgs): FlatSkill => {
const universalIdentifier = STANDARD_SKILL[skillName].universalIdentifier;
return {
id: v4(),
universalIdentifier,
standardId: universalIdentifier,
name,
label,
icon,
description,
content,
isCustom,
isActive: true,
workspaceId,
applicationId: twentyStandardApplicationId,
createdAt: now,
updatedAt: now,
};
};
@@ -1,6 +1,7 @@
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type TwentyStandardAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/types/twenty-standard-all-flat-entity-maps.type';
import { buildStandardFlatAgentMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/agent-metadata/build-standard-flat-agent-metadata-maps.util';
import { buildStandardFlatSkillMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/build-standard-flat-skill-metadata-maps.util';
import { buildStandardFlatFieldMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/build-standard-flat-field-metadata-maps.util';
import { getStandardObjectMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-object-metadata-related-entity-ids.util';
import { buildStandardFlatIndexMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/build-standard-flat-index-metadata-maps.util';
@@ -122,6 +123,14 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({
},
});
const flatSkillMaps = buildStandardFlatSkillMetadataMaps({
now,
workspaceId,
twentyStandardApplicationId,
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps: undefined,
});
return {
flatViewFieldMaps,
flatViewFilterMaps,
@@ -132,5 +141,6 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({
flatObjectMetadataMaps,
flatRoleMaps,
flatAgentMaps,
flatSkillMaps,
};
};
@@ -18,6 +18,7 @@ export const fromWorkspaceMigrationBuilderExceptionToMetadataValidationResponseE
return {
summary: {
invalidAgent: 0,
invalidSkill: 0,
invalidViewFilter: 0,
invalidViewFilterGroup: 0,
invalidObjectMetadata: 0,
@@ -29,6 +29,7 @@ import { WorkspaceMigrationV2RouteTriggerActionsBuilderService } from 'src/engin
import { WorkspaceMigrationV2RowLevelPermissionPredicateGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/row-level-permission-predicate-group/workspace-migration-v2-row-level-permission-predicate-group-actions-builder.service';
import { WorkspaceMigrationV2RowLevelPermissionPredicateActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/row-level-permission-predicate/workspace-migration-v2-row-level-permission-predicate-actions-builder.service';
import { WorkspaceMigrationV2ServerlessFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/serverless-function/workspace-migration-v2-serverless-function-actions-builder.service';
import { WorkspaceMigrationV2SkillActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/skill/workspace-migration-v2-skill-actions-builder.service';
import { WorkspaceMigrationV2ViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service';
import { WorkspaceMigrationV2ViewFilterGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter-group/workspace-migration-v2-view-filter-group-actions-builder.service';
import { WorkspaceMigrationV2ViewFilterActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter/workspace-migration-v2-view-filter-actions-builder.service';
@@ -53,6 +54,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
private readonly workspaceMigrationV2FieldActionsBuilderService: WorkspaceMigrationV2FieldActionsBuilderService,
private readonly workspaceMigrationV2RoleActionsBuilderService: WorkspaceMigrationV2RoleActionsBuilderService,
private readonly workspaceMigrationV2AgentActionsBuilderService: WorkspaceMigrationV2AgentActionsBuilderService,
private readonly workspaceMigrationV2SkillActionsBuilderService: WorkspaceMigrationV2SkillActionsBuilderService,
private readonly workspaceMigrationV2PageLayoutActionsBuilderService: WorkspaceMigrationV2PageLayoutActionsBuilderService,
private readonly workspaceMigrationV2PageLayoutWidgetActionsBuilderService: WorkspaceMigrationV2PageLayoutWidgetActionsBuilderService,
private readonly workspaceMigrationV2PageLayoutTabActionsBuilderService: WorkspaceMigrationV2PageLayoutTabActionsBuilderService,
@@ -153,6 +155,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
flatRoleMaps,
flatRoleTargetMaps,
flatAgentMaps,
flatSkillMaps,
flatPageLayoutMaps,
flatPageLayoutWidgetMaps,
flatPageLayoutTabMaps,
@@ -781,6 +784,36 @@ export class WorkspaceMigrationBuildOrchestratorService {
}
}
if (isDefined(flatSkillMaps)) {
const { from: fromFlatSkillMaps, to: toFlatSkillMaps } = flatSkillMaps;
const skillResult =
await this.workspaceMigrationV2SkillActionsBuilderService.validateAndBuild(
{
additionalCacheDataMaps,
from: fromFlatSkillMaps,
to: toFlatSkillMaps,
buildOptions,
dependencyOptimisticFlatEntityMaps: undefined,
workspaceId,
},
);
this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation(
{
allFlatEntityMaps: optimisticAllFlatEntityMaps,
flatEntityMapsAndRelatedFlatEntityMaps:
skillResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
},
);
if (skillResult.status === 'fail') {
orchestratorFailureReport.skill.push(...skillResult.errors);
} else {
orchestratorActionsReport.skill = skillResult.actions;
}
}
if (isDefined(flatPageLayoutMaps)) {
const { from: fromFlatPageLayoutMaps, to: toFlatPageLayoutMaps } =
flatPageLayoutMaps;
@@ -988,6 +1021,12 @@ export class WorkspaceMigrationBuildOrchestratorService {
...aggregatedOrchestratorActionsReport.agent.updated,
///
// Skills
...aggregatedOrchestratorActionsReport.skill.deleted,
...aggregatedOrchestratorActionsReport.skill.created,
...aggregatedOrchestratorActionsReport.skill.updated,
///
// Page layouts
...aggregatedOrchestratorActionsReport.pageLayout.deleted,
...aggregatedOrchestratorActionsReport.pageLayout.created,
@@ -0,0 +1,18 @@
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
import { type FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type';
export type UpdateSkillAction = {
type: 'update_skill';
flatEntityId: string;
flatEntityUpdates: FlatEntityPropertiesUpdates<'skill'>;
};
export type CreateSkillAction = {
type: 'create_skill';
flatEntity: FlatSkill;
};
export type DeleteSkillAction = {
type: 'delete_skill';
flatEntityId: string;
};
@@ -0,0 +1,96 @@
import { Injectable } from '@nestjs/common';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { UpdateSkillAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/skill/types/workspace-migration-v2-skill-action.type';
import { WorkspaceEntityMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type';
import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
import { FlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-result.type';
import { FlatSkillValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-skill-validator.service';
@Injectable()
export class WorkspaceMigrationV2SkillActionsBuilderService extends WorkspaceEntityMigrationBuilderV2Service<
typeof ALL_METADATA_NAME.skill
> {
constructor(
private readonly flatSkillValidatorService: FlatSkillValidatorService,
) {
super(ALL_METADATA_NAME.skill);
}
protected validateFlatEntityCreation(
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.skill>,
): FlatEntityValidationReturnType<typeof ALL_METADATA_NAME.skill, 'created'> {
const validationResult =
this.flatSkillValidatorService.validateFlatSkillCreation(args);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatSkillToValidate } = args;
return {
status: 'success',
action: {
type: 'create_skill',
flatEntity: flatSkillToValidate,
},
};
}
protected validateFlatEntityDeletion(
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.skill>,
): FlatEntityValidationReturnType<typeof ALL_METADATA_NAME.skill, 'deleted'> {
const validationResult =
this.flatSkillValidatorService.validateFlatSkillDeletion(args);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatSkillToValidate } = args;
return {
status: 'success',
action: {
type: 'delete_skill',
flatEntityId: flatSkillToValidate.id,
},
};
}
protected validateFlatEntityUpdate(
args: FlatEntityUpdateValidationArgs<typeof ALL_METADATA_NAME.skill>,
): FlatEntityValidationReturnType<typeof ALL_METADATA_NAME.skill, 'updated'> {
const validationResult =
this.flatSkillValidatorService.validateFlatSkillUpdate(args);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityId, flatEntityUpdates } = args;
const updateSkillAction: UpdateSkillAction = {
type: 'update_skill',
flatEntityId,
flatEntityUpdates,
};
return {
status: 'success',
action: updateSkillAction,
};
}
}
@@ -0,0 +1,217 @@
import { Injectable } from '@nestjs/common';
import { msg, t } from '@lingui/core/macro';
import { type ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
import { SkillExceptionCode } from 'src/engine/metadata-modules/skill/skill.exception';
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
import { findFlatEntityPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/utils/find-flat-entity-property-update.util';
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type';
import { type FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
import { validateSkillNameUniqueness } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-skill-name-uniqueness.util';
import {
validateSkillContentIsDefined,
validateSkillLabelIsDefined,
validateSkillRequiredProperties,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-skill-required-properties.util';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
@Injectable()
export class FlatSkillValidatorService {
public validateFlatSkillCreation({
flatEntityToValidate: flatSkill,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatSkillMaps: optimisticFlatSkillMaps,
},
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.skill
>): FailedFlatEntityValidation<FlatSkill> {
const validationResult: FailedFlatEntityValidation<FlatSkill> = {
type: 'create_skill',
errors: [],
flatEntityMinimalInformation: {
id: flatSkill.id,
name: flatSkill.name,
},
};
const existingSkills = Object.values(optimisticFlatSkillMaps.byId).filter(
isDefined,
);
validationResult.errors.push(
...validateSkillRequiredProperties({ flatSkill }),
);
validationResult.errors.push(
...validateSkillNameUniqueness({
name: flatSkill.name,
existingFlatSkills: existingSkills,
}),
);
return validationResult;
}
public validateFlatSkillDeletion({
flatEntityToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatSkillMaps: optimisticFlatSkillMaps,
},
buildOptions,
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.skill
>): FailedFlatEntityValidation<FlatSkill> {
const validationResult: FailedFlatEntityValidation<FlatSkill> = {
type: 'delete_skill',
errors: [],
flatEntityMinimalInformation: {
id: flatEntityToValidate.id,
name: flatEntityToValidate.name,
},
};
const existingSkill = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatEntityToValidate.id,
flatEntityMaps: optimisticFlatSkillMaps,
});
if (!isDefined(existingSkill)) {
validationResult.errors.push({
code: SkillExceptionCode.SKILL_NOT_FOUND,
message: t`Skill not found`,
userFriendlyMessage: msg`Skill not found`,
});
return validationResult;
}
if (!buildOptions.isSystemBuild && isStandardMetadata(existingSkill)) {
validationResult.errors.push({
code: SkillExceptionCode.SKILL_IS_STANDARD,
message: t`Cannot delete standard skill`,
userFriendlyMessage: msg`Cannot delete standard skill`,
});
}
return validationResult;
}
public validateFlatSkillUpdate({
flatEntityId,
flatEntityUpdates,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatSkillMaps: optimisticFlatSkillMaps,
},
buildOptions,
}: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.skill
>): FailedFlatEntityValidation<FlatSkill> {
const validationResult: FailedFlatEntityValidation<FlatSkill> = {
type: 'update_skill',
errors: [],
flatEntityMinimalInformation: {
id: flatEntityId,
},
};
const fromFlatSkill = findFlatEntityByIdInFlatEntityMaps({
flatEntityId,
flatEntityMaps: optimisticFlatSkillMaps,
});
if (!isDefined(fromFlatSkill)) {
validationResult.errors.push({
code: SkillExceptionCode.SKILL_NOT_FOUND,
message: t`Skill not found`,
userFriendlyMessage: msg`Skill not found`,
});
return validationResult;
}
// Standard skills can only have isActive toggled, not other properties
const isActiveUpdate = findFlatEntityPropertyUpdate({
flatEntityUpdates,
property: 'isActive',
});
const hasNonIsActiveUpdates = flatEntityUpdates.some(
(update) => update.property !== 'isActive',
);
if (
!buildOptions.isSystemBuild &&
isStandardMetadata(fromFlatSkill) &&
hasNonIsActiveUpdates
) {
validationResult.errors.push({
code: SkillExceptionCode.SKILL_IS_STANDARD,
message: t`Cannot update standard skill properties (only activation/deactivation allowed)`,
userFriendlyMessage: msg`Cannot update standard skill properties (only activation/deactivation allowed)`,
});
}
// If only isActive is being updated on a standard skill, allow it
if (
isStandardMetadata(fromFlatSkill) &&
isDefined(isActiveUpdate) &&
!hasNonIsActiveUpdates
) {
return validationResult;
}
const optimisticFlatSkill: FlatSkill = {
...fromFlatSkill,
...fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
updates: flatEntityUpdates,
}),
};
const labelUpdate = findFlatEntityPropertyUpdate({
flatEntityUpdates,
property: 'label',
});
if (isDefined(labelUpdate)) {
validationResult.errors.push(
...validateSkillLabelIsDefined({ flatSkill: optimisticFlatSkill }),
);
}
const contentUpdate = findFlatEntityPropertyUpdate({
flatEntityUpdates,
property: 'content',
});
if (isDefined(contentUpdate)) {
validationResult.errors.push(
...validateSkillContentIsDefined({ flatSkill: optimisticFlatSkill }),
);
}
const nameUpdate = findFlatEntityPropertyUpdate({
flatEntityUpdates,
property: 'name',
});
if (isDefined(nameUpdate)) {
const existingSkills = Object.values(optimisticFlatSkillMaps.byId)
.filter(isDefined)
.filter((skill) => skill.id !== flatEntityId);
validationResult.errors.push(
...validateSkillNameUniqueness({
name: nameUpdate.to,
existingFlatSkills: existingSkills,
}),
);
}
return validationResult;
}
}
@@ -0,0 +1,25 @@
import { msg, t } from '@lingui/core/macro';
import { SkillExceptionCode } from 'src/engine/metadata-modules/skill/skill.exception';
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
export const validateSkillNameUniqueness = ({
name,
existingFlatSkills,
}: {
name: string;
existingFlatSkills: FlatSkill[];
}): FlatEntityValidationError<SkillExceptionCode>[] => {
const errors: FlatEntityValidationError<SkillExceptionCode>[] = [];
if (existingFlatSkills.some((skill) => skill.name === name)) {
errors.push({
code: SkillExceptionCode.SKILL_ALREADY_EXISTS,
message: t`Skill with name "${name}" already exists`,
userFriendlyMessage: msg`A skill with this name already exists`,
});
}
return errors;
};
@@ -0,0 +1,51 @@
import { msg, t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
import { SkillExceptionCode } from 'src/engine/metadata-modules/skill/skill.exception';
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
export const validateSkillLabelIsDefined = ({
flatSkill,
}: {
flatSkill: FlatSkill;
}): FlatEntityValidationError<SkillExceptionCode>[] => {
if (isNonEmptyString(flatSkill.label)) {
return [];
}
return [
{
code: SkillExceptionCode.INVALID_SKILL_INPUT,
message: t`Label cannot be empty`,
userFriendlyMessage: msg`Label cannot be empty`,
},
];
};
export const validateSkillContentIsDefined = ({
flatSkill,
}: {
flatSkill: FlatSkill;
}): FlatEntityValidationError<SkillExceptionCode>[] => {
if (isNonEmptyString(flatSkill.content)) {
return [];
}
return [
{
code: SkillExceptionCode.INVALID_SKILL_INPUT,
message: t`Content cannot be empty`,
userFriendlyMessage: msg`Content cannot be empty`,
},
];
};
export const validateSkillRequiredProperties = ({
flatSkill,
}: {
flatSkill: FlatSkill;
}): FlatEntityValidationError<SkillExceptionCode>[] => [
...validateSkillLabelIsDefined({ flatSkill }),
...validateSkillContentIsDefined({ flatSkill }),
];
@@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
import { FlatAgentValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-agent-validator.service';
import { FlatSkillValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-skill-validator.service';
import { FlatCronTriggerValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-cron-trigger-validator.service';
import { FlatDatabaseEventTriggerValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-database-event-trigger-validator.service';
import { FlatFieldMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service';
@@ -42,6 +43,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
FlatRoleValidatorService,
FlatRoleTargetValidatorService,
FlatAgentValidatorService,
FlatSkillValidatorService,
FlatPageLayoutValidatorService,
FlatPageLayoutWidgetValidatorService,
FlatPageLayoutTabValidatorService,
@@ -65,6 +67,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
FlatRoleValidatorService,
FlatRoleTargetValidatorService,
FlatAgentValidatorService,
FlatSkillValidatorService,
FlatPageLayoutValidatorService,
FlatPageLayoutWidgetValidatorService,
FlatPageLayoutTabValidatorService,
@@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
import { WorkspaceMigrationV2AgentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/agent/workspace-migration-v2-agent-actions-builder.service';
import { WorkspaceMigrationV2SkillActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/skill/workspace-migration-v2-skill-actions-builder.service';
import { WorkspaceMigrationV2CronTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/cron-trigger/workspace-migration-v2-cron-trigger-action-builder.service';
import { WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/database-event-trigger/workspace-migration-v2-database-event-trigger-actions-builder.service';
import { WorkspaceMigrationV2FieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/workspace-migration-v2-field-actions-builder.service';
@@ -43,6 +44,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationV2RoleActionsBuilderService,
WorkspaceMigrationV2RoleTargetActionsBuilderService,
WorkspaceMigrationV2AgentActionsBuilderService,
WorkspaceMigrationV2SkillActionsBuilderService,
WorkspaceMigrationV2PageLayoutActionsBuilderService,
WorkspaceMigrationV2PageLayoutWidgetActionsBuilderService,
WorkspaceMigrationV2PageLayoutTabActionsBuilderService,
@@ -65,6 +67,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationV2RoleActionsBuilderService,
WorkspaceMigrationV2RoleTargetActionsBuilderService,
WorkspaceMigrationV2AgentActionsBuilderService,
WorkspaceMigrationV2SkillActionsBuilderService,
WorkspaceMigrationV2PageLayoutActionsBuilderService,
WorkspaceMigrationV2PageLayoutWidgetActionsBuilderService,
WorkspaceMigrationV2PageLayoutTabActionsBuilderService,
@@ -0,0 +1,59 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { CreateSkillAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/skill/types/workspace-migration-v2-skill-action.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class CreateSkillActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create_skill',
) {
constructor() {
super();
}
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<CreateSkillAction>): Partial<AllFlatEntityMaps> {
const { flatSkillMaps } = allFlatEntityMaps;
const { flatEntity } = action;
const updatedFlatSkillMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity,
flatEntityMaps: flatSkillMaps,
});
return {
flatSkillMaps: updatedFlatSkillMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<CreateSkillAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { flatEntity } = action;
const skillRepository =
queryRunner.manager.getRepository<SkillEntity>(SkillEntity);
await skillRepository.save({
...flatEntity,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<CreateSkillAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,56 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util';
import { DeleteSkillAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/skill/types/workspace-migration-v2-skill-action.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class DeleteSkillActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete_skill',
) {
constructor() {
super();
}
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<DeleteSkillAction>): Partial<AllFlatEntityMaps> {
const { flatSkillMaps } = allFlatEntityMaps;
const { flatEntityId } = action;
const updatedFlatSkillMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({
entityToDeleteId: flatEntityId,
flatEntityMaps: flatSkillMaps,
});
return {
flatSkillMaps: updatedFlatSkillMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<DeleteSkillAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { flatEntityId } = action;
const skillRepository =
queryRunner.manager.getRepository<SkillEntity>(SkillEntity);
await skillRepository.delete({ id: flatEntityId, workspaceId });
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<DeleteSkillAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,71 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { UpdateSkillAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/skill/types/workspace-migration-v2-skill-action.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
@Injectable()
export class UpdateSkillActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'update_skill',
) {
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<UpdateSkillAction>): Partial<AllFlatEntityMaps> {
const { flatSkillMaps } = allFlatEntityMaps;
const { flatEntityId, flatEntityUpdates } = action;
const existingSkill = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId,
flatEntityMaps: flatSkillMaps,
});
const updatedSkill = {
...existingSkill,
...fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
updates: flatEntityUpdates,
}),
};
const updatedFlatSkillMaps = replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: updatedSkill,
flatEntityMaps: flatSkillMaps,
});
return {
flatSkillMaps: updatedFlatSkillMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<UpdateSkillAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { flatEntityId, flatEntityUpdates } = action;
const skillRepository =
queryRunner.manager.getRepository<SkillEntity>(SkillEntity);
await skillRepository.update(
{ id: flatEntityId, workspaceId },
fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
updates: flatEntityUpdates,
}),
);
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<UpdateSkillAction>,
): Promise<void> {
return;
}
}
@@ -4,6 +4,9 @@ import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-sc
import { CreateAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/agent/services/create-agent-action-handler.service';
import { DeleteAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/agent/services/delete-agent-action-handler.service';
import { UpdateAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/agent/services/update-agent-action-handler.service';
import { CreateSkillActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/skill/services/create-skill-action-handler.service';
import { DeleteSkillActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/skill/services/delete-skill-action-handler.service';
import { UpdateSkillActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/skill/services/update-skill-action-handler.service';
import { CreateCronTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/cron-trigger/services/create-cron-trigger-action-handler.service';
import { DeleteCronTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/cron-trigger/services/delete-cron-trigger-action-handler.service';
import { UpdateCronTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/cron-trigger/services/update-cron-trigger-action-handler.service';
@@ -123,6 +126,10 @@ import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/wor
UpdateAgentActionHandlerService,
DeleteAgentActionHandlerService,
CreateSkillActionHandlerService,
UpdateSkillActionHandlerService,
DeleteSkillActionHandlerService,
CreatePageLayoutActionHandlerService,
UpdatePageLayoutActionHandlerService,
DeletePageLayoutActionHandlerService,