From 2f2589f0b9b226377f703f7cf4b1740108812243 Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Sat, 13 Jun 2026 15:27:07 +0200 Subject: [PATCH] feat(11-01): add tag-assignment migration and validation scripts (OFFER-13/D-02) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/migrate-tags.ts: idempotently assigns the "Offerta" tag to every services row where migrated_from = 'offer_services' (D-02) - scripts/validate-tags-migration.ts: row-count + orphan checks for the tags consolidation dimension of OFFER-13 NOTE: Not yet executed against the dev/staging DB (178.104.27.55:54321) — that host is firewalled and only reachable via SSH+docker exec per project memory, which is out of scope for this agent's authorization. Execution of this script, scripts/migrate-services.ts, scripts/validate-services-migration.ts, and scripts/push-11-tags-migration.ts is documented as a pending access gate in 11-01-SUMMARY.md. --- scripts/migrate-tags.ts | 50 +++++++++++++++++++++++++++ scripts/validate-tags-migration.ts | 55 ++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 scripts/migrate-tags.ts create mode 100644 scripts/validate-tags-migration.ts diff --git a/scripts/migrate-tags.ts b/scripts/migrate-tags.ts new file mode 100644 index 0000000..bebcdfa --- /dev/null +++ b/scripts/migrate-tags.ts @@ -0,0 +1,50 @@ +import { db } from "@/db"; +import { services, tags } from "@/db/schema"; +import { eq, and } from "drizzle-orm"; + +async function migrate() { + console.log("Starting tags migration: assigning 'Offerta' tag to services migrated from offer_services...\n"); + + const offerServices = await db + .select({ id: services.id }) + .from(services) + .where(eq(services.migrated_from, "offer_services")); + + let taggedCount = 0; + let skippedCount = 0; + + for (const service of offerServices) { + const existing = await db + .select() + .from(tags) + .where( + and( + eq(tags.entity_type, "services"), + eq(tags.entity_id, service.id), + eq(tags.name, "Offerta") + ) + ) + .limit(1); + + if (existing.length > 0) { + skippedCount++; + continue; + } + + await db.insert(tags).values({ + entity_type: "services", + entity_id: service.id, + name: "Offerta", + }); + taggedCount++; + } + + console.log(`Assigned 'Offerta' tag: ${taggedCount} services, ${skippedCount} already tagged`); + console.log("\nMigration complete. Run scripts/validate-tags-migration.ts next."); + process.exit(0); +} + +migrate().catch((err) => { + console.error("Migration failed:", err); + process.exit(1); +}); diff --git a/scripts/validate-tags-migration.ts b/scripts/validate-tags-migration.ts new file mode 100644 index 0000000..3c2d582 --- /dev/null +++ b/scripts/validate-tags-migration.ts @@ -0,0 +1,55 @@ +import { db } from "@/db"; +import { services, tags } from "@/db/schema"; +import { eq, and, sql } from "drizzle-orm"; + +async function validate() { + let failures = 0; + + const [offerServicesCount] = await db + .select({ n: sql`count(*)::int` }) + .from(services) + .where(eq(services.migrated_from, "offer_services")); + + const [offerTagCount] = await db + .select({ n: sql`count(*)::int` }) + .from(tags) + .where(and(eq(tags.entity_type, "services"), eq(tags.name, "Offerta"))); + + console.log(`offer_services-derived services: ${offerServicesCount.n} | Offerta tags: ${offerTagCount.n}`); + + if (offerServicesCount.n !== offerTagCount.n) { + console.log("FAIL: Offerta tag count does not match offer_services-derived services count"); + failures++; + } else { + console.log("PASS: all offer_services-derived services have the Offerta tag"); + } + + const orphanedTags = await db.execute(sql` + SELECT COUNT(*)::int AS n FROM tags t + WHERE t.entity_type = 'services' + AND NOT EXISTS (SELECT 1 FROM services s WHERE s.id = t.entity_id) + `); + const orphanedTagsCount = (orphanedTags as unknown as Array<{ n: number }>)[0]?.n ?? 0; + if (orphanedTagsCount > 0) { + console.log(`FAIL: ${orphanedTagsCount} tags reference non-existent services`); + failures++; + } else { + console.log("PASS: no orphaned tag references"); + } + + const tagCounts = await db.execute(sql` + SELECT entity_type, COUNT(*)::int AS n FROM tags GROUP BY entity_type + `); + const counts = tagCounts as unknown as Array<{ entity_type: string; n: number }>; + for (const row of counts) { + console.log(`INFO: ${row.n} tags for entity_type=${row.entity_type}`); + } + + console.log(`\n${failures === 0 ? "ALL CHECKS PASSED" : `${failures} CHECK(S) FAILED`}`); + process.exit(failures === 0 ? 0 : 1); +} + +validate().catch((err) => { + console.error("Validation failed:", err); + process.exit(1); +});