recoder-code
Version:
🚀 AI-powered development platform - Chat with 32+ models, build projects, automate workflows. Free models included!
547 lines • 24.2 kB
JavaScript
"use strict";
/**
* Package Service - Handles plugin package operations
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PackageService = void 0;
const Package_1 = require("../entities/Package");
const PackageVersion_1 = require("../entities/PackageVersion");
const User_1 = require("../entities/User");
//import { PackageStats } from '../entities/PackageStats';
const semver = __importStar(require("semver"));
const crypto = __importStar(require("crypto"));
const SecurityService_1 = require("./SecurityService");
const StorageService_1 = require("./StorageService");
const ValidationService_1 = require("./ValidationService");
// import { NotificationService } from './NotificationService';
// import { QueueService } from './QueueService';
const config_1 = __importDefault(require("../config"));
const database_1 = require("../database");
// Simple logger for now
const logger = {
info: (msg, ...args) => console.log(`[INFO] ${msg}`, ...args),
error: (msg, ...args) => console.error(`[ERROR] ${msg}`, ...args),
warn: (msg, ...args) => console.warn(`[WARN] ${msg}`, ...args)
};
class PackageService {
constructor() {
this.packageRepo = database_1.AppDataSource.getRepository(Package_1.Package);
this.versionRepo = database_1.AppDataSource.getRepository(PackageVersion_1.PackageVersion);
this.userRepo = database_1.AppDataSource.getRepository(User_1.User);
this.securityService = new SecurityService_1.SecurityService();
this.storageService = new StorageService_1.StorageService();
this.validationService = new ValidationService_1.ValidationService(config_1.default);
}
async publishPackage(packageData, user, tarballBuffer) {
const errors = [];
try {
logger.info(`Publishing package: ${packageData.name}@${packageData.version} by ${user.username}`);
// Basic package data validation
if (!packageData.name || !packageData.version) {
return { success: false, errors: ['Package name and version are required'] };
}
// Check if package exists
let existingPackage = await this.packageRepo.findOne({
where: { name: packageData.name },
relations: ['versions', 'maintainers']
});
// If package doesn't exist, create it
if (!existingPackage) {
existingPackage = this.packageRepo.create({
name: packageData.name,
description: packageData.description || '',
keywords: packageData.keywords || [],
author: { name: packageData.author?.name || user.username, email: packageData.author?.email || user.email },
license: packageData.license || 'MIT',
homepage: packageData.homepage,
repository: packageData.repository,
bugs: packageData.bugs,
categories: packageData.categories || [],
owner: user,
created_at: new Date(),
updated_at: new Date()
});
await this.packageRepo.save(existingPackage);
logger.info(`Created new package: ${packageData.name}`);
}
else {
// Check if user has permission to publish
const isOwner = existingPackage.owner?.id === user.id;
if (!isOwner && !user.is_admin) {
return { success: false, errors: ['You do not have permission to publish to this package'] };
}
// Update package metadata
existingPackage.description = packageData.description || existingPackage.description;
existingPackage.keywords = packageData.keywords || existingPackage.keywords;
existingPackage.license = packageData.license || existingPackage.license;
existingPackage.homepage = packageData.homepage || existingPackage.homepage;
existingPackage.repository = packageData.repository || existingPackage.repository;
existingPackage.bugs = packageData.bugs || existingPackage.bugs;
existingPackage.categories = packageData.categories || existingPackage.categories;
existingPackage.updated_at = new Date();
await this.packageRepo.save(existingPackage);
}
// Check if version already exists
const existingVersion = await this.versionRepo.findOne({
where: {
package: { id: existingPackage.id },
version: packageData.version
}
});
if (existingVersion) {
return { success: false, errors: [`Version ${packageData.version} already exists`] };
}
// Process tarball
let tarballPath;
let shasum;
let integrity;
let size = 0;
if (tarballBuffer) {
// Basic tarball validation
if (tarballBuffer.length === 0) {
return { success: false, errors: ['Empty tarball'] };
}
if (tarballBuffer.length > 50 * 1024 * 1024) { // 50MB limit
return { success: false, errors: ['Tarball too large'] };
}
// Calculate checksums
shasum = crypto.createHash('sha1').update(tarballBuffer).digest('hex');
integrity = `sha512-${crypto.createHash('sha512').update(tarballBuffer).digest('base64')}`;
size = tarballBuffer.length;
// Store tarball (simplified)
tarballPath = `tarballs/${existingPackage.name}-${packageData.version}.tgz`;
}
// Create package version
const packageVersion = this.versionRepo.create({
package: existingPackage,
package_id: existingPackage.id,
version: packageData.version,
description: packageData.description || '',
main: packageData.main || 'index.js',
engines: packageData.engines || {},
dependencies: packageData.dependencies || {},
dev_dependencies: packageData.devDependencies || {},
peer_dependencies: packageData.peerDependencies || {},
dist: {
tarball: tarballPath || '',
shasum: shasum || '',
integrity: integrity,
size: size
},
published_by: user,
published_by_id: user.id,
published_at: new Date()
});
await this.versionRepo.save(packageVersion);
// Update package stats
await this.updatePackageStats(existingPackage.id, 'publish');
// Background tasks would be queued here
// await this.queueService.addJob('package-published', {...});
// Notifications would be sent here
// await this.notificationService.notifyPackagePublished(...);
logger.info(`Successfully published: ${packageData.name}@${packageData.version}`);
return {
success: true,
package: existingPackage,
version: packageVersion
};
}
catch (error) {
logger.error(`Failed to publish package: ${error?.message || String(error)}`, error);
return { success: false, errors: [`Internal error: ${error?.message || String(error)}`] };
}
}
async getPackage(name) {
return this.packageRepo.findOne({
where: { name },
relations: ['versions', 'owner']
});
}
async getPackageVersion(name, version) {
return this.versionRepo.findOne({
where: {
package: { name },
version
},
relations: ['package', 'publishedBy']
});
}
async searchPackages(options) {
const { query, category, keywords, author, license, minDownloads = 0, sortBy = 'relevance', sortOrder = 'desc', limit = 20, offset = 0 } = options;
const queryBuilder = this.packageRepo.createQueryBuilder('package')
.leftJoinAndSelect('package.versions', 'versions')
.leftJoinAndSelect('package.owner', 'owner');
// Text search
if (query) {
queryBuilder.andWhere('(package.name ILIKE :query OR package.description ILIKE :query OR package.keywords::text ILIKE :query)', { query: `%${query}%` });
}
// Category filter
if (category) {
queryBuilder.andWhere(':category = ANY(package.categories)', { category });
}
// Keywords filter
if (keywords && keywords.length > 0) {
queryBuilder.andWhere('package.keywords && :keywords', { keywords });
}
// Author filter
if (author) {
queryBuilder.andWhere('package.author ILIKE :author', { author: `%${author}%` });
}
// License filter
if (license) {
queryBuilder.andWhere('package.license = :license', { license });
}
// Minimum downloads filter
if (minDownloads > 0) {
queryBuilder.andWhere('package.download_count >= :minDownloads', { minDownloads });
}
// Sorting
switch (sortBy) {
case 'downloads':
queryBuilder.orderBy('package.download_count', sortOrder.toUpperCase());
break;
case 'updated':
queryBuilder.orderBy('package.updated_at', sortOrder.toUpperCase());
break;
case 'created':
queryBuilder.orderBy('package.created_at', sortOrder.toUpperCase());
break;
default:
// Relevance (default)
if (query) {
queryBuilder.orderBy('package.download_count', 'DESC');
}
else {
queryBuilder.orderBy('package.download_count', 'DESC');
}
}
// Pagination
queryBuilder.skip(offset).take(limit);
const [packages, total] = await queryBuilder.getManyAndCount();
return {
packages,
total,
page: Math.floor(offset / limit) + 1,
limit
};
}
async downloadPackage(name, version) {
try {
const packageVersion = await this.getPackageVersion(name, version);
if (!packageVersion) {
return { success: false, error: 'Package version not found' };
}
if (!packageVersion.dist?.tarball) {
return { success: false, error: 'Tarball not available' };
}
// Update download stats
await this.updatePackageStats(packageVersion.package.id, 'download');
// Get download stream (simplified)
const stream = undefined; // Would implement actual stream here
const filename = `${name}-${version}.tgz`;
return {
success: true,
stream,
filename,
contentType: 'application/octet-stream',
size: packageVersion.dist?.size
};
}
catch (error) {
logger.error(`Failed to download package: ${error?.message || String(error)}`, error);
return { success: false, error: 'Download failed' };
}
}
async unpublishPackage(name, version, user) {
try {
const pkg = await this.getPackage(name);
if (!pkg) {
return { success: false, error: 'Package not found' };
}
// Check permissions
const isMaintainer = pkg.maintainers.some(m => m.name === user.username || m.email === user.email);
if (!isMaintainer && !user.is_admin) {
return { success: false, error: 'Permission denied' };
}
const packageVersion = await this.getPackageVersion(name, version);
if (!packageVersion) {
return { success: false, error: 'Version not found' };
}
// Check if this is the only version
const versionCount = await this.versionRepo.count({
where: { package: { id: pkg.id } }
});
if (versionCount === 1) {
// Delete entire package
await this.deletePackage(name, user);
}
else {
// Delete version
await this.versionRepo.remove(packageVersion);
// Delete tarball
if (packageVersion.dist?.tarball) {
// await this.storageService.deleteTarball(packageVersion.dist?.tarball);
}
// Update package timestamp
pkg.updated_at = new Date();
await this.packageRepo.save(pkg);
}
logger.info(`Unpublished: ${name}@${version} by ${user.username}`);
return { success: true };
}
catch (error) {
logger.error(`Failed to unpublish package: ${error?.message || String(error)}`, error);
return { success: false, error: 'Unpublish failed' };
}
}
async deletePackage(name, user) {
try {
const pkg = await this.getPackage(name);
if (!pkg) {
return { success: false, error: 'Package not found' };
}
// Check permissions
const isMaintainer = pkg.maintainers.some(m => m.name === user.username || m.email === user.email);
if (!isMaintainer && !user.is_admin) {
return { success: false, error: 'Permission denied' };
}
// Delete all tarballs
for (const version of pkg.versions) {
if (version.dist?.tarball) {
// await this.storageService.deleteTarball(version.dist?.tarball);
}
}
// Delete package and all related data (cascade)
await this.packageRepo.remove(pkg);
logger.info(`Deleted package: ${name} by ${user.username}`);
return { success: true };
}
catch (error) {
logger.error(`Failed to delete package: ${error?.message || String(error)}`, error);
return { success: false, error: 'Delete failed' };
}
}
async getPackageStats(name) {
const pkg = await this.getPackage(name);
if (!pkg || !pkg.stats)
return null;
return {
packageName: pkg.name,
totalDownloads: pkg.download_count || 0,
monthlyDownloads: pkg.stats.monthly_downloads || 0,
weeklyDownloads: pkg.stats.weekly_downloads || 0,
lastUpdateDate: pkg.updated_at
};
}
async getPopularPackages(limit = 10) {
return this.packageRepo.createQueryBuilder('package')
.leftJoinAndSelect('package.versions', 'versions')
.leftJoinAndSelect('package.owner', 'owner')
.orderBy('package.download_count', 'DESC')
.take(limit)
.getMany();
}
async getRecentlyUpdated(limit = 10) {
return this.packageRepo.find({
relations: ['versions', 'owner'],
order: { updated_at: 'DESC' },
take: limit
});
}
async addMaintainer(packageName, username, currentUser) {
try {
const pkg = await this.getPackage(packageName);
if (!pkg) {
return { success: false, error: 'Package not found' };
}
// Check permissions
const isMaintainer = pkg.maintainers.some(m => m.name === currentUser.username || m.email === currentUser.email);
if (!isMaintainer && !currentUser.is_admin) {
return { success: false, error: 'Permission denied' };
}
const newMaintainer = await this.userRepo.findOne({ where: { username } });
if (!newMaintainer) {
return { success: false, error: 'User not found' };
}
// Check if already a maintainer
const isAlreadyMaintainer = pkg.maintainers.some(m => m.name === newMaintainer.username || m.email === newMaintainer.email);
if (isAlreadyMaintainer) {
return { success: false, error: 'User is already a maintainer' };
}
pkg.maintainers.push({ name: newMaintainer.username, email: newMaintainer.email });
await this.packageRepo.save(pkg);
logger.info(`Added maintainer ${username} to package ${packageName}`);
return { success: true };
}
catch (error) {
logger.error(`Failed to add maintainer: ${error?.message || String(error)}`, error);
return { success: false, error: 'Failed to add maintainer' };
}
}
async removeMaintainer(packageName, username, currentUser) {
try {
const pkg = await this.getPackage(packageName);
if (!pkg) {
return { success: false, error: 'Package not found' };
}
// Check permissions
const isMaintainer = pkg.maintainers.some(m => m.name === currentUser.username || m.email === currentUser.email);
if (!isMaintainer && !currentUser.is_admin) {
return { success: false, error: 'Permission denied' };
}
// Can't remove the last maintainer
if (pkg.maintainers.length === 1) {
return { success: false, error: 'Cannot remove the last maintainer' };
}
const maintainerToRemove = pkg.maintainers.find(m => m.name === username);
if (!maintainerToRemove) {
return { success: false, error: 'User is not a maintainer' };
}
pkg.maintainers = pkg.maintainers.filter(m => m.name !== maintainerToRemove.name);
await this.packageRepo.save(pkg);
logger.info(`Removed maintainer ${username} from package ${packageName}`);
return { success: true };
}
catch (error) {
logger.error(`Failed to remove maintainer: ${error?.message || String(error)}`, error);
return { success: false, error: 'Failed to remove maintainer' };
}
}
async updatePackageStats(packageId, action) {
const pkg = await this.packageRepo.findOne({ where: { id: packageId } });
if (!pkg)
return;
switch (action) {
case 'download':
pkg.download_count += 1;
if (!pkg.stats) {
pkg.stats = {
weekly_downloads: 0,
monthly_downloads: 0,
yearly_downloads: 0,
dependents: 0,
dependencies: 0
};
}
pkg.stats.weekly_downloads += 1;
pkg.stats.monthly_downloads += 1;
pkg.stats.yearly_downloads += 1;
break;
case 'publish':
pkg.version_count += 1;
pkg.last_published = new Date();
break;
}
await this.packageRepo.save(pkg);
}
async getLatestVersion(packageName) {
const versions = await this.versionRepo.find({
where: { package: { name: packageName } },
select: ['version']
});
if (versions.length === 0)
return null;
const sortedVersions = versions
.map(v => v.version)
.sort((a, b) => semver.rcompare(a, b));
return sortedVersions[0];
}
async getVersions(packageName) {
const versions = await this.versionRepo.find({
where: { package: { name: packageName } },
select: ['version'],
order: { published_at: 'DESC' }
});
return versions.map(v => v.version);
}
async getPackageVersions(packageName) {
return await this.versionRepo.find({
where: { package: { name: packageName } },
order: { published_at: 'DESC' }
});
}
async incrementDownloadCount(packageName, version) {
try {
const packageVersion = await this.getPackageVersion(packageName, version);
if (packageVersion) {
packageVersion.download_count = (packageVersion.download_count || 0) + 1;
await this.versionRepo.save(packageVersion);
}
}
catch (error) {
logger.error(`Failed to increment download count for ${packageName}@${version}:`, error);
}
}
async unpublishVersion(packageName, version, user) {
return this.unpublishPackage(packageName, version, user);
}
async deprecateVersion(packageName, version, user, message) {
try {
const packageVersion = await this.getPackageVersion(packageName, version);
if (!packageVersion) {
return { success: false, error: 'Version not found' };
}
const pkg = await this.getPackage(packageName);
if (!pkg) {
return { success: false, error: 'Package not found' };
}
// Check permissions
const isMaintainer = pkg.maintainers.some(m => m.name === user.username || m.email === user.email);
if (!isMaintainer && !user.is_admin) {
return { success: false, error: 'Permission denied' };
}
// Mark version as deprecated
packageVersion.deprecate(message);
await this.versionRepo.save(packageVersion);
logger.info(`Deprecated version: ${packageName}@${version} by ${user.username}`);
return { success: true };
}
catch (error) {
logger.error(`Failed to deprecate version: ${error?.message || String(error)}`, error);
return { success: false, error: 'Deprecation failed' };
}
}
async addCollaborator(packageName, username, currentUser) {
return this.addMaintainer(packageName, username, currentUser);
}
async removeCollaborator(packageName, username, currentUser) {
return this.removeMaintainer(packageName, username, currentUser);
}
async getPackageCount() {
try {
return await this.packageRepo.count();
}
catch (error) {
logger.error('Failed to get package count:', error);
throw error;
}
}
}
exports.PackageService = PackageService;
//# sourceMappingURL=PackageService.js.map