martmull
2026-03-30 15:03:23 +00:00
committed by GitHub
parent 40abe1e6d0
commit 8985dfbc5d
21 changed files with 181 additions and 104 deletions
@@ -1,29 +0,0 @@
export type CuratedAppEntry = {
universalIdentifier: string;
sourcePackage: string;
isFeatured: boolean;
name: string;
description: string;
author: string;
logoUrl?: string;
websiteUrl?: string;
termsUrl?: string;
latestAvailableVersion?: string;
};
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: 'a1b2c3d4-0000-0000-0000-000000000001',
sourcePackage: '@twentyhq/app-data-enrichment',
isFeatured: true,
name: 'Data Enrichment',
description: 'Enrich your data easily. Choose your provider.',
author: 'Twenty',
logoUrl: ENCODED_MOCK_LOGO,
websiteUrl: 'https://twenty.com',
latestAvailableVersion: '1.0.0',
},
];
@@ -0,0 +1,4 @@
export const MARKETPLACE_CURATED_APPLICATIONS: {
universalIdentifier: string;
position?: number;
}[] = [];
@@ -2,11 +2,11 @@ import { Injectable, Logger } from '@nestjs/common';
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 { 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';
import { MARKETPLACE_CURATED_APPLICATIONS } from 'src/engine/core-modules/application/application-marketplace/constants/marketplace-curated-applications.constant';
@Injectable()
export class MarketplaceCatalogSyncService {
@@ -19,59 +19,54 @@ export class MarketplaceCatalogSyncService {
) {}
async syncCatalog(): Promise<void> {
await this.syncCuratedApps();
await this.syncRegistryApps();
this.logger.log('Marketplace catalog sync completed');
}
private async syncCuratedApps(): Promise<void> {
for (const entry of MARKETPLACE_CATALOG_INDEX) {
try {
await this.applicationRegistrationService.upsertFromCatalog({
universalIdentifier: entry.universalIdentifier,
name: entry.name,
sourceType: ApplicationRegistrationSourceType.NPM,
sourcePackage: entry.sourcePackage,
latestAvailableVersion: entry.latestAvailableVersion ?? null,
isListed: true,
isFeatured: entry.isFeatured,
manifest: null,
ownerWorkspaceId: null,
});
} catch (error) {
this.logger.error(
`Failed to sync curated app "${entry.name}": ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
private async syncRegistryApps(): Promise<void> {
const packages = await this.marketplaceService.fetchAppsFromRegistry();
const curatedIdentifiers = new Set(
MARKETPLACE_CATALOG_INDEX.map((entry) => entry.universalIdentifier),
MARKETPLACE_CURATED_APPLICATIONS.map(
(entry) => entry.universalIdentifier,
),
);
for (const pkg of packages) {
try {
const manifest =
const fetchedManifest =
await this.marketplaceService.fetchManifestFromRegistryCdn(
pkg.name,
pkg.version,
);
if (!manifest) {
if (!fetchedManifest) {
this.logger.debug(`Skipping ${pkg.name}: no manifest found on CDN`);
continue;
}
const universalIdentifier = manifest.application.universalIdentifier;
const universalIdentifier =
fetchedManifest.application.universalIdentifier;
if (curatedIdentifiers.has(universalIdentifier)) {
continue;
}
const isFeatured = curatedIdentifiers.has(universalIdentifier);
const aboutDescription =
fetchedManifest.application.aboutDescription ??
(await this.marketplaceService.fetchReadmeFromRegistryCdn(
pkg.name,
pkg.version,
));
const manifest = aboutDescription
? {
...fetchedManifest,
application: {
...fetchedManifest.application,
aboutDescription,
},
}
: fetchedManifest;
const cdnBaseUrl = this.twentyConfigService.get('APP_REGISTRY_CDN_URL');
@@ -93,7 +88,7 @@ export class MarketplaceCatalogSyncService {
sourcePackage: pkg.name,
latestAvailableVersion: pkg.version ?? null,
isListed: true,
isFeatured: false,
isFeatured,
manifest: manifestWithResolvedUrls,
ownerWorkspaceId: null,
});
@@ -73,6 +73,39 @@ export class MarketplaceService {
}
}
async fetchReadmeFromRegistryCdn(
packageName: string,
version: string,
): Promise<string | null> {
const cdnBaseUrl = this.twentyConfigService.get('APP_REGISTRY_CDN_URL');
const url = buildRegistryCdnUrl({
cdnBaseUrl,
packageName,
version,
filePath: 'README.md',
});
try {
const { data } = await axios.get(url, {
headers: { 'User-Agent': 'Twenty-Marketplace' },
timeout: 5_000,
responseType: 'text',
});
if (!data || data.trim().length === 0) {
return null;
}
return data;
} catch {
this.logger.debug(
`Could not fetch README from CDN for ${packageName}@${version}`,
);
return null;
}
}
async fetchAppsFromRegistry(): Promise<RegistryPackageInfo[]> {
const registryUrl = this.twentyConfigService.get('APP_REGISTRY_URL');