diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-19/2-19-instance-command-fast-1783069672191-add-logo-to-application-registration.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-19/2-19-instance-command-fast-1783069672191-add-logo-to-application-registration.ts new file mode 100644 index 0000000000..e8dea5e284 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-19/2-19-instance-command-fast-1783069672191-add-logo-to-application-registration.ts @@ -0,0 +1,21 @@ +import { QueryRunner } from 'typeorm'; + +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; +import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface'; + +@RegisteredInstanceCommand('2.19.0', 1783069672191) +export class AddLogoToApplicationRegistrationFastInstanceCommand + implements FastInstanceCommand +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE "core"."applicationRegistration" ADD COLUMN IF NOT EXISTS "logo" text', + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE "core"."applicationRegistration" DROP COLUMN "logo"', + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-19/2-19-instance-command-slow-1783069673191-backfill-logo-on-application-registration.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-19/2-19-instance-command-slow-1783069673191-backfill-logo-on-application-registration.ts new file mode 100644 index 0000000000..72e7ee5825 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-19/2-19-instance-command-slow-1783069673191-backfill-logo-on-application-registration.ts @@ -0,0 +1,23 @@ +import { DataSource, QueryRunner } from 'typeorm'; + +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; +import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface'; + +@RegisteredInstanceCommand('2.19.0', 1783069673191, { type: 'slow' }) +export class BackfillLogoOnApplicationRegistrationSlowInstanceCommand + implements SlowInstanceCommand +{ + async runDataMigration(dataSource: DataSource): Promise { + await dataSource.query( + `UPDATE "core"."applicationRegistration" SET "logo" = "manifest"->'application'->>'logoUrl' WHERE "manifest" IS NOT NULL AND "logo" IS NULL`, + ); + } + + public async up(_queryRunner: QueryRunner): Promise {} + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'UPDATE "core"."applicationRegistration" SET "logo" = NULL', + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts index ee2be9706c..9907561f13 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts @@ -91,6 +91,8 @@ import { AddViewKanbanColumnWidthFastInstanceCommand } from './2-15/2-15-instanc import { AddPendingQuestionMessageIdToAgentChatThreadFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-19/2-19-instance-command-fast-1782999138000-add-pending-question-to-agent-chat-thread'; import { AddWorkspaceDiscoverabilityToWorkspaceFastInstanceCommand } from './2-19/2-19-instance-command-fast-1783004140000-add-workspace-discoverability-to-workspace'; import { DropMetadataStandardOverridesColumnFastInstanceCommand } from './2-20/2-20-instance-command-fast-1825000000000-drop-metadata-standard-overrides-column'; +import { AddLogoToApplicationRegistrationFastInstanceCommand } from './2-19/2-19-instance-command-fast-1783069672191-add-logo-to-application-registration'; +import { BackfillLogoOnApplicationRegistrationSlowInstanceCommand } from './2-19/2-19-instance-command-slow-1783069673191-backfill-logo-on-application-registration'; export const INSTANCE_COMMANDS = [ AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand, @@ -184,4 +186,6 @@ export const INSTANCE_COMMANDS = [ AddLastStreamErrorToAgentChatThreadFastInstanceCommand, DropMetadataStandardOverridesColumnFastInstanceCommand, AddTypeAndOptionsToApplicationVariablesFastInstanceCommand, + AddLogoToApplicationRegistrationFastInstanceCommand, + BackfillLogoOnApplicationRegistrationSlowInstanceCommand, ]; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace-query.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace-query.service.ts index 0af7a3a15b..71711c4dd4 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace-query.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace-query.service.ts @@ -8,7 +8,10 @@ import { ApplicationRegistrationExceptionCode, } from 'src/engine/core-modules/application/application-registration/application-registration.exception'; import { ApplicationRegistrationVariableService } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.service'; -import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service'; +import { + type ApplicationRegistrationCatalogCard, + ApplicationRegistrationService, +} from 'src/engine/core-modules/application/application-registration/application-registration.service'; import { MarketplaceCatalogSyncCronJob } from 'src/engine/core-modules/application/application-marketplace/crons/marketplace-catalog-sync.cron.job'; import { MarketplaceAppDTO } from 'src/engine/core-modules/application/application-marketplace/dtos/marketplace-app.dto'; import { MarketplaceAppDetailDTO } from 'src/engine/core-modules/application/application-marketplace/dtos/marketplace-app-detail.dto'; @@ -30,7 +33,7 @@ export class MarketplaceQueryService { async findManyMarketplaceApps(): Promise { const registrations = - await this.applicationRegistrationService.findManyListed(); + await this.applicationRegistrationService.findManyListedCatalogCards(); if (registrations.length === 0) { if (!this.hasSyncBeenEnqueued) { @@ -86,19 +89,17 @@ export class MarketplaceQueryService { } private toMarketplaceAppDTO( - registration: ApplicationRegistrationEntity, + catalogCard: ApplicationRegistrationCatalogCard, ): MarketplaceAppDTO { - const app = registration.manifest?.application; - return { - id: registration.universalIdentifier, - name: app?.displayName ?? registration.name, - description: app?.description ?? '', - author: `${app?.author ?? 'Unknown'}`, - category: app?.category ?? '', - logo: app?.logoUrl ?? undefined, - sourcePackage: registration.sourcePackage ?? undefined, - isFeatured: registration.isFeatured, + id: catalogCard.universalIdentifier, + name: catalogCard.displayName ?? catalogCard.name, + description: catalogCard.description ?? '', + author: catalogCard.author ?? 'Unknown', + category: catalogCard.category ?? '', + logo: catalogCard.logoUrl ?? undefined, + sourcePackage: catalogCard.sourcePackage ?? undefined, + isFeatured: catalogCard.isFeatured, }; } diff --git a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.entity.ts b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.entity.ts index 7708e78ec7..2a26f31c7b 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.entity.ts @@ -22,6 +22,7 @@ import { type Manifest } from 'twenty-shared/application'; import { ApplicationRegistrationVariableEntity } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.entity'; 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 { UserEntity } from 'src/engine/core-modules/user/user.entity'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -133,9 +134,16 @@ export class ApplicationRegistrationEntity { @Column({ type: 'jsonb', nullable: true }) manifest: Manifest | null; + @Column({ nullable: true, type: 'text' }) + @WasIntroducedInUpgrade({ + upgradeCommandName: + '2.19.0_AddLogoToApplicationRegistrationFastInstanceCommand_1783069672191', + }) + logo: string | null; + @Field(() => String, { nullable: true }) get logoUrl(): string | null { - return this.manifest?.application?.logoUrl ?? null; + return this.logo ?? this.manifest?.application?.logoUrl ?? null; } @OneToMany( diff --git a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.service.ts index 9c957a9e37..5fc8905004 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.service.ts @@ -33,6 +33,41 @@ import { MARKETPLACE_CURATED_APPLICATIONS } from 'src/engine/core-modules/applic const BCRYPT_SALT_ROUNDS = 10; +const APPLICATION_REGISTRATION_WITHOUT_MANIFEST_SELECT: (keyof ApplicationRegistrationEntity)[] = + [ + 'id', + 'universalIdentifier', + 'name', + 'oAuthClientId', + 'oAuthRedirectUris', + 'oAuthScopes', + 'createdByUserId', + 'ownerWorkspaceId', + 'sourceType', + 'sourcePackage', + 'tarballFileId', + 'latestAvailableVersion', + 'isListed', + 'isFeatured', + 'isPreInstalled', + 'logo', + 'createdAt', + 'updatedAt', + ]; + +export type ApplicationRegistrationCatalogCard = { + id: string; + universalIdentifier: string; + name: string; + sourcePackage: string | null; + isFeatured: boolean; + displayName: string | null; + description: string | null; + author: string | null; + category: string | null; + logoUrl: string | null; +}; + @Injectable() export class ApplicationRegistrationService { constructor( @@ -49,6 +84,7 @@ export class ApplicationRegistrationService { ownerWorkspaceId: string, ): Promise { return this.applicationRegistrationRepository.find({ + select: APPLICATION_REGISTRATION_WITHOUT_MANIFEST_SELECT, where: { ownerWorkspaceId }, order: { createdAt: 'DESC' }, }); @@ -56,6 +92,7 @@ export class ApplicationRegistrationService { async findAll(): Promise { return this.applicationRegistrationRepository.find({ + select: APPLICATION_REGISTRATION_WITHOUT_MANIFEST_SELECT, order: { createdAt: 'DESC' }, }); } @@ -65,6 +102,7 @@ export class ApplicationRegistrationService { ownerWorkspaceId: string, ): Promise { const registration = await this.applicationRegistrationRepository.findOne({ + select: APPLICATION_REGISTRATION_WITHOUT_MANIFEST_SELECT, where: { id, ownerWorkspaceId }, }); @@ -80,6 +118,7 @@ export class ApplicationRegistrationService { async findOneByIdGlobal(id: string): Promise { const registration = await this.applicationRegistrationRepository.findOne({ + select: APPLICATION_REGISTRATION_WITHOUT_MANIFEST_SELECT, where: { id }, }); @@ -251,6 +290,7 @@ export class ApplicationRegistrationService { ...existing, name: manifest.application.displayName, manifest, + logo: manifest.application.logoUrl ?? null, ...(sourceType !== undefined && { sourceType }), }); } @@ -320,6 +360,7 @@ export class ApplicationRegistrationService { sourcePackage: params.sourcePackage, latestAvailableVersion: params.latestAvailableVersion, manifest: params.manifest, + logo: params.manifest?.application?.logoUrl ?? null, isFeatured, }); } else { @@ -332,6 +373,7 @@ export class ApplicationRegistrationService { isListed: true, isFeatured, manifest: params.manifest, + logo: params.manifest?.application?.logoUrl ?? null, oAuthClientId: v4(), oAuthRedirectUris: [], oAuthScopes: [], @@ -384,13 +426,31 @@ export class ApplicationRegistrationService { return this.applicationRegistrationRepository.save(registration); } - async findManyListed(): Promise { - return this.applicationRegistrationRepository.find({ + async findManyListedCatalogCards(): Promise< + ApplicationRegistrationCatalogCard[] + > { + const registrations = await this.applicationRegistrationRepository.find({ where: { isListed: true, sourceType: ApplicationRegistrationSourceType.NPM, }, }); + + return registrations.map((registration) => ({ + id: registration.id, + universalIdentifier: registration.universalIdentifier, + 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, + })); } async getStats(