From d126d54bbcdda561591f9d5dc39d15457d7b7624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Thu, 26 Mar 2026 08:53:11 +0100 Subject: [PATCH] feat: make workflow objects searchable (#18906) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Adds `isSearchable: true` to the workflow standard object definition so new workspaces get searchable workflows automatically - Adds a `upgrade:1-20:make-workflow-searchable` migration command that flips the `isSearchable` flag on the `objectMetadata` row for existing workspaces, with proper cache invalidation and metadata version increment - Registers the command in the 1-20 upgrade module and the `upgrade.command.ts` orchestrator The `searchVector` stored generated column already exists on the workflow table, so no data backfill is needed — this is purely a metadata flag change that makes the search service include workflows in results. ## Test plan - [x] `--dry-run` logs what it would do without making changes - [x] Actual run updates both workspaces and invalidates caches - [x] Idempotent: re-running skips already-searchable workspaces - [x] Typecheck passes - [x] Lint passes on changed files Made with [Cursor](https://cursor.com) --- .../1-20-make-workflow-searchable.command.ts | 87 +++++++++++++++++++ .../1-20-upgrade-version-command.module.ts | 3 + .../upgrade.command.ts | 3 + ...eate-standard-flat-object-metadata.util.ts | 1 + .../search-resolver.integration-spec.ts | 1 + 5 files changed, 95 insertions(+) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-workflow-searchable.command.ts diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-workflow-searchable.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-workflow-searchable.command.ts new file mode 100644 index 0000000000..0451b709cf --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-make-workflow-searchable.command.ts @@ -0,0 +1,87 @@ +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { DataSource, Repository } from 'typeorm'; + +import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; + +@Command({ + name: 'upgrade:1-20:make-workflow-searchable', + description: 'Set isSearchable to true on the workflow object metadata', +}) +export class MakeWorkflowSearchableCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + @InjectDataSource() + private readonly coreDataSource: DataSource, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, + protected readonly dataSourceService: DataSourceService, + private readonly workspaceCacheService: WorkspaceCacheService, + private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, + private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, + ) { + super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + } + + override async runOnWorkspace({ + workspaceId, + options, + }: RunOnWorkspaceArgs): Promise { + const isDryRun = options.dryRun ?? false; + + if (isDryRun) { + this.logger.log( + `[DRY RUN] Would set isSearchable=true on workflow object for workspace ${workspaceId}. Skipping.`, + ); + + return; + } + + const queryRunner = this.coreDataSource.createQueryRunner(); + + await queryRunner.connect(); + + try { + const result = await queryRunner.query( + `UPDATE core."objectMetadata" + SET "isSearchable" = true + WHERE "workspaceId" = $1 + AND "nameSingular" = 'workflow' + AND "isSearchable" = false`, + [workspaceId], + ); + + const updatedCount = result?.[1] ?? 0; + + if (updatedCount > 0) { + this.logger.log( + `Set isSearchable=true on workflow object for workspace ${workspaceId}`, + ); + + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'flatObjectMetadataMaps', + ]); + + await this.workspaceMetadataVersionService.incrementMetadataVersion( + workspaceId, + ); + + await this.workspaceCacheStorageService.flush(workspaceId); + } else { + this.logger.log( + `Workflow already searchable or not found for workspace ${workspaceId}, skipping`, + ); + } + } finally { + await queryRunner.release(); + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-upgrade-version-command.module.ts index 9c0f371770..5f81022cf3 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-upgrade-version-command.module.ts @@ -12,6 +12,7 @@ import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/u import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command'; import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command'; import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command'; +import { MakeWorkflowSearchableCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-workflow-searchable.command'; import { MigrateMessagingInfrastructureToMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command'; import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command'; import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command'; @@ -76,6 +77,7 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo MigrateRichTextToTextCommand, MigrateMessagingInfrastructureToMetadataCommand, UpdateStandardIndexViewNamesCommand, + MakeWorkflowSearchableCommand, ], exports: [ IdentifyPermissionFlagMetadataCommand, @@ -93,6 +95,7 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo MigrateRichTextToTextCommand, MigrateMessagingInfrastructureToMetadataCommand, UpdateStandardIndexViewNamesCommand, + MakeWorkflowSearchableCommand, ], }) export class V1_20_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index bf59c80ffe..9af02deb51 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -43,6 +43,7 @@ import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/u import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command'; import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command'; import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command'; +import { MakeWorkflowSearchableCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-workflow-searchable.command'; import { MigrateMessagingInfrastructureToMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command'; import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command'; import { GenerateApplicationSdkClientsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-generate-application-sdk-clients.command'; @@ -113,6 +114,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { protected readonly backfillFieldWidgetsCommand: BackfillFieldWidgetsCommand, protected readonly backfillSelectFieldOptionIdsCommand: BackfillSelectFieldOptionIdsCommand, protected readonly updateStandardIndexViewNamesCommand: UpdateStandardIndexViewNamesCommand, + protected readonly makeWorkflowSearchableCommand: MakeWorkflowSearchableCommand, ) { super( workspaceRepository, @@ -176,6 +178,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { this.backfillFieldWidgetsCommand, this.backfillSelectFieldOptionIdsCommand, this.updateStandardIndexViewNamesCommand, + this.makeWorkflowSearchableCommand, this.generateApplicationSdkClientsCommand, ]; diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-flat-object-metadata.util.ts index 77855d5a86..750312d8f3 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-flat-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-flat-object-metadata.util.ts @@ -761,6 +761,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = { labelPlural: i18nLabel(msg`Workflows`), description: i18nLabel(msg`A workflow`), icon: 'IconSettingsAutomation', + isSearchable: true, shortcut: 'W', labelIdentifierFieldMetadataName: 'name', }, diff --git a/packages/twenty-server/test/integration/graphql/suites/search/search-resolver.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/search/search-resolver.integration-spec.ts index 582e00d0bb..7e872123da 100644 --- a/packages/twenty-server/test/integration/graphql/suites/search/search-resolver.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/search/search-resolver.integration-spec.ts @@ -173,6 +173,7 @@ describe('SearchResolver', () => { await deleteAllRecords('noteTarget'); await deleteAllRecords('taskTarget'); await deleteAllRecords('dashboard'); + await deleteAllRecords('workflow'); await deleteAllRecords('_pet'); await deleteAllRecords('_surveyResult'); await deleteAllRecords('_rocket');