refactor(app-marketplace): rename featured to vetted (#22674)

## What

Renames the application-registration "featured" flag to "vetted" across
the backend, frontend, GraphQL schema/DTOs, and the marketplace UI.

"Vetted" better describes what the flag actually does today: it marks an
app as reviewed and approved by the Twenty team (a trust signal), rather
than "featured" which reads as spotlighting/promotion. The admin toggle
description was already "Mark this app as reviewed and approved".

## How

- Renamed `isFeatured` -> `isVetted` on the `ApplicationRegistration`
entity, DTOs (`MarketplaceApp`, `MarketplaceAppDetail`,
`UpdateApplicationRegistrationPayload`), services, GraphQL fragments,
and the settings/admin UI (labels: "Featured" -> "Vetted", "Featured
only" -> "Vetted only", etc.).
- Renamed the `MARKETPLACE_FEATURED_APPLICATIONS` constant/file to
`MARKETPLACE_VETTED_APPLICATIONS`.
- Regenerated GraphQL client artifacts (`generated-metadata`,
`generated-admin`, `twenty-client-sdk`).

### Database

The `isFeatured` column is renamed in place to `isVetted` via a single
2.20 fast instance command (`ALTER TABLE ... RENAME COLUMN`). No new
column, no data-copy backfill.

- Since all 2.19 commands (including the existing `isFeatured` backfill)
complete before any 2.20 command runs, the rename carries over the
values that backfill set.
- The entity uses `@WasRenamedInUpgrade` so the upgrade-aware layer
queries the old column name until the rename step runs during an
upgrade.

## Testing

- `nx typecheck` and `nx lint:diff-with-main` pass for twenty-server and
twenty-front.
- Ran `database:reset` on a fresh dev DB: the 2.19 `isFeatured` backfill
runs first, then the 2.20 rename; the column ends up as `isVetted` (and
`isFeatured` no longer exists), values preserved.
- Booted the server: the `@WasRenamedInUpgrade` decorator validates
against the upgrade sequence, and GraphQL introspection confirms all
four types expose `isVetted` and none expose `isFeatured`.
- Ran the three `graphql:generate` configs and the SDK metadata client
generator so the committed generated files match the generator output
(field ordering included).

## Notes

- The `api-breaking-changes` check flags the removal of the `isFeatured`
GraphQL field — that is expected and inherent to this rename.
- Translation catalogs (`locales/`) are intentionally not touched here
since they are managed via Crowdin; new English strings render via
Lingui's default-message fallback until translated.
This commit is contained in:
martmull
2026-07-08 18:41:58 +02:00
committed by GitHub
parent 163c96c2e5
commit 0ea0c23556
24 changed files with 138 additions and 111 deletions
@@ -1,4 +1,4 @@
export const MARKETPLACE_FEATURED_APPLICATIONS: {
export const MARKETPLACE_VETTED_APPLICATIONS: {
universalIdentifier: string;
position?: number;
}[] = [
@@ -43,7 +43,7 @@ export class MarketplaceAppDetailDTO {
@IsBoolean()
@Field(() => Boolean)
isFeatured: boolean;
isVetted: boolean;
@IsOptional()
@IsString()
@@ -45,5 +45,5 @@ export class MarketplaceAppDTO {
@IsBoolean()
@Field(() => Boolean)
isFeatured: boolean;
isVetted: boolean;
}
@@ -58,7 +58,7 @@ export class MarketplaceCatalogCacheProviderService extends CoreEntityCacheProvi
category: catalogCard.category ?? '',
logo: catalogCard.logoUrl ?? undefined,
sourcePackage: catalogCard.sourcePackage ?? undefined,
isFeatured: catalogCard.isFeatured,
isVetted: catalogCard.isVetted,
};
}
}
@@ -23,10 +23,10 @@ export class MarketplacePublicResolver {
@Query(() => [MarketplaceAppDTO], { name: 'publicMarketplaceApps' })
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
async findManyPublicMarketplaceApps(
@Args('isFeatured', { type: () => Boolean, defaultValue: true })
isFeatured: boolean,
@Args('isVetted', { type: () => Boolean, defaultValue: true })
isVetted: boolean,
): Promise<MarketplaceAppDTO[]> {
return this.marketplaceQueryService.findManyMarketplaceApps(isFeatured);
return this.marketplaceQueryService.findManyMarketplaceApps(isVetted);
}
@Query(() => MarketplaceAppDetailDTO, { name: 'publicMarketplaceAppDetail' })
@@ -23,7 +23,7 @@ export class MarketplaceQueryService {
) {}
async findManyMarketplaceApps(
isFeatured?: boolean,
isVetted?: boolean,
): Promise<MarketplaceAppDTO[]> {
const appsByUniversalIdentifier =
(await this.coreEntityCacheService.get(
@@ -33,11 +33,11 @@ export class MarketplaceQueryService {
const apps = Object.values(appsByUniversalIdentifier);
if (!isDefined(isFeatured)) {
if (!isDefined(isVetted)) {
return apps;
}
return apps.filter((app) => app.isFeatured === isFeatured);
return apps.filter((app) => app.isVetted === isVetted);
}
async findMarketplaceAppDetail(
@@ -78,7 +78,7 @@ export class MarketplaceQueryService {
sourcePackage: registration.sourcePackage ?? undefined,
latestAvailableVersion: registration.latestAvailableVersion ?? undefined,
isListed: registration.isListed,
isFeatured: registration.isFeatured,
isVetted: registration.isVetted,
description:
registration.description ??
registration.manifest?.application?.description ??
@@ -23,6 +23,7 @@ import { ApplicationRegistrationVariableEntity } from 'src/engine/core-modules/a
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
import { WasRenamedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-renamed-in-upgrade.decorator';
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -122,8 +123,15 @@ export class ApplicationRegistrationEntity {
isListed: boolean;
@Field(() => Boolean)
@Column({ name: 'isFeatured', type: 'boolean', default: false })
isFeatured: boolean;
@Column({ name: 'isVetted', type: 'boolean', default: false })
@WasRenamedInUpgrade([
{
previousName: 'isFeatured',
upgradeCommandName:
'2.20.0_RenameIsFeaturedToIsVettedOnApplicationRegistrationFastInstanceCommand_1783527064000',
},
])
isVetted: boolean;
// Auto-installed on every new workspace; existing workspaces are
// backfilled by the `install-pre-installed-apps` CLI command.
@@ -34,7 +34,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
import { ApplicationRegistrationVariableService } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.service';
import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.service';
import { MARKETPLACE_CATALOG_CACHE_ENTITY_ID } from 'src/engine/core-modules/application/application-marketplace/constants/marketplace-apps-cache.constant';
import { MARKETPLACE_FEATURED_APPLICATIONS } from 'src/engine/core-modules/application/application-marketplace/constants/marketplace-featured-applications.constant';
import { MARKETPLACE_VETTED_APPLICATIONS } from 'src/engine/core-modules/application/application-marketplace/constants/marketplace-vetted-applications.constant';
const BCRYPT_SALT_ROUNDS = 10;
@@ -53,7 +53,7 @@ const APPLICATION_REGISTRATION_WITHOUT_MANIFEST_SELECT: (keyof ApplicationRegist
'tarballFileId',
'latestAvailableVersion',
'isListed',
'isFeatured',
'isVetted',
'isPreInstalled',
'logo',
'description',
@@ -74,7 +74,7 @@ export type ApplicationRegistrationCatalogCard = {
universalIdentifier: string;
name: string;
sourcePackage: string | null;
isFeatured: boolean;
isVetted: boolean;
description: string | null;
author: string | null;
category: string | null;
@@ -297,7 +297,7 @@ export class ApplicationRegistrationService {
if (isDefined(update.isListed)) updateData.isListed = update.isListed;
if (isDefined(update.isPreInstalled))
updateData.isPreInstalled = update.isPreInstalled;
if (isDefined(update.isFeatured)) updateData.isFeatured = update.isFeatured;
if (isDefined(update.isVetted)) updateData.isVetted = update.isVetted;
if (Object.keys(updateData).length > 0) {
await this.applicationRegistrationRepository.update(id, updateData);
@@ -407,13 +407,11 @@ export class ApplicationRegistrationService {
params.universalIdentifier,
);
const featuredIdentifiers = new Set(
MARKETPLACE_FEATURED_APPLICATIONS.map(
(entry) => entry.universalIdentifier,
),
const vettedIdentifiers = new Set(
MARKETPLACE_VETTED_APPLICATIONS.map((entry) => entry.universalIdentifier),
);
const isFeatured = featuredIdentifiers.has(params.universalIdentifier);
const isVetted = vettedIdentifiers.has(params.universalIdentifier);
if (isDefined(existing)) {
await this.applicationRegistrationRepository.save({
@@ -422,7 +420,7 @@ export class ApplicationRegistrationService {
sourceType: params.sourceType,
sourcePackage: params.sourcePackage,
latestAvailableVersion: params.latestAvailableVersion,
isFeatured,
isVetted,
manifest: params.manifest,
...fromManifestApplicationToDisplayFields(params.manifest?.application),
});
@@ -434,7 +432,7 @@ export class ApplicationRegistrationService {
sourcePackage: params.sourcePackage,
latestAvailableVersion: params.latestAvailableVersion,
isListed: true,
isFeatured,
isVetted,
manifest: params.manifest,
...fromManifestApplicationToDisplayFields(params.manifest?.application),
oAuthClientId: v4(),
@@ -505,7 +503,7 @@ export class ApplicationRegistrationService {
'universalIdentifier',
'name',
'sourcePackage',
'isFeatured',
'isVetted',
'logo',
'description',
'author',
@@ -522,7 +520,7 @@ export class ApplicationRegistrationService {
universalIdentifier: registration.universalIdentifier,
name: registration.name,
sourcePackage: registration.sourcePackage,
isFeatured: registration.isFeatured,
isVetted: registration.isVetted,
description: registration.description,
author: registration.author,
category: registration.category,
@@ -160,7 +160,7 @@ export class ApplicationTarballService {
...fromManifestApplicationToDisplayFields(manifest.application),
latestAvailableVersion: packageJson?.version ?? null,
isListed: false,
isFeatured: false,
isVetted: false,
oAuthClientId: v4(),
oAuthRedirectUris: [],
oAuthScopes: [],
@@ -198,7 +198,7 @@ export class ApplicationTarballService {
...fromManifestApplicationToDisplayFields(manifest.application),
latestAvailableVersion: packageJson?.version ?? null,
isListed: false,
isFeatured: false,
isVetted: false,
ownerWorkspaceId: params.ownerWorkspaceId,
} as QueryDeepPartialEntity<ApplicationRegistrationEntity>);
@@ -50,7 +50,7 @@ export class UpdateApplicationRegistrationPayload {
@Field(() => Boolean, { nullable: true })
@IsBoolean()
@IsOptional()
isFeatured?: boolean;
isVetted?: boolean;
}
@InputType()