Provide applicatiion assets (#18973)
- improve backend - improve frontend <img width="1293" height="824" alt="image" src="https://github.com/user-attachments/assets/7a4633f1-85cd-4126-b058-dbeae6ba2218" />
This commit is contained in:
+24
-46
@@ -24,6 +24,7 @@ import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/
|
||||
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service';
|
||||
import { ApplicationTokenPairDTO } from 'src/engine/core-modules/application/application-oauth/dtos/application-token-pair.dto';
|
||||
import { ApplicationRegistrationVariableService } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.service';
|
||||
import { resolveManifestAssetUrls } from 'src/engine/core-modules/application/application-marketplace/utils/resolve-manifest-asset-urls.util';
|
||||
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
|
||||
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
|
||||
import {
|
||||
@@ -36,6 +37,7 @@ import { FileStorageService } from 'src/engine/core-modules/file-storage/file-st
|
||||
import { FileDTO } from 'src/engine/core-modules/file/dtos/file.dto';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { SdkClientGenerationService } from 'src/engine/core-modules/sdk-client/sdk-client-generation.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
@@ -64,6 +66,7 @@ export class ApplicationDevelopmentResolver {
|
||||
private readonly applicationRegistrationVariableService: ApplicationRegistrationVariableService,
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
private readonly sdkClientGenerationService: SdkClientGenerationService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
) {}
|
||||
|
||||
@Mutation(() => DevelopmentApplicationDTO)
|
||||
@@ -71,10 +74,8 @@ export class ApplicationDevelopmentResolver {
|
||||
@Args() { universalIdentifier, name }: CreateDevelopmentApplicationInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
): Promise<DevelopmentApplicationDTO> {
|
||||
const applicationRegistrationId = await this.findApplicationRegistrationId(
|
||||
universalIdentifier,
|
||||
workspaceId,
|
||||
);
|
||||
const applicationRegistrationId =
|
||||
await this.findApplicationRegistrationId(universalIdentifier);
|
||||
|
||||
const existing = await this.applicationService.findByUniversalIdentifier({
|
||||
universalIdentifier,
|
||||
@@ -125,7 +126,6 @@ export class ApplicationDevelopmentResolver {
|
||||
): Promise<WorkspaceMigrationDTO> {
|
||||
const applicationRegistrationId = await this.findApplicationRegistrationId(
|
||||
manifest.application.universalIdentifier,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const application = await this.applicationService.findByUniversalIdentifier(
|
||||
@@ -164,6 +164,7 @@ export class ApplicationDevelopmentResolver {
|
||||
applicationRegistrationId,
|
||||
manifest,
|
||||
workspaceId,
|
||||
application.id,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -216,7 +217,6 @@ export class ApplicationDevelopmentResolver {
|
||||
|
||||
private async findApplicationRegistrationId(
|
||||
universalIdentifier: string,
|
||||
workspaceId: string,
|
||||
): Promise<string> {
|
||||
const existingRegistration =
|
||||
await this.applicationRegistrationService.findOneByUniversalIdentifier(
|
||||
@@ -230,55 +230,33 @@ export class ApplicationDevelopmentResolver {
|
||||
);
|
||||
}
|
||||
|
||||
const isOwner =
|
||||
await this.applicationRegistrationService.isOwnedByWorkspace(
|
||||
existingRegistration.id,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!isOwner) {
|
||||
throw new ApplicationException(
|
||||
'Cannot sync application: registration is owned by another workspace',
|
||||
ApplicationExceptionCode.FORBIDDEN,
|
||||
);
|
||||
}
|
||||
|
||||
return existingRegistration.id;
|
||||
}
|
||||
|
||||
private async syncRegistrationMetadata(
|
||||
applicationRegistrationId: string,
|
||||
manifest: { application: ApplicationInput['manifest']['application'] },
|
||||
manifest: ApplicationInput['manifest'],
|
||||
workspaceId: string,
|
||||
applicationId: string,
|
||||
): Promise<void> {
|
||||
const isOwner =
|
||||
await this.applicationRegistrationService.isOwnedByWorkspace(
|
||||
const serverUrl = this.twentyConfigService.get('SERVER_URL');
|
||||
|
||||
const manifestWithResolvedUrls = resolveManifestAssetUrls(
|
||||
manifest,
|
||||
(filePath) =>
|
||||
`${serverUrl}/public-assets/${workspaceId}/${applicationId}/${filePath}`,
|
||||
);
|
||||
|
||||
await this.applicationRegistrationService.updateFromManifest(
|
||||
applicationRegistrationId,
|
||||
manifestWithResolvedUrls,
|
||||
);
|
||||
|
||||
if (manifest.application.serverVariables) {
|
||||
await this.applicationRegistrationVariableService.syncVariableSchemas(
|
||||
applicationRegistrationId,
|
||||
workspaceId,
|
||||
manifest.application.serverVariables,
|
||||
);
|
||||
|
||||
if (isOwner) {
|
||||
await this.applicationRegistrationService.update(
|
||||
{
|
||||
id: applicationRegistrationId,
|
||||
update: {
|
||||
name: manifest.application.displayName,
|
||||
description: manifest.application.description,
|
||||
logoUrl: manifest.application.logoUrl,
|
||||
author: manifest.application.author,
|
||||
websiteUrl: manifest.application.websiteUrl,
|
||||
termsUrl: manifest.application.termsUrl,
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (manifest.application.serverVariables) {
|
||||
await this.applicationRegistrationVariableService.syncVariableSchemas(
|
||||
applicationRegistrationId,
|
||||
manifest.application.serverVariables,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-153
@@ -1,32 +1,22 @@
|
||||
import { type MarketplaceDisplayData } from 'src/engine/core-modules/application/application-marketplace/types/marketplace-display-data.type';
|
||||
|
||||
export type CuratedAppEntry = {
|
||||
universalIdentifier: string;
|
||||
sourcePackage: string;
|
||||
isFeatured: boolean;
|
||||
|
||||
name: string;
|
||||
description: string;
|
||||
author: string;
|
||||
logoUrl?: string;
|
||||
websiteUrl?: string;
|
||||
termsUrl?: string;
|
||||
|
||||
richDisplayData: MarketplaceDisplayData;
|
||||
latestAvailableVersion?: string;
|
||||
};
|
||||
|
||||
const MOCK_ENRICHMENT_APP_ID = 'a1b2c3d4-0000-0000-0000-000000000001';
|
||||
const MOCK_ENRICHMENT_JOB_ID = 'a1b2c3d4-0000-0000-0000-000000000100';
|
||||
|
||||
const COMPANY_UNIVERSAL_ID = '20202020-b374-4779-a561-80086cb2e17f';
|
||||
const PERSON_UNIVERSAL_ID = '20202020-e674-48e5-a542-72570eee7213';
|
||||
|
||||
const MOCK_LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="#1a2744"><ellipse cx="38" cy="20" rx="28" ry="10"/><rect x="10" y="20" width="56" height="50"/><ellipse cx="38" cy="70" rx="28" ry="10"/><ellipse cx="38" cy="35" rx="28" ry="10" fill="none" stroke="#fff" stroke-width="3"/><ellipse cx="38" cy="52" rx="28" ry="10" fill="none" stroke="#fff" stroke-width="3"/><circle cx="72" cy="62" r="22" fill="#1a2744"/><circle cx="72" cy="62" r="18" fill="#fff"/><path d="M72 50 L72 74 M62 58 L72 48 L82 58" stroke="#1a2744" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>`;
|
||||
const ENCODED_MOCK_LOGO = `data:image/svg+xml,${encodeURIComponent(MOCK_LOGO_SVG)}`;
|
||||
|
||||
export const MARKETPLACE_CATALOG_INDEX: CuratedAppEntry[] = [
|
||||
{
|
||||
universalIdentifier: MOCK_ENRICHMENT_APP_ID,
|
||||
universalIdentifier: 'a1b2c3d4-0000-0000-0000-000000000001',
|
||||
sourcePackage: '@twentyhq/app-data-enrichment',
|
||||
isFeatured: true,
|
||||
name: 'Data Enrichment',
|
||||
@@ -34,146 +24,6 @@ export const MARKETPLACE_CATALOG_INDEX: CuratedAppEntry[] = [
|
||||
author: 'Twenty',
|
||||
logoUrl: ENCODED_MOCK_LOGO,
|
||||
websiteUrl: 'https://twenty.com',
|
||||
richDisplayData: {
|
||||
icon: 'IconSparkles',
|
||||
version: '1.0.0',
|
||||
category: 'Data',
|
||||
logo: ENCODED_MOCK_LOGO,
|
||||
screenshots: [
|
||||
'https://placehold.co/800x400/f5f5f5/666?text=Screenshot+1',
|
||||
'https://placehold.co/800x400/f5f5f5/666?text=Screenshot+2',
|
||||
'https://placehold.co/800x400/f5f5f5/666?text=Screenshot+3',
|
||||
],
|
||||
aboutDescription:
|
||||
'Enhance your workspace with automated data intelligence. This app monitors your new records and automatically populates missing details such as job titles, company size, social profiles, and industry insights.',
|
||||
providers: ['Clearbit', 'Apollo', 'Hunter.io'],
|
||||
objects: [
|
||||
{
|
||||
universalIdentifier: MOCK_ENRICHMENT_JOB_ID,
|
||||
nameSingular: 'enrichmentJob',
|
||||
namePlural: 'enrichmentJobs',
|
||||
labelSingular: 'Enrichment Job',
|
||||
labelPlural: 'Enrichment Jobs',
|
||||
description: 'Tracks data enrichment requests and their status',
|
||||
icon: 'IconSparkles',
|
||||
fields: [
|
||||
{
|
||||
name: 'status',
|
||||
type: 'SELECT',
|
||||
label: 'Status',
|
||||
description: 'Current status of the enrichment job',
|
||||
icon: 'IconProgressCheck',
|
||||
universalIdentifier: 'a1b2c3d4-0000-0000-0000-000000000101',
|
||||
objectUniversalIdentifier: MOCK_ENRICHMENT_JOB_ID,
|
||||
},
|
||||
{
|
||||
name: 'provider',
|
||||
type: 'TEXT',
|
||||
label: 'Provider',
|
||||
description: 'Enrichment provider used',
|
||||
icon: 'IconCloud',
|
||||
universalIdentifier: 'a1b2c3d4-0000-0000-0000-000000000102',
|
||||
objectUniversalIdentifier: MOCK_ENRICHMENT_JOB_ID,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
name: 'industry',
|
||||
type: 'TEXT',
|
||||
label: 'Industry',
|
||||
description: 'Company industry from enrichment',
|
||||
icon: 'IconBuildingFactory2',
|
||||
objectUniversalIdentifier: COMPANY_UNIVERSAL_ID,
|
||||
universalIdentifier: 'a1b2c3d4-0000-0000-0000-000000000201',
|
||||
},
|
||||
{
|
||||
name: 'linkedInUrl',
|
||||
type: 'LINKS',
|
||||
label: 'LinkedIn URL',
|
||||
description: 'LinkedIn profile URL from enrichment',
|
||||
icon: 'IconBrandLinkedin',
|
||||
objectUniversalIdentifier: PERSON_UNIVERSAL_ID,
|
||||
universalIdentifier: 'a1b2c3d4-0000-0000-0000-000000000203',
|
||||
},
|
||||
],
|
||||
logicFunctions: [
|
||||
{
|
||||
name: 'enrich-on-create',
|
||||
description:
|
||||
'Automatically enriches new records when they are created',
|
||||
timeoutSeconds: 30,
|
||||
},
|
||||
],
|
||||
frontComponents: [],
|
||||
defaultRole: {
|
||||
id: 'a1b2c3d4-0000-0000-0000-000000000010',
|
||||
label: 'Data Enrichment default role',
|
||||
description: 'Default permissions for the Data Enrichment app',
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: true,
|
||||
canUpdateAllSettings: false,
|
||||
canAccessAllTools: false,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: COMPANY_UNIVERSAL_ID,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
{
|
||||
objectUniversalIdentifier: PERSON_UNIVERSAL_ID,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
],
|
||||
fieldPermissions: [],
|
||||
permissionFlags: ['DATA_MODEL', 'API_KEYS_AND_WEBHOOKS'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
sourcePackage: '@twentyhq/hello-world',
|
||||
isFeatured: false,
|
||||
name: 'Hello World',
|
||||
description: 'A simple hello world app to get started with Twenty apps.',
|
||||
author: 'Twenty',
|
||||
websiteUrl: 'https://twenty.com',
|
||||
richDisplayData: {
|
||||
icon: 'IconWorld',
|
||||
version: '0.2.2',
|
||||
category: 'Getting Started',
|
||||
screenshots: [],
|
||||
aboutDescription:
|
||||
'A minimal example app that demonstrates the Twenty app framework. Creates a PostCard object and a logic function to generate new postcards. Great starting point for building your own apps.',
|
||||
providers: [],
|
||||
objects: [
|
||||
{
|
||||
universalIdentifier: 'e2c3d4f5-0000-0000-0000-000000000001',
|
||||
nameSingular: 'postCard',
|
||||
namePlural: 'postCards',
|
||||
labelSingular: 'Post Card',
|
||||
labelPlural: 'Post Cards',
|
||||
description: 'A simple postcard object',
|
||||
icon: 'IconMail',
|
||||
fields: [],
|
||||
},
|
||||
],
|
||||
fields: [],
|
||||
logicFunctions: [
|
||||
{
|
||||
name: 'create-new-post-card',
|
||||
description: 'Creates a new postcard record',
|
||||
},
|
||||
],
|
||||
frontComponents: [],
|
||||
},
|
||||
latestAvailableVersion: '1.0.0',
|
||||
},
|
||||
];
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { Command, CommandRunner } from 'nest-commander';
|
||||
|
||||
import { MarketplaceCatalogSyncService } from 'src/engine/core-modules/application/application-marketplace/marketplace-catalog-sync.service';
|
||||
|
||||
@Command({
|
||||
name: 'marketplace:catalog-sync',
|
||||
description: 'Sync the marketplace catalog into ApplicationRegistration',
|
||||
})
|
||||
export class MarketplaceCatalogSyncCommand extends CommandRunner {
|
||||
constructor(
|
||||
private readonly marketplaceCatalogSyncService: MarketplaceCatalogSyncService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
await this.marketplaceCatalogSyncService.syncCatalog();
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
|
||||
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
|
||||
|
||||
@ObjectType('MarketplaceAppDetail')
|
||||
export class MarketplaceAppDetailDTO {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
universalIdentifier: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
name: string;
|
||||
|
||||
@Field(() => ApplicationRegistrationSourceType)
|
||||
sourceType: ApplicationRegistrationSourceType;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
sourcePackage?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
latestAvailableVersion?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@Field(() => Boolean)
|
||||
isListed: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@Field(() => Boolean)
|
||||
isFeatured: boolean;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
manifest?: Manifest;
|
||||
}
|
||||
+1
-241
@@ -1,204 +1,12 @@
|
||||
import { Field, Int, ObjectType } from '@nestjs/graphql';
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
@ObjectType('MarketplaceAppField')
|
||||
export class MarketplaceAppFieldDTO {
|
||||
@IsString()
|
||||
@Field()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
type: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
label: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
objectUniversalIdentifier: string;
|
||||
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
universalIdentifier: string;
|
||||
}
|
||||
|
||||
@ObjectType('MarketplaceAppObject')
|
||||
export class MarketplaceAppObjectDTO {
|
||||
@IsString()
|
||||
@Field()
|
||||
universalIdentifier: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
nameSingular: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
namePlural: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
labelSingular: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
labelPlural: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@IsArray()
|
||||
@Field(() => [MarketplaceAppFieldDTO])
|
||||
fields: MarketplaceAppFieldDTO[];
|
||||
}
|
||||
|
||||
@ObjectType('MarketplaceAppLogicFunction')
|
||||
export class MarketplaceAppLogicFunctionDTO {
|
||||
@IsString()
|
||||
@Field()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Field(() => Int, { nullable: true })
|
||||
timeoutSeconds?: number;
|
||||
}
|
||||
|
||||
@ObjectType('MarketplaceAppFrontComponent')
|
||||
export class MarketplaceAppFrontComponentDTO {
|
||||
@IsString()
|
||||
@Field()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@ObjectType('MarketplaceAppRoleObjectPermission')
|
||||
export class MarketplaceAppRoleObjectPermissionDTO {
|
||||
@IsString()
|
||||
@Field()
|
||||
objectUniversalIdentifier: string;
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canReadObjectRecords?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canUpdateObjectRecords?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canSoftDeleteObjectRecords?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canDestroyObjectRecords?: boolean;
|
||||
}
|
||||
|
||||
@ObjectType('MarketplaceAppRoleFieldPermission')
|
||||
export class MarketplaceAppRoleFieldPermissionDTO {
|
||||
@IsString()
|
||||
@Field()
|
||||
objectUniversalIdentifier: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
fieldUniversalIdentifier: string;
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canReadFieldValue?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canUpdateFieldValue?: boolean;
|
||||
}
|
||||
|
||||
@ObjectType('MarketplaceAppDefaultRole')
|
||||
export class MarketplaceAppDefaultRoleDTO {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
label: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Field(() => Boolean)
|
||||
canReadAllObjectRecords: boolean;
|
||||
|
||||
@Field(() => Boolean)
|
||||
canUpdateAllObjectRecords: boolean;
|
||||
|
||||
@Field(() => Boolean)
|
||||
canSoftDeleteAllObjectRecords: boolean;
|
||||
|
||||
@Field(() => Boolean)
|
||||
canDestroyAllObjectRecords: boolean;
|
||||
|
||||
@Field(() => Boolean)
|
||||
canUpdateAllSettings: boolean;
|
||||
|
||||
@Field(() => Boolean)
|
||||
canAccessAllTools: boolean;
|
||||
|
||||
@IsArray()
|
||||
@Field(() => [MarketplaceAppRoleObjectPermissionDTO])
|
||||
objectPermissions: MarketplaceAppRoleObjectPermissionDTO[];
|
||||
|
||||
@IsArray()
|
||||
@Field(() => [MarketplaceAppRoleFieldPermissionDTO])
|
||||
fieldPermissions: MarketplaceAppRoleFieldPermissionDTO[];
|
||||
|
||||
@IsArray()
|
||||
@Field(() => [String])
|
||||
permissionFlags: string[];
|
||||
}
|
||||
|
||||
@ObjectType('MarketplaceApp')
|
||||
export class MarketplaceAppDTO {
|
||||
@@ -221,10 +29,6 @@ export class MarketplaceAppDTO {
|
||||
@Field()
|
||||
icon: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
version: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
author: string;
|
||||
@@ -238,50 +42,6 @@ export class MarketplaceAppDTO {
|
||||
@Field({ nullable: true })
|
||||
logo?: string;
|
||||
|
||||
@IsArray()
|
||||
@Field(() => [String])
|
||||
screenshots: string[];
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
aboutDescription: string;
|
||||
|
||||
@IsArray()
|
||||
@Field(() => [String])
|
||||
providers: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
websiteUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
termsUrl?: string;
|
||||
|
||||
@IsArray()
|
||||
@Field(() => [MarketplaceAppObjectDTO])
|
||||
objects: MarketplaceAppObjectDTO[];
|
||||
|
||||
@IsArray()
|
||||
@Field(() => [MarketplaceAppFieldDTO])
|
||||
fields: MarketplaceAppFieldDTO[];
|
||||
|
||||
@IsArray()
|
||||
@Field(() => [MarketplaceAppLogicFunctionDTO])
|
||||
logicFunctions: MarketplaceAppLogicFunctionDTO[];
|
||||
|
||||
@IsArray()
|
||||
@Field(() => [MarketplaceAppFrontComponentDTO])
|
||||
frontComponents: MarketplaceAppFrontComponentDTO[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => MarketplaceAppDefaultRoleDTO)
|
||||
@Field(() => MarketplaceAppDefaultRoleDTO, { nullable: true })
|
||||
defaultRole?: MarketplaceAppDefaultRoleDTO;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
|
||||
+46
-27
@@ -4,6 +4,9 @@ import { ApplicationRegistrationService } from 'src/engine/core-modules/applicat
|
||||
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
|
||||
import { MARKETPLACE_CATALOG_INDEX } from 'src/engine/core-modules/application/application-marketplace/constants/marketplace-catalog-index.constant';
|
||||
import { MarketplaceService } from 'src/engine/core-modules/application/application-marketplace/marketplace.service';
|
||||
import { buildRegistryCdnUrl } from 'src/engine/core-modules/application/application-marketplace/utils/build-registry-cdn-url.util';
|
||||
import { resolveManifestAssetUrls } from 'src/engine/core-modules/application/application-marketplace/utils/resolve-manifest-asset-urls.util';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
@Injectable()
|
||||
export class MarketplaceCatalogSyncService {
|
||||
@@ -12,11 +15,12 @@ export class MarketplaceCatalogSyncService {
|
||||
constructor(
|
||||
private readonly applicationRegistrationService: ApplicationRegistrationService,
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
) {}
|
||||
|
||||
async syncCatalog(): Promise<void> {
|
||||
await this.syncCuratedApps();
|
||||
await this.syncNpmApps();
|
||||
await this.syncRegistryApps();
|
||||
|
||||
this.logger.log('Marketplace catalog sync completed');
|
||||
}
|
||||
@@ -27,18 +31,12 @@ export class MarketplaceCatalogSyncService {
|
||||
await this.applicationRegistrationService.upsertFromCatalog({
|
||||
universalIdentifier: entry.universalIdentifier,
|
||||
name: entry.name,
|
||||
description:
|
||||
entry.richDisplayData.aboutDescription ?? entry.description,
|
||||
author: entry.author,
|
||||
sourceType: ApplicationRegistrationSourceType.NPM,
|
||||
sourcePackage: entry.sourcePackage,
|
||||
logoUrl: entry.logoUrl ?? null,
|
||||
websiteUrl: entry.websiteUrl ?? null,
|
||||
termsUrl: entry.termsUrl ?? null,
|
||||
latestAvailableVersion: entry.richDisplayData.version ?? null,
|
||||
latestAvailableVersion: entry.latestAvailableVersion ?? null,
|
||||
isListed: true,
|
||||
isFeatured: entry.isFeatured,
|
||||
marketplaceDisplayData: entry.richDisplayData,
|
||||
manifest: null,
|
||||
ownerWorkspaceId: null,
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -49,38 +47,59 @@ export class MarketplaceCatalogSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
private async syncNpmApps(): Promise<void> {
|
||||
const npmApps = await this.marketplaceService.fetchAppsFromNpmRegistry();
|
||||
private async syncRegistryApps(): Promise<void> {
|
||||
const packages = await this.marketplaceService.fetchAppsFromRegistry();
|
||||
|
||||
const curatedIdentifiers = new Set(
|
||||
MARKETPLACE_CATALOG_INDEX.map((entry) => entry.universalIdentifier),
|
||||
);
|
||||
|
||||
for (const app of npmApps) {
|
||||
if (curatedIdentifiers.has(app.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const pkg of packages) {
|
||||
try {
|
||||
const manifest =
|
||||
await this.marketplaceService.fetchManifestFromRegistryCdn(
|
||||
pkg.name,
|
||||
pkg.version,
|
||||
);
|
||||
|
||||
if (!manifest) {
|
||||
this.logger.debug(`Skipping ${pkg.name}: no manifest found on CDN`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const universalIdentifier = manifest.application.universalIdentifier;
|
||||
|
||||
if (curatedIdentifiers.has(universalIdentifier)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cdnBaseUrl = this.twentyConfigService.get('APP_REGISTRY_CDN_URL');
|
||||
|
||||
const manifestWithResolvedUrls = resolveManifestAssetUrls(
|
||||
manifest,
|
||||
(filePath) =>
|
||||
buildRegistryCdnUrl({
|
||||
cdnBaseUrl,
|
||||
packageName: pkg.name,
|
||||
version: pkg.version,
|
||||
filePath,
|
||||
}),
|
||||
);
|
||||
|
||||
await this.applicationRegistrationService.upsertFromCatalog({
|
||||
universalIdentifier: app.id,
|
||||
name: app.name,
|
||||
description: app.description,
|
||||
author: app.author,
|
||||
universalIdentifier,
|
||||
name: manifest.application.displayName ?? pkg.name,
|
||||
sourceType: ApplicationRegistrationSourceType.NPM,
|
||||
sourcePackage: app.sourcePackage ?? app.name,
|
||||
logoUrl: null,
|
||||
websiteUrl: app.websiteUrl ?? null,
|
||||
termsUrl: null,
|
||||
latestAvailableVersion: app.version ?? null,
|
||||
sourcePackage: pkg.name,
|
||||
latestAvailableVersion: pkg.version ?? null,
|
||||
isListed: true,
|
||||
isFeatured: false,
|
||||
marketplaceDisplayData: null,
|
||||
manifest: manifestWithResolvedUrls,
|
||||
ownerWorkspaceId: null,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to sync npm app "${app.name}": ${error instanceof Error ? error.message : String(error)}`,
|
||||
`Failed to sync registry app "${pkg.name}": ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+30
-36
@@ -10,17 +10,14 @@ import {
|
||||
import { 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';
|
||||
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';
|
||||
|
||||
const MARKETPLACE_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
@Injectable()
|
||||
export class MarketplaceQueryService {
|
||||
private readonly logger = new Logger(MarketplaceQueryService.name);
|
||||
private cachedApps: MarketplaceAppDTO[] | null = null;
|
||||
private cacheExpiresAt = 0;
|
||||
private hasSyncBeenEnqueued = false;
|
||||
|
||||
constructor(
|
||||
@@ -30,10 +27,6 @@ export class MarketplaceQueryService {
|
||||
) {}
|
||||
|
||||
async findManyMarketplaceApps(): Promise<MarketplaceAppDTO[]> {
|
||||
if (this.cachedApps !== null && Date.now() < this.cacheExpiresAt) {
|
||||
return this.cachedApps;
|
||||
}
|
||||
|
||||
const registrations =
|
||||
await this.applicationRegistrationService.findManyListed();
|
||||
|
||||
@@ -46,27 +39,25 @@ export class MarketplaceQueryService {
|
||||
await this.messageQueueService.add(
|
||||
MarketplaceCatalogSyncCronJob.name,
|
||||
{},
|
||||
{ id: 'marketplace-catalog-sync' }, // Avoids triggering multiple pending jobs
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
this.cachedApps = registrations.map((registration) =>
|
||||
return registrations.map((registration) =>
|
||||
this.toMarketplaceAppDTO(registration),
|
||||
);
|
||||
this.cacheExpiresAt = Date.now() + MARKETPLACE_CACHE_TTL_MS;
|
||||
|
||||
return this.cachedApps;
|
||||
}
|
||||
|
||||
async findOneMarketplaceApp(
|
||||
async findMarketplaceAppDetail(
|
||||
universalIdentifier: string,
|
||||
): Promise<MarketplaceAppDTO> {
|
||||
): Promise<MarketplaceAppDetailDTO> {
|
||||
const registration =
|
||||
await this.findRegistrationByUniversalIdentifier(universalIdentifier);
|
||||
|
||||
return this.toMarketplaceAppDTO(registration);
|
||||
return this.toMarketplaceAppDetailDTO(registration);
|
||||
}
|
||||
|
||||
async findRegistrationByUniversalIdentifier(
|
||||
@@ -87,34 +78,37 @@ export class MarketplaceQueryService {
|
||||
return registration;
|
||||
}
|
||||
|
||||
toMarketplaceAppDTO(
|
||||
private toMarketplaceAppDTO(
|
||||
registration: ApplicationRegistrationEntity,
|
||||
): MarketplaceAppDTO {
|
||||
const displayData = registration.marketplaceDisplayData;
|
||||
const app = registration.manifest?.application;
|
||||
|
||||
return {
|
||||
id: registration.universalIdentifier,
|
||||
name: registration.name,
|
||||
description: registration.description ?? '',
|
||||
icon: displayData?.icon ?? 'IconApps',
|
||||
version:
|
||||
displayData?.version ?? registration.latestAvailableVersion ?? '0.0.0',
|
||||
author: registration.author ?? 'Unknown',
|
||||
category: displayData?.category ?? '',
|
||||
logo: displayData?.logo,
|
||||
screenshots: displayData?.screenshots ?? [],
|
||||
aboutDescription:
|
||||
displayData?.aboutDescription ?? registration.description ?? '',
|
||||
providers: displayData?.providers ?? [],
|
||||
websiteUrl: registration.websiteUrl ?? undefined,
|
||||
termsUrl: registration.termsUrl ?? undefined,
|
||||
objects: displayData?.objects ?? [],
|
||||
fields: displayData?.fields ?? [],
|
||||
logicFunctions: displayData?.logicFunctions ?? [],
|
||||
frontComponents: displayData?.frontComponents ?? [],
|
||||
name: app?.displayName ?? registration.name,
|
||||
description: app?.description ?? '',
|
||||
icon: app?.icon ?? 'IconApps',
|
||||
author: app?.author ?? 'Unknown',
|
||||
category: app?.category ?? '',
|
||||
logo: app?.logoUrl ?? undefined,
|
||||
sourcePackage: registration.sourcePackage ?? undefined,
|
||||
defaultRole: displayData?.defaultRole,
|
||||
isFeatured: registration.isFeatured,
|
||||
};
|
||||
}
|
||||
|
||||
private toMarketplaceAppDetailDTO(
|
||||
registration: ApplicationRegistrationEntity,
|
||||
): MarketplaceAppDetailDTO {
|
||||
return {
|
||||
id: registration.id,
|
||||
universalIdentifier: registration.universalIdentifier,
|
||||
name: registration.name,
|
||||
sourceType: registration.sourceType,
|
||||
sourcePackage: registration.sourcePackage ?? undefined,
|
||||
latestAvailableVersion: registration.latestAvailableVersion ?? undefined,
|
||||
isListed: registration.isListed,
|
||||
isFeatured: registration.isFeatured,
|
||||
manifest: registration.manifest ?? undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -11,6 +11,7 @@ import { MarketplaceService } from 'src/engine/core-modules/application/applicat
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { MarketplaceCatalogSyncCommand } from 'src/engine/core-modules/application/application-marketplace/crons/commands/marketplace-catalog-sync.command';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -26,6 +27,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
MarketplaceQueryService,
|
||||
MarketplaceCatalogSyncCronJob,
|
||||
MarketplaceCatalogSyncCronCommand,
|
||||
MarketplaceCatalogSyncCommand,
|
||||
MarketplaceResolver,
|
||||
],
|
||||
exports: [
|
||||
|
||||
+23
-4
@@ -6,6 +6,7 @@ import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorato
|
||||
import { ApplicationRegistrationExceptionFilter } from 'src/engine/core-modules/application/application-registration/application-registration-exception-filter';
|
||||
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
|
||||
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 { MarketplaceQueryService } from 'src/engine/core-modules/application/application-marketplace/marketplace-query.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
@@ -13,6 +14,10 @@ import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { MarketplaceCatalogSyncCronJob } from 'src/engine/core-modules/application/application-marketplace/crons/marketplace-catalog-sync.cron.job';
|
||||
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';
|
||||
|
||||
@MetadataResolver()
|
||||
@UseFilters(ApplicationRegistrationExceptionFilter)
|
||||
@@ -21,6 +26,8 @@ export class MarketplaceResolver {
|
||||
constructor(
|
||||
private readonly marketplaceQueryService: MarketplaceQueryService,
|
||||
private readonly applicationInstallService: ApplicationInstallService,
|
||||
@InjectMessageQueue(MessageQueue.cronQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
) {}
|
||||
|
||||
@Query(() => [MarketplaceAppDTO])
|
||||
@@ -28,11 +35,11 @@ export class MarketplaceResolver {
|
||||
return this.marketplaceQueryService.findManyMarketplaceApps();
|
||||
}
|
||||
|
||||
@Query(() => MarketplaceAppDTO)
|
||||
async findOneMarketplaceApp(
|
||||
@Query(() => MarketplaceAppDetailDTO)
|
||||
async findMarketplaceAppDetail(
|
||||
@Args('universalIdentifier') universalIdentifier: string,
|
||||
): Promise<MarketplaceAppDTO> {
|
||||
return this.marketplaceQueryService.findOneMarketplaceApp(
|
||||
): Promise<MarketplaceAppDetailDTO> {
|
||||
return this.marketplaceQueryService.findMarketplaceAppDetail(
|
||||
universalIdentifier,
|
||||
);
|
||||
}
|
||||
@@ -56,4 +63,16 @@ export class MarketplaceResolver {
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.MARKETPLACE_APPS))
|
||||
async syncMarketplaceCatalog(): Promise<boolean> {
|
||||
await this.messageQueueService.add(
|
||||
MarketplaceCatalogSyncCronJob.name,
|
||||
{},
|
||||
{ id: 'marketplace-catalog-sync' }, // Avoids triggering multiple pending jobs
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+63
-41
@@ -1,13 +1,21 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import axios from 'axios';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { MarketplaceAppDTO } from 'src/engine/core-modules/application/application-marketplace/dtos/marketplace-app.dto';
|
||||
import { buildRegistryCdnUrl } from 'src/engine/core-modules/application/application-marketplace/utils/build-registry-cdn-url.util';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
const npmSearchResultSchema = z.object({
|
||||
export type RegistryPackageInfo = {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
author: string;
|
||||
websiteUrl?: string;
|
||||
};
|
||||
|
||||
const registrySearchResultSchema = z.object({
|
||||
objects: z.array(
|
||||
z.object({
|
||||
package: z.object({
|
||||
@@ -16,7 +24,12 @@ const npmSearchResultSchema = z.object({
|
||||
description: z.string().optional(),
|
||||
keywords: z.array(z.string()).optional(),
|
||||
author: z.object({ name: z.string().optional() }).optional(),
|
||||
links: z.object({ homepage: z.string().optional() }).optional(),
|
||||
links: z
|
||||
.object({
|
||||
homepage: z.string().optional(),
|
||||
npm: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
@@ -28,7 +41,39 @@ export class MarketplaceService {
|
||||
|
||||
constructor(private readonly twentyConfigService: TwentyConfigService) {}
|
||||
|
||||
async fetchAppsFromNpmRegistry(): Promise<MarketplaceAppDTO[]> {
|
||||
async fetchManifestFromRegistryCdn(
|
||||
packageName: string,
|
||||
version: string,
|
||||
): Promise<Manifest | null> {
|
||||
const cdnBaseUrl = this.twentyConfigService.get('APP_REGISTRY_CDN_URL');
|
||||
const url = buildRegistryCdnUrl({
|
||||
cdnBaseUrl,
|
||||
packageName,
|
||||
version,
|
||||
filePath: 'manifest.json',
|
||||
});
|
||||
|
||||
try {
|
||||
const { data } = await axios.get(url, {
|
||||
headers: { 'User-Agent': 'Twenty-Marketplace' },
|
||||
timeout: 5_000,
|
||||
});
|
||||
|
||||
if (!data?.application) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return data as Manifest;
|
||||
} catch {
|
||||
this.logger.debug(
|
||||
`Could not fetch manifest from CDN for ${packageName}@${version}`,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchAppsFromRegistry(): Promise<RegistryPackageInfo[]> {
|
||||
const registryUrl = this.twentyConfigService.get('APP_REGISTRY_URL');
|
||||
|
||||
try {
|
||||
@@ -40,53 +85,30 @@ export class MarketplaceService {
|
||||
},
|
||||
);
|
||||
|
||||
const parsed = npmSearchResultSchema.safeParse(data);
|
||||
const parsed = registrySearchResultSchema.safeParse(data);
|
||||
|
||||
if (!parsed.success) {
|
||||
this.logger.warn(
|
||||
`Unexpected npm search response shape: ${parsed.error.message}`,
|
||||
`Unexpected registry search response shape: ${parsed.error.message}`,
|
||||
);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
return parsed.data.objects
|
||||
.map((result) => {
|
||||
const { name, version, description, author, links } = result.package;
|
||||
const twentyKeyword = (result.package.keywords ?? []).find(
|
||||
(keyword) => keyword.startsWith('twenty-uid:'),
|
||||
);
|
||||
return parsed.data.objects.map((result) => {
|
||||
const { name, version, description, author, links } = result.package;
|
||||
|
||||
if (!isDefined(twentyKeyword)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const universalIdentifier = twentyKeyword.replace('twenty-uid:', '');
|
||||
|
||||
return {
|
||||
id: universalIdentifier,
|
||||
name,
|
||||
description: description ?? '',
|
||||
icon: 'IconApps',
|
||||
version,
|
||||
author: author?.name ?? 'Unknown',
|
||||
category: '',
|
||||
screenshots: [],
|
||||
aboutDescription: description ?? '',
|
||||
providers: [],
|
||||
websiteUrl: links?.homepage,
|
||||
objects: [],
|
||||
fields: [],
|
||||
logicFunctions: [],
|
||||
frontComponents: [],
|
||||
sourcePackage: name,
|
||||
isFeatured: false,
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
description: description ?? '',
|
||||
author: author?.name ?? 'Unknown',
|
||||
websiteUrl: links?.homepage ?? links?.npm,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Failed to fetch apps from npm registry: ${error instanceof Error ? error.message : String(error)}`,
|
||||
`Failed to fetch apps from registry ${registryUrl}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
|
||||
return [];
|
||||
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
// Rich display data stored alongside ApplicationRegistration for marketplace
|
||||
// rendering. This is denormalized from the catalog source so it can be displayed
|
||||
// pre-install without resolving the package.
|
||||
export type MarketplaceDisplayData = {
|
||||
icon?: string;
|
||||
version?: string;
|
||||
category?: string;
|
||||
logo?: string;
|
||||
screenshots?: string[];
|
||||
aboutDescription?: string;
|
||||
providers?: string[];
|
||||
objects?: MarketplaceDisplayObject[];
|
||||
fields?: MarketplaceDisplayField[];
|
||||
logicFunctions?: MarketplaceDisplayLogicFunction[];
|
||||
frontComponents?: MarketplaceDisplayFrontComponent[];
|
||||
defaultRole?: MarketplaceDisplayDefaultRole;
|
||||
};
|
||||
|
||||
type MarketplaceDisplayObject = {
|
||||
universalIdentifier: string;
|
||||
nameSingular: string;
|
||||
namePlural: string;
|
||||
labelSingular: string;
|
||||
labelPlural: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
fields: MarketplaceDisplayField[];
|
||||
};
|
||||
|
||||
type MarketplaceDisplayField = {
|
||||
name: string;
|
||||
type: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
objectUniversalIdentifier: string;
|
||||
universalIdentifier: string;
|
||||
};
|
||||
|
||||
type MarketplaceDisplayLogicFunction = {
|
||||
name: string;
|
||||
description?: string;
|
||||
timeoutSeconds?: number;
|
||||
};
|
||||
|
||||
type MarketplaceDisplayFrontComponent = {
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
type MarketplaceDisplayDefaultRole = {
|
||||
id: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
canReadAllObjectRecords: boolean;
|
||||
canUpdateAllObjectRecords: boolean;
|
||||
canSoftDeleteAllObjectRecords: boolean;
|
||||
canDestroyAllObjectRecords: boolean;
|
||||
canUpdateAllSettings: boolean;
|
||||
canAccessAllTools: boolean;
|
||||
objectPermissions: Array<{
|
||||
objectUniversalIdentifier: string;
|
||||
canReadObjectRecords?: boolean;
|
||||
canUpdateObjectRecords?: boolean;
|
||||
canSoftDeleteObjectRecords?: boolean;
|
||||
canDestroyObjectRecords?: boolean;
|
||||
}>;
|
||||
fieldPermissions: Array<{
|
||||
objectUniversalIdentifier: string;
|
||||
fieldUniversalIdentifier: string;
|
||||
canReadFieldValue?: boolean;
|
||||
canUpdateFieldValue?: boolean;
|
||||
}>;
|
||||
permissionFlags: string[];
|
||||
};
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
|
||||
import { resolveManifestAssetUrls } from 'src/engine/core-modules/application/application-marketplace/utils/resolve-manifest-asset-urls.util';
|
||||
|
||||
const buildMinimalManifest = (
|
||||
overrides: Partial<Manifest['application']> = {},
|
||||
): Manifest => ({
|
||||
application: {
|
||||
universalIdentifier: 'app-1',
|
||||
defaultRoleUniversalIdentifier: 'role-1',
|
||||
displayName: 'Test App',
|
||||
description: 'A test app',
|
||||
packageJsonChecksum: null,
|
||||
yarnLockChecksum: null,
|
||||
...overrides,
|
||||
},
|
||||
objects: [],
|
||||
fields: [],
|
||||
logicFunctions: [],
|
||||
frontComponents: [],
|
||||
roles: [],
|
||||
skills: [],
|
||||
agents: [],
|
||||
publicAssets: [],
|
||||
views: [],
|
||||
navigationMenuItems: [],
|
||||
pageLayouts: [],
|
||||
});
|
||||
|
||||
describe('resolveManifestAssetUrls', () => {
|
||||
const urlBuilder = (filePath: string) =>
|
||||
`https://cdn.example.com/pkg/${filePath}`;
|
||||
|
||||
it('should resolve a relative logoUrl', () => {
|
||||
const manifest = buildMinimalManifest({ logoUrl: 'logo.png' });
|
||||
|
||||
const result = resolveManifestAssetUrls(manifest, urlBuilder);
|
||||
|
||||
expect(result.application.logoUrl).toBe(
|
||||
'https://cdn.example.com/pkg/logo.png',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not modify an absolute logoUrl', () => {
|
||||
const manifest = buildMinimalManifest({
|
||||
logoUrl: 'https://example.com/logo.png',
|
||||
});
|
||||
|
||||
const result = resolveManifestAssetUrls(manifest, urlBuilder);
|
||||
|
||||
expect(result.application.logoUrl).toBe('https://example.com/logo.png');
|
||||
});
|
||||
|
||||
it('should not modify an http logoUrl', () => {
|
||||
const manifest = buildMinimalManifest({
|
||||
logoUrl: 'http://example.com/logo.png',
|
||||
});
|
||||
|
||||
const result = resolveManifestAssetUrls(manifest, urlBuilder);
|
||||
|
||||
expect(result.application.logoUrl).toBe('http://example.com/logo.png');
|
||||
});
|
||||
|
||||
it('should leave logoUrl undefined when not set', () => {
|
||||
const manifest = buildMinimalManifest();
|
||||
|
||||
const result = resolveManifestAssetUrls(manifest, urlBuilder);
|
||||
|
||||
expect(result.application.logoUrl).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should resolve relative screenshot paths', () => {
|
||||
const manifest = buildMinimalManifest({
|
||||
screenshots: ['screen1.png', 'screen2.png'],
|
||||
});
|
||||
|
||||
const result = resolveManifestAssetUrls(manifest, urlBuilder);
|
||||
|
||||
expect(result.application.screenshots).toEqual([
|
||||
'https://cdn.example.com/pkg/screen1.png',
|
||||
'https://cdn.example.com/pkg/screen2.png',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not modify absolute screenshot URLs', () => {
|
||||
const manifest = buildMinimalManifest({
|
||||
screenshots: [
|
||||
'https://example.com/screen1.png',
|
||||
'https://example.com/screen2.png',
|
||||
],
|
||||
});
|
||||
|
||||
const result = resolveManifestAssetUrls(manifest, urlBuilder);
|
||||
|
||||
expect(result.application.screenshots).toEqual([
|
||||
'https://example.com/screen1.png',
|
||||
'https://example.com/screen2.png',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle a mix of relative and absolute screenshot URLs', () => {
|
||||
const manifest = buildMinimalManifest({
|
||||
screenshots: ['relative.png', 'https://example.com/absolute.png'],
|
||||
});
|
||||
|
||||
const result = resolveManifestAssetUrls(manifest, urlBuilder);
|
||||
|
||||
expect(result.application.screenshots).toEqual([
|
||||
'https://cdn.example.com/pkg/relative.png',
|
||||
'https://example.com/absolute.png',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty screenshots array', () => {
|
||||
const manifest = buildMinimalManifest({ screenshots: [] });
|
||||
|
||||
const result = resolveManifestAssetUrls(manifest, urlBuilder);
|
||||
|
||||
expect(result.application.screenshots).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle undefined screenshots', () => {
|
||||
const manifest = buildMinimalManifest();
|
||||
|
||||
const result = resolveManifestAssetUrls(manifest, urlBuilder);
|
||||
|
||||
expect(result.application.screenshots).toEqual([]);
|
||||
});
|
||||
|
||||
it('should preserve all other manifest properties', () => {
|
||||
const manifest = buildMinimalManifest({
|
||||
logoUrl: 'logo.png',
|
||||
author: 'Test Author',
|
||||
websiteUrl: 'https://test.com',
|
||||
});
|
||||
manifest.objects = [{ universalIdentifier: 'obj-1' } as never];
|
||||
|
||||
const result = resolveManifestAssetUrls(manifest, urlBuilder);
|
||||
|
||||
expect(result.application.author).toBe('Test Author');
|
||||
expect(result.application.websiteUrl).toBe('https://test.com');
|
||||
expect(result.application.displayName).toBe('Test App');
|
||||
expect(result.objects).toEqual([{ universalIdentifier: 'obj-1' }]);
|
||||
});
|
||||
});
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
export const buildRegistryCdnUrl = (params: {
|
||||
cdnBaseUrl: string;
|
||||
packageName: string;
|
||||
version: string;
|
||||
filePath: string;
|
||||
}): string => {
|
||||
return `${params.cdnBaseUrl}/${params.packageName}@${params.version}/${params.filePath}`;
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
|
||||
const isAbsoluteUrl = (url: string): boolean =>
|
||||
url.startsWith('http://') || url.startsWith('https://');
|
||||
|
||||
export const resolveManifestAssetUrls = (
|
||||
manifest: Manifest,
|
||||
urlBuilder: (filePath: string) => string,
|
||||
): Manifest => {
|
||||
const resolveUrl = (url: string): string =>
|
||||
isAbsoluteUrl(url) ? url : urlBuilder(url);
|
||||
|
||||
return {
|
||||
...manifest,
|
||||
application: {
|
||||
...manifest.application,
|
||||
logoUrl: manifest.application.logoUrl
|
||||
? resolveUrl(manifest.application.logoUrl)
|
||||
: undefined,
|
||||
screenshots: (manifest.application.screenshots ?? []).map(resolveUrl),
|
||||
},
|
||||
};
|
||||
};
|
||||
-4
@@ -140,9 +140,6 @@ export class OAuthRegistrationController {
|
||||
const registration = this.applicationRegistrationRepository.create({
|
||||
universalIdentifier: v4(),
|
||||
name: body.client_name,
|
||||
description: null,
|
||||
logoUrl: body.logo_uri ?? null,
|
||||
author: null,
|
||||
oAuthClientId: clientId,
|
||||
oAuthClientSecretHash: null,
|
||||
oAuthRedirectUris: body.redirect_uris,
|
||||
@@ -150,7 +147,6 @@ export class OAuthRegistrationController {
|
||||
createdByUserId: null,
|
||||
ownerWorkspaceId: null,
|
||||
sourceType: ApplicationRegistrationSourceType.OAUTH_ONLY,
|
||||
websiteUrl: body.client_uri ?? null,
|
||||
});
|
||||
|
||||
await this.applicationRegistrationRepository.save(registration);
|
||||
|
||||
-1
@@ -586,7 +586,6 @@ export class OAuthService {
|
||||
return this.applicationService.create({
|
||||
universalIdentifier: applicationRegistration.universalIdentifier,
|
||||
name: applicationRegistration.name,
|
||||
description: applicationRegistration.description,
|
||||
version: '0.0.0',
|
||||
sourcePath: 'oauth-install',
|
||||
applicationRegistrationId: applicationRegistration.id,
|
||||
|
||||
+2
-22
@@ -18,7 +18,7 @@ import {
|
||||
} from 'typeorm';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { type MarketplaceDisplayData } from 'src/engine/core-modules/application/application-marketplace/types/marketplace-display-data.type';
|
||||
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';
|
||||
@@ -62,18 +62,6 @@ export class ApplicationRegistrationEntity {
|
||||
@Column({ nullable: false, type: 'text' })
|
||||
name: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
description: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
logoUrl: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
author: string | null;
|
||||
|
||||
@Field()
|
||||
@Column({ nullable: false, type: 'text' })
|
||||
oAuthClientId: string;
|
||||
@@ -128,14 +116,6 @@ export class ApplicationRegistrationEntity {
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
latestAvailableVersion: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
websiteUrl: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
termsUrl: string | null;
|
||||
|
||||
@Field(() => Boolean)
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isListed: boolean;
|
||||
@@ -145,7 +125,7 @@ export class ApplicationRegistrationEntity {
|
||||
isFeatured: boolean;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
marketplaceDisplayData: MarketplaceDisplayData | null;
|
||||
manifest: Manifest | null;
|
||||
|
||||
@OneToMany(
|
||||
() => ApplicationRegistrationVariableEntity,
|
||||
|
||||
+2
@@ -8,6 +8,7 @@ import { ApplicationRegistrationVariableModule } from 'src/engine/core-modules/a
|
||||
import { ApplicationTarballService } from 'src/engine/core-modules/application/application-registration/application-tarball.service';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
|
||||
import { FileUrlModule } from 'src/engine/core-modules/file/file-url/file-url.module';
|
||||
@@ -24,6 +25,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
|
||||
]),
|
||||
ApplicationRegistrationVariableModule,
|
||||
ApplicationModule,
|
||||
DomainServerConfigModule,
|
||||
FeatureFlagModule,
|
||||
PermissionsModule,
|
||||
FileStorageModule,
|
||||
|
||||
+21
@@ -32,6 +32,7 @@ import { RotateClientSecretDTO } from 'src/engine/core-modules/application/appli
|
||||
import { TransferApplicationRegistrationOwnershipInput } from 'src/engine/core-modules/application/application-registration/dtos/transfer-application-registration-ownership.input';
|
||||
import { UpdateApplicationRegistrationInput } 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 { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
|
||||
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
||||
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
|
||||
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
@@ -59,6 +60,7 @@ export class ApplicationRegistrationResolver {
|
||||
private readonly applicationRegistrationVariableService: ApplicationRegistrationVariableService,
|
||||
private readonly applicationTarballService: ApplicationTarballService,
|
||||
private readonly fileUrlService: FileUrlService,
|
||||
private readonly domainServerConfigService: DomainServerConfigService,
|
||||
) {}
|
||||
|
||||
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
|
||||
@@ -290,6 +292,25 @@ export class ApplicationRegistrationResolver {
|
||||
});
|
||||
}
|
||||
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
SettingsPermissionGuard(PermissionFlagType.API_KEYS_AND_WEBHOOKS),
|
||||
)
|
||||
@Query(() => String)
|
||||
async getApplicationShareLink(
|
||||
@Args('id') id: string,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
): Promise<string> {
|
||||
const registration = await this.applicationRegistrationService.findOneById(
|
||||
id,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const frontUrl = this.domainServerConfigService.getFrontUrl();
|
||||
|
||||
return `${frontUrl.origin}/settings/applications/available/${registration.universalIdentifier}`;
|
||||
}
|
||||
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
SettingsPermissionGuard(PermissionFlagType.APPLICATIONS),
|
||||
|
||||
+24
-43
@@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import crypto from 'crypto';
|
||||
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, type Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
@@ -102,7 +103,7 @@ export class ApplicationRegistrationService {
|
||||
): Promise<PublicApplicationRegistrationDTO | null> {
|
||||
const registration = await this.applicationRegistrationRepository.findOne({
|
||||
where: { oAuthClientId: clientId },
|
||||
select: ['id', 'name', 'logoUrl', 'websiteUrl', 'oAuthScopes'],
|
||||
select: ['id', 'name', 'manifest', 'oAuthScopes'],
|
||||
});
|
||||
|
||||
if (!registration) {
|
||||
@@ -112,22 +113,12 @@ export class ApplicationRegistrationService {
|
||||
return {
|
||||
id: registration.id,
|
||||
name: registration.name,
|
||||
logoUrl: registration.logoUrl,
|
||||
websiteUrl: registration.websiteUrl,
|
||||
logoUrl: registration.manifest?.application?.logoUrl ?? null,
|
||||
websiteUrl: registration.manifest?.application?.websiteUrl ?? null,
|
||||
oAuthScopes: registration.oAuthScopes,
|
||||
};
|
||||
}
|
||||
|
||||
async isOwnedByWorkspace(id: string, workspaceId: string): Promise<boolean> {
|
||||
const registration = await this.applicationRegistrationRepository.findOne({
|
||||
where: { id },
|
||||
select: ['id', 'ownerWorkspaceId'],
|
||||
});
|
||||
|
||||
return registration?.ownerWorkspaceId === workspaceId;
|
||||
}
|
||||
|
||||
// Global lookup — used by app sync to find existing registrations
|
||||
async findOneByUniversalIdentifier(
|
||||
universalIdentifier: string,
|
||||
): Promise<ApplicationRegistrationEntity | null> {
|
||||
@@ -172,17 +163,12 @@ export class ApplicationRegistrationService {
|
||||
this.applicationRegistrationRepository.create({
|
||||
universalIdentifier,
|
||||
name: input.name,
|
||||
description: input.description ?? null,
|
||||
logoUrl: input.logoUrl ?? null,
|
||||
author: input.author ?? null,
|
||||
oAuthClientId: clientId,
|
||||
oAuthClientSecretHash: clientSecretHash,
|
||||
oAuthRedirectUris: input.oAuthRedirectUris ?? [],
|
||||
oAuthScopes: input.oAuthScopes ?? [],
|
||||
createdByUserId,
|
||||
ownerWorkspaceId,
|
||||
websiteUrl: input.websiteUrl ?? null,
|
||||
termsUrl: input.termsUrl ?? null,
|
||||
});
|
||||
|
||||
const saved = await this.applicationRegistrationRepository.save(
|
||||
@@ -211,16 +197,10 @@ export class ApplicationRegistrationService {
|
||||
const updateData: Record<string, unknown> = {};
|
||||
|
||||
if (isDefined(update.name)) updateData.name = update.name;
|
||||
if (isDefined(update.description))
|
||||
updateData.description = update.description;
|
||||
if (isDefined(update.logoUrl)) updateData.logoUrl = update.logoUrl;
|
||||
if (isDefined(update.author)) updateData.author = update.author;
|
||||
if (isDefined(update.oAuthRedirectUris))
|
||||
updateData.oAuthRedirectUris = update.oAuthRedirectUris;
|
||||
if (isDefined(update.oAuthScopes))
|
||||
updateData.oAuthScopes = update.oAuthScopes;
|
||||
if (isDefined(update.websiteUrl)) updateData.websiteUrl = update.websiteUrl;
|
||||
if (isDefined(update.termsUrl)) updateData.termsUrl = update.termsUrl;
|
||||
if (isDefined(update.isListed)) updateData.isListed = update.isListed;
|
||||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
@@ -230,6 +210,21 @@ export class ApplicationRegistrationService {
|
||||
return this.findOneById(id, ownerWorkspaceId);
|
||||
}
|
||||
|
||||
async updateFromManifest(
|
||||
applicationRegistrationId: string,
|
||||
manifest: Manifest,
|
||||
): Promise<void> {
|
||||
const existing = await this.applicationRegistrationRepository.findOneOrFail(
|
||||
{ where: { id: applicationRegistrationId } },
|
||||
);
|
||||
|
||||
await this.applicationRegistrationRepository.save({
|
||||
...existing,
|
||||
name: manifest.application.displayName,
|
||||
manifest,
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string, ownerWorkspaceId: string): Promise<boolean> {
|
||||
await this.findOneById(id, ownerWorkspaceId);
|
||||
await this.applicationRegistrationRepository.softDelete(id);
|
||||
@@ -269,17 +264,12 @@ export class ApplicationRegistrationService {
|
||||
ApplicationRegistrationEntity,
|
||||
| 'universalIdentifier'
|
||||
| 'name'
|
||||
| 'description'
|
||||
| 'author'
|
||||
| 'sourceType'
|
||||
| 'sourcePackage'
|
||||
| 'logoUrl'
|
||||
| 'websiteUrl'
|
||||
| 'termsUrl'
|
||||
| 'latestAvailableVersion'
|
||||
| 'isListed'
|
||||
| 'isFeatured'
|
||||
| 'marketplaceDisplayData'
|
||||
| 'manifest'
|
||||
| 'ownerWorkspaceId'
|
||||
>,
|
||||
): Promise<void> {
|
||||
@@ -291,15 +281,12 @@ export class ApplicationRegistrationService {
|
||||
await this.applicationRegistrationRepository.save({
|
||||
...existing,
|
||||
name: params.name,
|
||||
description: params.description,
|
||||
author: params.author,
|
||||
sourceType: params.sourceType,
|
||||
sourcePackage: params.sourcePackage,
|
||||
logoUrl: params.logoUrl,
|
||||
websiteUrl: params.websiteUrl,
|
||||
termsUrl: params.termsUrl,
|
||||
latestAvailableVersion: params.latestAvailableVersion,
|
||||
marketplaceDisplayData: params.marketplaceDisplayData,
|
||||
manifest: params.manifest,
|
||||
isListed: params.isListed,
|
||||
isFeatured: params.isFeatured,
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -308,17 +295,12 @@ export class ApplicationRegistrationService {
|
||||
const registration = this.applicationRegistrationRepository.create({
|
||||
universalIdentifier: params.universalIdentifier,
|
||||
name: params.name,
|
||||
description: params.description,
|
||||
author: params.author,
|
||||
sourceType: params.sourceType,
|
||||
sourcePackage: params.sourcePackage,
|
||||
logoUrl: params.logoUrl,
|
||||
websiteUrl: params.websiteUrl,
|
||||
termsUrl: params.termsUrl,
|
||||
latestAvailableVersion: params.latestAvailableVersion,
|
||||
isListed: params.isListed,
|
||||
isFeatured: params.isFeatured,
|
||||
marketplaceDisplayData: params.marketplaceDisplayData,
|
||||
manifest: params.manifest,
|
||||
oAuthClientId: v4(),
|
||||
oAuthRedirectUris: [],
|
||||
oAuthScopes: [],
|
||||
@@ -341,7 +323,6 @@ export class ApplicationRegistrationService {
|
||||
universalIdentifier:
|
||||
TWENTY_CLI_APPLICATION_REGISTRATION.universalIdentifier,
|
||||
name: TWENTY_CLI_APPLICATION_REGISTRATION.name,
|
||||
description: TWENTY_CLI_APPLICATION_REGISTRATION.description,
|
||||
oAuthClientId: v4(),
|
||||
oAuthClientSecretHash: null,
|
||||
oAuthRedirectUris: [],
|
||||
|
||||
+14
@@ -63,6 +63,10 @@ export class ApplicationTarballService {
|
||||
};
|
||||
}>(contentDir, 'manifest.json');
|
||||
|
||||
const packageJson = await readJsonFile<{
|
||||
version: string;
|
||||
}>(contentDir, 'package.json');
|
||||
|
||||
if (manifest === null) {
|
||||
throw new ApplicationRegistrationException(
|
||||
'manifest.json not found or invalid in tarball',
|
||||
@@ -104,6 +108,10 @@ export class ApplicationTarballService {
|
||||
universalIdentifier,
|
||||
name: manifest.application?.displayName ?? 'Unknown App',
|
||||
sourceType: ApplicationRegistrationSourceType.TARBALL,
|
||||
manifest,
|
||||
latestAvailableVersion: packageJson?.version ?? null,
|
||||
isListed: false,
|
||||
isFeatured: false,
|
||||
oAuthClientId: v4(),
|
||||
oAuthRedirectUris: [],
|
||||
oAuthScopes: [],
|
||||
@@ -137,6 +145,12 @@ export class ApplicationTarballService {
|
||||
await this.appRegistrationRepository.update(appRegistration.id, {
|
||||
sourceType: ApplicationRegistrationSourceType.TARBALL,
|
||||
tarballFileId: savedFile.id,
|
||||
name: manifest.application?.displayName ?? 'Unknown App',
|
||||
manifest,
|
||||
latestAvailableVersion: packageJson?.version ?? null,
|
||||
isListed: false,
|
||||
isFeatured: false,
|
||||
ownerWorkspaceId: params.ownerWorkspaceId,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
|
||||
-30
@@ -16,24 +16,6 @@ export class CreateApplicationRegistrationInput {
|
||||
@MaxLength(256)
|
||||
name: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsString()
|
||||
@MaxLength(2000)
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsString()
|
||||
@MaxLength(2048)
|
||||
@IsOptional()
|
||||
logoUrl?: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsString()
|
||||
@MaxLength(256)
|
||||
@IsOptional()
|
||||
author?: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@@ -54,16 +36,4 @@ export class CreateApplicationRegistrationInput {
|
||||
@MaxLength(256, { each: true })
|
||||
@IsOptional()
|
||||
oAuthScopes?: string[];
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsString()
|
||||
@MaxLength(2048)
|
||||
@IsOptional()
|
||||
websiteUrl?: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsString()
|
||||
@MaxLength(2048)
|
||||
@IsOptional()
|
||||
termsUrl?: string;
|
||||
}
|
||||
|
||||
-30
@@ -21,24 +21,6 @@ export class UpdateApplicationRegistrationPayload {
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsString()
|
||||
@MaxLength(2000)
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsString()
|
||||
@MaxLength(2048)
|
||||
@IsOptional()
|
||||
logoUrl?: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsString()
|
||||
@MaxLength(256)
|
||||
@IsOptional()
|
||||
author?: string;
|
||||
|
||||
@Field(() => [String], { nullable: true })
|
||||
@IsArray()
|
||||
@ArrayMaxSize(20)
|
||||
@@ -55,18 +37,6 @@ export class UpdateApplicationRegistrationPayload {
|
||||
@IsOptional()
|
||||
oAuthScopes?: string[];
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsString()
|
||||
@MaxLength(2048)
|
||||
@IsOptional()
|
||||
websiteUrl?: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsString()
|
||||
@MaxLength(2048)
|
||||
@IsOptional()
|
||||
termsUrl?: string;
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
|
||||
Reference in New Issue
Block a user