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:
@@ -0,0 +1,33 @@
|
||||
-- Phase 17 (D-02): RSS-Feeds bekommen einen Besitzer. Bisher galt
|
||||
-- "TenderRssFeedSource.url" plattformweit eindeutig -- ab dieser Migration
|
||||
-- gibt es zwei Sorten: plattformweite Feeds (userId = NULL, von der
|
||||
-- Administration gepflegt, gilt fuer alle -- dazu gehoert der seit Phase 14
|
||||
-- gesetzte service.bund.de-Feed) und persoenliche Feeds (userId gesetzt,
|
||||
-- gehoeren genau einem Nutzer). Bestandszeilen werden NICHT angefasst --
|
||||
-- sie behalten einen leeren Besitzer und sind damit plattformweit, also
|
||||
-- genau der heutige Zustand (17-CONTEXT.md, offener Punkt 2).
|
||||
--
|
||||
-- tenantId ist -- wie bei TenderEmailConfig aus Plan 17-01 -- ein
|
||||
-- denormalisiertes Feld des Besitzers, leer bei plattformweiten Feeds;
|
||||
-- es traegt spaeter die D-13-Herkunftsmarkierung fuer Ausschreibungen aus
|
||||
-- persoenlichen Feeds (D-06, 17-02-PLAN.md).
|
||||
|
||||
-- 1. Beide Spalten ohne Pflichtwert -- Bestandszeilen bleiben unangetastet
|
||||
-- (leerer Besitzer = plattformweit, entspricht dem Ist-Zustand).
|
||||
ALTER TABLE "TenderRssFeedSource" ADD COLUMN "userId" TEXT;
|
||||
ALTER TABLE "TenderRssFeedSource" ADD COLUMN "tenantId" TEXT;
|
||||
|
||||
-- 2. Alte Eindeutigkeitsregel "eine Adresse plattformweit" aufheben --
|
||||
-- zwei Nutzer sollen dieselbe Adresse unabhaengig voneinander verfolgen
|
||||
-- koennen.
|
||||
DROP INDEX "TenderRssFeedSource_url_key";
|
||||
|
||||
-- 3. Neue Eindeutigkeitsregel: eine Adresse ist je Besitzer eindeutig.
|
||||
-- Leere Werte gelten in PostgreSQL in einer Eindeutigkeitsregel als
|
||||
-- jeweils verschieden -- das deckt daher nur persoenliche Feeds ab,
|
||||
-- nicht doppelte plattformweite Feeds (siehe Begruendung 17-02-PLAN.md
|
||||
-- Objective, T-17-13, bewusst hingenommen).
|
||||
CREATE UNIQUE INDEX "TenderRssFeedSource_userId_url_key" ON "TenderRssFeedSource"("userId", "url");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TenderRssFeedSource_userId_idx" ON "TenderRssFeedSource"("userId");
|
||||
@@ -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