feat: add package pruning system with configurable grace periods

- Implemented automatic pruning of stale packages across all platform sync endpoints (Arch, AUR, Debian, Ubuntu, Fedora, Homebrew, Winget)
- Added `last_seen_at` tracking to mark packages as seen during sync runs and identify inactive packages
- Introduced `PRUNE_GRACE_DAYS` and `PRUNE_HARD_DELETE_DAYS` environment variables for configurable soft-delete and hard-delete thresholds
This commit is contained in:
Yusuf İpek
2025-11-13 01:54:23 +03:00
parent 560e78cb96
commit ea792a8a7f
15 changed files with 242 additions and 17 deletions
+8 -3
View File
@@ -175,8 +175,8 @@ export class ArchPackageFetcher {
if (existingResult.rows.length === 0) {
// Insert new package (id will be auto-generated by SERIAL)
await query(
`INSERT INTO packages (id, name, description, version, platform_id, type, repository, popularity_score, is_active)
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8)`,
`INSERT INTO packages (id, name, description, version, platform_id, type, repository, popularity_score, is_active, last_seen_at)
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, NOW())`,
[pkg.name, pkg.description, pkg.version, 'arch', null, 'official', 0, true]
)
stored++
@@ -184,12 +184,17 @@ export class ArchPackageFetcher {
// Update if version changed
await query(
`UPDATE packages
SET version = $1, description = $2, repository = $3, updated_at = NOW()
SET version = $1, description = $2, repository = $3, last_seen_at = NOW(), is_active = true, updated_at = NOW()
WHERE id = $4`,
[pkg.version, pkg.description, 'official', existingResult.rows[0].id]
)
updated++
} else {
// Still mark as seen to avoid pruning
await query(
`UPDATE packages SET last_seen_at = NOW(), is_active = true WHERE id = $1`,
[existingResult.rows[0].id]
)
skipped++
}
+8 -3
View File
@@ -190,18 +190,23 @@ export class AurPackageFetcher {
)
if (existing.rows.length === 0) {
await query(
`INSERT INTO packages (id, name, description, version, platform_id, type, repository, popularity_score, is_active)
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8)`,
`INSERT INTO packages (id, name, description, version, platform_id, type, repository, popularity_score, is_active, last_seen_at)
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, NOW())`,
[pkg.name, pkg.description, pkg.version, 'arch', null, 'aur', 0, true]
)
stored++
} else if (existing.rows[0].version !== pkg.version) {
await query(
`UPDATE packages SET version = $1, description = $2, repository = $3, updated_at = NOW() WHERE id = $4`,
`UPDATE packages SET version = $1, description = $2, repository = $3, last_seen_at = NOW(), is_active = true, updated_at = NOW() WHERE id = $4`,
[pkg.version, pkg.description, 'aur', existing.rows[0].id]
)
updated++
} else {
// Still mark as seen to avoid pruning
await query(
`UPDATE packages SET last_seen_at = NOW(), is_active = true WHERE id = $1`,
[existing.rows[0].id]
)
skipped++
}
if (onProgress && (stored + updated + skipped) % 100 === 0) {
+8 -3
View File
@@ -144,8 +144,8 @@ export class FedoraPackageFetcher {
if (existingResult.rows.length === 0) {
// Insert new package
await query(
`INSERT INTO packages (id, name, description, version, platform_id, type, repository, popularity_score, is_active)
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8)`,
`INSERT INTO packages (id, name, description, version, platform_id, type, repository, popularity_score, is_active, last_seen_at)
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, NOW())`,
[pkg.name, pkg.description, pkg.version, 'fedora', null, null, 0, true]
)
stored++
@@ -153,12 +153,17 @@ export class FedoraPackageFetcher {
// Update if version changed
await query(
`UPDATE packages
SET version = $1, description = $2, repository = $3, updated_at = NOW()
SET version = $1, description = $2, repository = $3, last_seen_at = NOW(), is_active = true, updated_at = NOW()
WHERE id = $4`,
[pkg.version, pkg.description, null, existingResult.rows[0].id]
)
updated++
} else {
// Still mark as seen to avoid pruning
await query(
`UPDATE packages SET last_seen_at = NOW(), is_active = true WHERE id = $1`,
[existingResult.rows[0].id]
)
skipped++
}
+8 -3
View File
@@ -143,8 +143,8 @@ export class HomebrewPackageFetcher {
if (existingResult.rows.length === 0) {
// Insert new package
await query(
`INSERT INTO packages (id, name, description, version, platform_id, type, repository, popularity_score, is_active)
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8)`,
`INSERT INTO packages (id, name, description, version, platform_id, type, repository, popularity_score, is_active, last_seen_at)
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, NOW())`,
[pkg.name, pkg.description, pkg.version, 'macos', null, null, 0, true]
)
stored++
@@ -152,12 +152,17 @@ export class HomebrewPackageFetcher {
// Update if version changed
await query(
`UPDATE packages
SET version = $1, description = $2, updated_at = NOW()
SET version = $1, description = $2, last_seen_at = NOW(), is_active = true, updated_at = NOW()
WHERE id = $3`,
[pkg.version, pkg.description, existingResult.rows[0].id]
)
updated++
} else {
// Still mark as seen to avoid pruning
await query(
`UPDATE packages SET last_seen_at = NOW(), is_active = true WHERE id = $1`,
[existingResult.rows[0].id]
)
skipped++
}
+7 -2
View File
@@ -210,8 +210,8 @@ export class PackageService {
`INSERT INTO packages (
id, name, description, version, platform_id, category_id,
license_id, type, repository, homepage_url, download_url,
popularity_score
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
popularity_score, last_seen_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW())
RETURNING *`,
[id, name, description, version, platform_id, category_id,
license_id, type, repository, homepage_url, download_url,
@@ -238,6 +238,11 @@ export class PackageService {
}
}
// Always mark as seen and active on update
fields.push('last_seen_at = NOW()')
fields.push('is_active = true')
fields.push('updated_at = NOW()')
if (fields.length === 0) {
return this.getById(id)
}
+8 -3
View File
@@ -230,8 +230,8 @@ export class WingetPackageFetcher {
if (existingResult.rows.length === 0) {
// Insert new package
await query(
`INSERT INTO packages (id, name, description, version, platform_id, type, repository, popularity_score, is_active)
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8)`,
`INSERT INTO packages (id, name, description, version, platform_id, type, repository, popularity_score, is_active, last_seen_at)
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, NOW())`,
[pkg.name, pkg.description, pkg.version, 'windows', null, null, 0, true]
)
stored++
@@ -239,12 +239,17 @@ export class WingetPackageFetcher {
// Update if version changed
await query(
`UPDATE packages
SET version = $1, description = $2, updated_at = NOW()
SET version = $1, description = $2, last_seen_at = NOW(), is_active = true, updated_at = NOW()
WHERE id = $3`,
[pkg.version, pkg.description, existingResult.rows[0].id]
)
updated++
} else {
// Still mark as seen to avoid pruning
await query(
`UPDATE packages SET last_seen_at = NOW(), is_active = true WHERE id = $1`,
[existingResult.rows[0].id]
)
skipped++
}