feat(17-02): RSS feeds get an owner — platform-wide vs personal (D-02)
- TenderRssFeedSource.userId/tenantId (nullable): null = platform-wide
(admin-managed, includes the existing service.bund.de default),
set = personal feed owned by exactly one user
- Migration replaces url @unique with @@unique([userId, url]) — two
users can now follow the same address independently; existing rows
keep an empty owner (platform-wide, unchanged behavior)
- Service: listForUser/createForUser/createPlatform replace list/create
- Controller: GET/POST /rss-feeds move from @Roles(ADMIN,SUPER_ADMIN) to
@UseModule('tender-radar'); POST with scope:'platform' still requires
ADMIN/SUPER_ADMIN, checked inline (T-17-08)
- tenders.module.ts seed switched from upsert-on-url to find-then-create
(Rule 3, pulled forward from Task 3): the new compound unique index
requires a non-null userId in Prisma's generated type, so a
platform-wide row can no longer be addressed via upsert
- Files modified: apps/api/prisma/schema.prisma, apps/api/prisma/migrations/20260812110000_tender_rss_feed_owner/migration.sql, apps/api/src/tenders/tender-rss-feed.service.ts, apps/api/src/tenders/dto/tender-rss-feed.dto.ts, apps/api/src/tenders/tenders.controller.ts, apps/api/src/tenders/tenders.module.ts, apps/api/src/tenders/tender-rss-feed.service.spec.ts, apps/api/src/tenders/tenders.controller.spec.ts
This commit is contained in:
@@ -546,19 +546,40 @@ model TenderSourcePollConfig {
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// Phase 14, Plan 02 (INGEST-04, D-14) — admin-managed GLOBAL RSS feed list.
|
||||
// Deliberately NO tenantId (mirrors TenderSourcePollConfig's global/
|
||||
// RLS-exempt stance, D-08: RSS feeds are public and identical for every
|
||||
// tenant). Feed URLs are RUNTIME admin input — unlike the hardcoded
|
||||
// Phase 14, Plan 02 (INGEST-04, D-14) — admin-managed RSS feed list.
|
||||
// Feed URLs are RUNTIME admin/user input — unlike the hardcoded
|
||||
// NETSERVER_PORTALS/COSINEX_BASE_URL constants, the code-level
|
||||
// SourceRegistry denylist gate does NOT cover this data (RESEARCH.md
|
||||
// Pitfall 3); TenderRssFeedSourceService enforces a SEPARATE save-time
|
||||
// hostname/SSRF guard (T-14-02-01) on create/update.
|
||||
//
|
||||
// Phase 17, Plan 02 (D-02): the list is now two-part. `userId = null` is a
|
||||
// PLATFORM-WIDE feed — admin-managed, active for every tenant (mirrors
|
||||
// TenderSourcePollConfig's global/RLS-exempt stance, D-08); this is the
|
||||
// bucket the service.bund.de default (seeded since Phase 14) lives in, and
|
||||
// stays there unmigrated (17-CONTEXT.md, offener Punkt 2). `userId` set is
|
||||
// a PERSONAL feed owned by exactly one user. `tenantId` is denormalized
|
||||
// from the owner (same role as `TenderEmailConfig.tenantId`, Phase 17 Plan
|
||||
// 01) — null for platform-wide feeds, set for personal feeds so
|
||||
// `RssAdapter` can tag their ingested `Tender` rows with the existing D-13
|
||||
// `ownerTenantId` origin marking (D-06, this plan).
|
||||
//
|
||||
// `@@unique([userId, url])` replaces the old `url @unique`: two different
|
||||
// users may now follow the same address. NULL is distinct per-row in a
|
||||
// PostgreSQL unique index, so this does NOT prevent the same URL being
|
||||
// registered twice platform-wide (both userId NULL) — deliberately
|
||||
// accepted, see 17-02-PLAN.md Objective (T-17-13): cross-source dedup
|
||||
// absorbs the duplicate, only costing one extra fetch.
|
||||
model TenderRssFeedSource {
|
||||
id String @id @default(uuid())
|
||||
url String @unique
|
||||
url String
|
||||
label String
|
||||
isActive Boolean @default(true)
|
||||
userId String? // null = platform-wide (D-02); set = personal feed owner
|
||||
tenantId String? // denormalized owner's tenant, null for platform-wide feeds (D-06)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([userId, url])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user