Improve apps (#19120)
fixes https://discord.com/channels/1130383047699738754/1488094970241089586
This commit is contained in:
-29
@@ -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',
|
||||
},
|
||||
];
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export const MARKETPLACE_CURATED_APPLICATIONS: {
|
||||
universalIdentifier: string;
|
||||
position?: number;
|
||||
}[] = [];
|
||||
+27
-32
@@ -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,
|
||||
});
|
||||
|
||||
+33
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user