feat(server): promote registration display fields to first-class columns (#22513)

Part of the application settings architecture work:
https://github.com/twentyhq/core-team-issues/issues/2456 — follow-up to
#22453, delivering the promised removal of the temporary manifest load.

Display data (description, author, category, websiteUrl,
aboutDescription, termsUrl, emailSupport, issueReportUrl, screenshots)
only existed inside the `manifest` jsonb, forcing hot paths to load it.
This PR:

- Promotes those 9 fields to first-class columns on
`applicationRegistration`, populated at every ingestion point
(`updateFromManifest`, both `upsertFromCatalog` branches) — fast command
creates the columns at deploy, slow command backfills them from the
manifest.
- `findManyListedCatalogCards()` (marketplace list) now selects only
scalar columns — the manifest jsonb is no longer loaded there.
- `findPublicByClientId()` (OAuth consent page) now selects `id, name,
logo, websiteUrl, oAuthScopes` — no manifest.
- The narrow select used by
`findMany`/`findAll`/`findOneById`/`findOneByIdGlobal` includes the new
columns.
- GraphQL surface unchanged (no new fields); the marketplace detail
endpoint still reads the manifest and is slimmed in the next PR.

Verified: migration applied via the real runner (both commands recorded
completed), backfill SQL exercised against live rows (full + minimal
manifests), migration generator reports no pending schema changes,
typecheck, lint, unit suites (application-registration + marketplace
15/15).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_011sST4rPLU1Koi2oVGi84ei

---
_Generated by [Claude
Code](https://claude.ai/code/session_011sST4rPLU1Koi2oVGi84ei)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22513?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
martmull
2026-07-03 14:47:24 +02:00
committed by GitHub
parent 5d3b6d05b3
commit 0992d0b748
9 changed files with 236 additions and 18 deletions
@@ -93,7 +93,7 @@ export class MarketplaceQueryService {
): MarketplaceAppDTO {
return {
id: catalogCard.universalIdentifier,
name: catalogCard.displayName ?? catalogCard.name,
name: catalogCard.name,
description: catalogCard.description ?? '',
author: catalogCard.author ?? 'Unknown',
category: catalogCard.category ?? '',
@@ -141,6 +141,69 @@ export class ApplicationRegistrationEntity {
})
logo: string | null;
@Column({ nullable: true, type: 'text' })
@WasIntroducedInUpgrade({
upgradeCommandName:
'2.19.0_AddDisplayFieldsToApplicationRegistrationFastInstanceCommand_1783073776590',
})
description: string | null;
@Column({ nullable: true, type: 'text' })
@WasIntroducedInUpgrade({
upgradeCommandName:
'2.19.0_AddDisplayFieldsToApplicationRegistrationFastInstanceCommand_1783073776590',
})
author: string | null;
@Column({ nullable: true, type: 'text' })
@WasIntroducedInUpgrade({
upgradeCommandName:
'2.19.0_AddDisplayFieldsToApplicationRegistrationFastInstanceCommand_1783073776590',
})
category: string | null;
@Column({ nullable: true, type: 'text' })
@WasIntroducedInUpgrade({
upgradeCommandName:
'2.19.0_AddDisplayFieldsToApplicationRegistrationFastInstanceCommand_1783073776590',
})
websiteUrl: string | null;
@Column({ nullable: true, type: 'text' })
@WasIntroducedInUpgrade({
upgradeCommandName:
'2.19.0_AddDisplayFieldsToApplicationRegistrationFastInstanceCommand_1783073776590',
})
aboutDescription: string | null;
@Column({ nullable: true, type: 'text' })
@WasIntroducedInUpgrade({
upgradeCommandName:
'2.19.0_AddDisplayFieldsToApplicationRegistrationFastInstanceCommand_1783073776590',
})
termsUrl: string | null;
@Column({ nullable: true, type: 'text' })
@WasIntroducedInUpgrade({
upgradeCommandName:
'2.19.0_AddDisplayFieldsToApplicationRegistrationFastInstanceCommand_1783073776590',
})
emailSupport: string | null;
@Column({ nullable: true, type: 'text' })
@WasIntroducedInUpgrade({
upgradeCommandName:
'2.19.0_AddDisplayFieldsToApplicationRegistrationFastInstanceCommand_1783073776590',
})
issueReportUrl: string | null;
@Column({ type: 'text', array: true, default: '{}' })
@WasIntroducedInUpgrade({
upgradeCommandName:
'2.19.0_AddDisplayFieldsToApplicationRegistrationFastInstanceCommand_1783073776590',
})
screenshots: string[];
@Field(() => String, { nullable: true })
get logoUrl(): string | null {
return this.logo ?? this.manifest?.application?.logoUrl ?? null;
@@ -25,6 +25,7 @@ import {
type UpdateApplicationRegistrationPayload,
} from 'src/engine/core-modules/application/application-registration/dtos/update-application-registration.input';
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
import { fromManifestApplicationToDisplayFields } from 'src/engine/core-modules/application/application-registration/utils/from-manifest-application-to-display-fields.util';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { validateRedirectUri } from 'src/engine/core-modules/auth/utils/validate-redirect-uri.util';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -51,6 +52,15 @@ const APPLICATION_REGISTRATION_WITHOUT_MANIFEST_SELECT: (keyof ApplicationRegist
'isFeatured',
'isPreInstalled',
'logo',
'description',
'author',
'category',
'websiteUrl',
'aboutDescription',
'termsUrl',
'emailSupport',
'issueReportUrl',
'screenshots',
'createdAt',
'updatedAt',
];
@@ -61,7 +71,6 @@ export type ApplicationRegistrationCatalogCard = {
name: string;
sourcePackage: string | null;
isFeatured: boolean;
displayName: string | null;
description: string | null;
author: string | null;
category: string | null;
@@ -147,7 +156,7 @@ export class ApplicationRegistrationService {
): Promise<PublicApplicationRegistrationDTO | null> {
const registration = await this.applicationRegistrationRepository.findOne({
where: { oAuthClientId: clientId },
select: ['id', 'name', 'manifest', 'oAuthScopes'],
select: ['id', 'name', 'logo', 'websiteUrl', 'oAuthScopes'],
});
if (!registration) {
@@ -157,8 +166,8 @@ export class ApplicationRegistrationService {
return {
id: registration.id,
name: registration.name,
logoUrl: registration.manifest?.application?.logoUrl ?? null,
websiteUrl: registration.manifest?.application?.websiteUrl ?? null,
logoUrl: registration.logo,
websiteUrl: registration.websiteUrl,
oAuthScopes: registration.oAuthScopes,
};
}
@@ -290,7 +299,7 @@ export class ApplicationRegistrationService {
...existing,
name: manifest.application.displayName,
manifest,
logo: manifest.application.logoUrl ?? null,
...fromManifestApplicationToDisplayFields(manifest.application),
...(sourceType !== undefined && { sourceType }),
});
}
@@ -360,7 +369,7 @@ export class ApplicationRegistrationService {
sourcePackage: params.sourcePackage,
latestAvailableVersion: params.latestAvailableVersion,
manifest: params.manifest,
logo: params.manifest?.application?.logoUrl ?? null,
...fromManifestApplicationToDisplayFields(params.manifest?.application),
isFeatured,
});
} else {
@@ -373,7 +382,7 @@ export class ApplicationRegistrationService {
isListed: true,
isFeatured,
manifest: params.manifest,
logo: params.manifest?.application?.logoUrl ?? null,
...fromManifestApplicationToDisplayFields(params.manifest?.application),
oAuthClientId: v4(),
oAuthRedirectUris: [],
oAuthScopes: [],
@@ -430,6 +439,17 @@ export class ApplicationRegistrationService {
ApplicationRegistrationCatalogCard[]
> {
const registrations = await this.applicationRegistrationRepository.find({
select: [
'id',
'universalIdentifier',
'name',
'sourcePackage',
'isFeatured',
'logo',
'description',
'author',
'category',
],
where: {
isListed: true,
sourceType: ApplicationRegistrationSourceType.NPM,
@@ -442,14 +462,10 @@ export class ApplicationRegistrationService {
name: registration.name,
sourcePackage: registration.sourcePackage,
isFeatured: registration.isFeatured,
displayName: registration.manifest?.application?.displayName ?? null,
description: registration.manifest?.application?.description ?? null,
author: registration.manifest?.application?.author ?? null,
category: registration.manifest?.application?.category ?? null,
logoUrl:
registration.logo ??
registration.manifest?.application?.logoUrl ??
null,
description: registration.description,
author: registration.author,
category: registration.category,
logoUrl: registration.logo,
}));
}
@@ -18,6 +18,7 @@ import {
ApplicationRegistrationExceptionCode,
} from 'src/engine/core-modules/application/application-registration/application-registration.exception';
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
import { fromManifestApplicationToDisplayFields } from 'src/engine/core-modules/application/application-registration/utils/from-manifest-application-to-display-fields.util';
import { extractTarballSecurely } from 'src/engine/core-modules/application/application-package/utils/extract-tarball-securely.util';
import { readJsonFile } from 'src/engine/core-modules/application/application-package/utils/read-json-file.util';
import { resolvePackageContentDir } from 'src/engine/core-modules/application/application-package/utils/tarball-utils';
@@ -170,6 +171,7 @@ export class ApplicationTarballService {
name: manifest.application?.displayName ?? 'Unknown App',
sourceType: ApplicationRegistrationSourceType.TARBALL,
manifest,
...fromManifestApplicationToDisplayFields(manifest.application),
latestAvailableVersion: packageJson?.version ?? null,
isListed: false,
isFeatured: false,
@@ -207,6 +209,7 @@ export class ApplicationTarballService {
tarballFileId: savedFile.id,
name: manifest.application?.displayName ?? 'Unknown App',
manifest,
...fromManifestApplicationToDisplayFields(manifest.application),
latestAvailableVersion: packageJson?.version ?? null,
isListed: false,
isFeatured: false,
@@ -0,0 +1,16 @@
import { type ApplicationManifest } from 'twenty-shared/application';
export const fromManifestApplicationToDisplayFields = (
application: ApplicationManifest | undefined,
) => ({
logo: application?.logoUrl ?? null,
description: application?.description ?? null,
author: application?.author ?? null,
category: application?.category ?? null,
websiteUrl: application?.websiteUrl ?? null,
aboutDescription: application?.aboutDescription ?? null,
termsUrl: application?.termsUrl ?? null,
emailSupport: application?.emailSupport ?? null,
issueReportUrl: application?.issueReportUrl ?? null,
screenshots: application?.screenshots ?? [],
});