feat: expose marketplace app detail fields explicitly and deprecate manifest blob (#22526)
Part of the application settings architecture work: https://github.com/twentyhq/core-team-issues/issues/2456 — follow-up to #22513. `MarketplaceAppDetail` returned the entire `manifest` jsonb (100KB+) over GraphQL and the front dug display fields and roles out of it. This PR: - Adds explicit fields to `MarketplaceAppDetail`: `description, author, category, logo, websiteUrl, aboutDescription, termsUrl, emailSupport, issueReportUrl, screenshots, defaultRoleUniversalIdentifier`, sourced from the registration columns introduced in #22513, and `roles: [MarketplaceAppRole!]` (full permission shape — the permissions tab and install modal render object/field permissions), sourced from the manifest at detail time. - Marks the `manifest` field `@deprecated` (kept functional — removal would be a breaking change). - Front: the shared `marketplaceAppDetailFragment` no longer selects `manifest`; display and role reads are flattened across `SettingsAvailableApplicationDetails`, `SettingsApplicationDetails`, and the share-link buttons. The three consumers that genuinely need deep manifest structure (content-tab counts/`manifestContent`, permissions objects, `useApplicationManifest` page-layout/view reads) use a scoped `FindMarketplaceAppManifest` query until the manifest demotion PR removes that need. - Codegen regenerated where the documents live: front metadata config + twenty-client-sdk metadata client (data/admin configs verified untouched). Verified: server+front typecheck, lint (0 warnings), server marketplace suite 10/10, front marketplace/applications suites 41/41, live schema introspection confirms the new fields and the deprecation. <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22526?utm_source=github" rel="nofollow noreferrer noopener" target="_blank">``<img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">``</a>
This commit is contained in:
+63
-1
@@ -4,6 +4,7 @@ import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
|
||||
import { MarketplaceAppRoleDTO } from 'src/engine/core-modules/application/application-marketplace/dtos/marketplace-app-role.dto';
|
||||
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
|
||||
|
||||
@ObjectType('MarketplaceAppDetail')
|
||||
@@ -44,6 +45,67 @@ export class MarketplaceAppDetailDTO {
|
||||
@Field(() => Boolean)
|
||||
isFeatured: boolean;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
author?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
category?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
logo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
websiteUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
aboutDescription?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
termsUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
emailSupport?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
issueReportUrl?: string;
|
||||
|
||||
@Field(() => [String])
|
||||
screenshots: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
defaultRoleUniversalIdentifier?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => [MarketplaceAppRoleDTO], { nullable: true })
|
||||
roles?: MarketplaceAppRoleDTO[];
|
||||
|
||||
@Field(() => GraphQLJSON, {
|
||||
nullable: true,
|
||||
deprecationReason:
|
||||
'Use the explicit MarketplaceAppDetail fields (description, author, roles, ...) instead',
|
||||
})
|
||||
manifest?: Manifest;
|
||||
}
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
@ObjectType('MarketplaceAppRoleObjectPermission')
|
||||
export class MarketplaceAppRoleObjectPermissionDTO {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
universalIdentifier: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
objectUniversalIdentifier: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canReadObjectRecords?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canUpdateObjectRecords?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canSoftDeleteObjectRecords?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canDestroyObjectRecords?: boolean;
|
||||
}
|
||||
|
||||
@ObjectType('MarketplaceAppRoleFieldPermission')
|
||||
export class MarketplaceAppRoleFieldPermissionDTO {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
universalIdentifier: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
objectUniversalIdentifier: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
fieldUniversalIdentifier: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canReadFieldValue?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canUpdateFieldValue?: boolean;
|
||||
}
|
||||
|
||||
@ObjectType('MarketplaceAppRole')
|
||||
export class MarketplaceAppRoleDTO {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
universalIdentifier: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
label: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canUpdateAllSettings?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canAccessAllTools?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canReadAllObjectRecords?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canUpdateAllObjectRecords?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canSoftDeleteAllObjectRecords?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canDestroyAllObjectRecords?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => [String], { nullable: true })
|
||||
permissionFlagUniversalIdentifiers?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => [MarketplaceAppRoleObjectPermissionDTO], { nullable: true })
|
||||
objectPermissions?: MarketplaceAppRoleObjectPermissionDTO[];
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => [MarketplaceAppRoleFieldPermissionDTO], { nullable: true })
|
||||
fieldPermissions?: MarketplaceAppRoleFieldPermissionDTO[];
|
||||
}
|
||||
+76
-1
@@ -1,6 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type RoleManifest } from 'twenty-shared/application';
|
||||
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
|
||||
|
||||
import { type ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
import {
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
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';
|
||||
import { MarketplaceAppRoleDTO } from 'src/engine/core-modules/application/application-marketplace/dtos/marketplace-app-role.dto';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
@@ -115,7 +117,80 @@ export class MarketplaceQueryService {
|
||||
latestAvailableVersion: registration.latestAvailableVersion ?? undefined,
|
||||
isListed: registration.isListed,
|
||||
isFeatured: registration.isFeatured,
|
||||
description:
|
||||
registration.description ??
|
||||
registration.manifest?.application?.description ??
|
||||
undefined,
|
||||
author:
|
||||
registration.author ??
|
||||
registration.manifest?.application?.author ??
|
||||
undefined,
|
||||
category:
|
||||
registration.category ??
|
||||
registration.manifest?.application?.category ??
|
||||
undefined,
|
||||
logo: registration.logoUrl ?? undefined,
|
||||
websiteUrl:
|
||||
registration.websiteUrl ??
|
||||
registration.manifest?.application?.websiteUrl ??
|
||||
undefined,
|
||||
aboutDescription:
|
||||
registration.aboutDescription ??
|
||||
registration.manifest?.application?.aboutDescription ??
|
||||
undefined,
|
||||
termsUrl:
|
||||
registration.termsUrl ??
|
||||
registration.manifest?.application?.termsUrl ??
|
||||
undefined,
|
||||
emailSupport:
|
||||
registration.emailSupport ??
|
||||
registration.manifest?.application?.emailSupport ??
|
||||
undefined,
|
||||
issueReportUrl:
|
||||
registration.issueReportUrl ??
|
||||
registration.manifest?.application?.issueReportUrl ??
|
||||
undefined,
|
||||
screenshots: isNonEmptyArray(registration.screenshots)
|
||||
? registration.screenshots
|
||||
: (registration.manifest?.application?.screenshots ?? []),
|
||||
defaultRoleUniversalIdentifier:
|
||||
registration.manifest?.application?.defaultRoleUniversalIdentifier,
|
||||
roles: registration.manifest?.roles?.map((role) =>
|
||||
this.toMarketplaceAppRoleDTO(role),
|
||||
),
|
||||
manifest: registration.manifest ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private toMarketplaceAppRoleDTO(role: RoleManifest): MarketplaceAppRoleDTO {
|
||||
return {
|
||||
universalIdentifier: role.universalIdentifier,
|
||||
label: role.label,
|
||||
description: role.description,
|
||||
icon: role.icon,
|
||||
canUpdateAllSettings: role.canUpdateAllSettings,
|
||||
canAccessAllTools: role.canAccessAllTools,
|
||||
canReadAllObjectRecords: role.canReadAllObjectRecords,
|
||||
canUpdateAllObjectRecords: role.canUpdateAllObjectRecords,
|
||||
canSoftDeleteAllObjectRecords: role.canSoftDeleteAllObjectRecords,
|
||||
canDestroyAllObjectRecords: role.canDestroyAllObjectRecords,
|
||||
permissionFlagUniversalIdentifiers:
|
||||
role.permissionFlagUniversalIdentifiers,
|
||||
objectPermissions: role.objectPermissions?.map((permission) => ({
|
||||
universalIdentifier: permission.universalIdentifier,
|
||||
objectUniversalIdentifier: permission.objectUniversalIdentifier,
|
||||
canReadObjectRecords: permission.canReadObjectRecords,
|
||||
canUpdateObjectRecords: permission.canUpdateObjectRecords,
|
||||
canSoftDeleteObjectRecords: permission.canSoftDeleteObjectRecords,
|
||||
canDestroyObjectRecords: permission.canDestroyObjectRecords,
|
||||
})),
|
||||
fieldPermissions: role.fieldPermissions?.map((permission) => ({
|
||||
universalIdentifier: permission.universalIdentifier,
|
||||
objectUniversalIdentifier: permission.objectUniversalIdentifier,
|
||||
fieldUniversalIdentifier: permission.fieldUniversalIdentifier,
|
||||
canReadFieldValue: permission.canReadFieldValue,
|
||||
canUpdateFieldValue: permission.canUpdateFieldValue,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user