From 0928559ea4c98b0b95754da371c6a995db580468 Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Sun, 20 Sep 2026 08:40:26 +0300 Subject: [PATCH] =?UTF-8?q?fix(catalog):=20browse=20onarmas=C4=B1nda=20yal?= =?UTF-8?q?n=C4=B1z=20bayat=20sat=C4=B1rlar=C4=B1=20sil?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bir servis karışık olabilir: bazı satırları yeni mimariyle yeniden listelenmiş, bazıları hâlâ eski. Silme servis adına göre yapıldığında zaten taşınmış satırlar da gidiyordu — üstteki insert onları `onConflictDoNothing` ile atladığı için geri gelmiyorlar, yani temelli kayıp. Silme artık yalnız gerçekten bayat olan satırlara uygulanıyor. Hangi satırların silindiğini doğrulayan test eklendi (drizzle `inArray` parametrelerini okuyarak). Co-Authored-By: Claude Opus 5 (1M context) --- .../src/catalog/catalog-browse-heal.spec.ts | 23 ++++++++++++++-- apps/api/src/catalog/catalog.service.ts | 26 +++++++++---------- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/apps/api/src/catalog/catalog-browse-heal.spec.ts b/apps/api/src/catalog/catalog-browse-heal.spec.ts index 2b5eeb7..57f1be2 100644 --- a/apps/api/src/catalog/catalog-browse-heal.spec.ts +++ b/apps/api/src/catalog/catalog-browse-heal.spec.ts @@ -50,8 +50,15 @@ function makeService(opts: { }), delete: () => ({ where: async (cond: unknown) => { - // drizzle's inArray() is opaque here; record the call itself. - deleted.push([String(cond)]); + // drizzle's inArray() puts its bound values in a `queryChunks` entry + // that is itself an array of Param objects; pull the plain ids back out + // so a test can assert WHICH rows were removed, not just how many. + const chunks = (cond as { queryChunks?: unknown[] })?.queryChunks ?? []; + const params = chunks.find((c): c is unknown[] => Array.isArray(c)) ?? []; + const ids = params + .map((param) => (param as { value?: unknown })?.value) + .filter((v): v is string => typeof v === "string"); + deleted.push(ids); return undefined; }, }), @@ -148,6 +155,18 @@ describe("healStaleBrowseRows", () => { expect(out).toEqual(refetched); }); + it("deletes only the stale rows when a service holds a mix", async () => { + // A row already re-listed under the new architecture conflicts on insert and + // is skipped, so deleting it would drop it for good. + const { svc, deleted } = makeService({ + fetched: [{ model: "208", vehicleId: "p5-208" }], + refetched: [freshRow("keep")], + }); + await svc.healStaleBrowseRows("Peugeot", [psaRow("old1"), freshRow("keep")], []); + expect(deleted).toHaveLength(1); + expect(deleted).toEqual([["old1"]]); + }); + it("keeps the stale rows when the re-list throws", async () => { const { svc, deleted, inserted } = makeService({ fetchThrows: true }); const out = await svc.healStaleBrowseRows("Volvo", [psaRow("old1")], []); diff --git a/apps/api/src/catalog/catalog.service.ts b/apps/api/src/catalog/catalog.service.ts index 8897153..35e3ebb 100644 --- a/apps/api/src/catalog/catalog.service.ts +++ b/apps/api/src/catalog/catalog.service.ts @@ -259,18 +259,12 @@ export class CatalogService { dbVehicles: (typeof catalogVehicles.$inferSelect)[], whereConditions: ReturnType[], ): Promise<(typeof catalogVehicles.$inferSelect)[] | null> { - const staleServices = [ - ...new Set( - dbVehicles - .filter((v) => - isStaleBrowseArchitecture({ - storedArchitecture: v.architecture, - currentArchitecture: PL24_SERVICE_CATALOGS[v.serviceName]?.architecture, - }), - ) - .map((v) => v.serviceName), - ), - ]; + const isStale = (v: typeof catalogVehicles.$inferSelect) => + isStaleBrowseArchitecture({ + storedArchitecture: v.architecture, + currentArchitecture: PL24_SERVICE_CATALOGS[v.serviceName]?.architecture, + }); + const staleServices = [...new Set(dbVehicles.filter(isStale).map((v) => v.serviceName))]; if (staleServices.length === 0) return null; if (!(await this.redis.setNx(`pl24:browse-heal:${brandName}`, "1", 86_400))) return null; @@ -281,7 +275,13 @@ export class CatalogService { let migrated = 0; for (const svc of staleServices) { - const staleIds = dbVehicles.filter((v) => v.serviceName === svc).map((v) => v.id); + // ONLY the stale rows. A service can hold a mix — some rows already + // re-listed under the new architecture — and those must survive: the + // insert below skips them on conflict, so deleting them here would drop + // them for good. + const staleIds = dbVehicles + .filter((v) => v.serviceName === svc && isStale(v)) + .map((v) => v.id); try { const fetched = await this.pl24Service.fetchVehicleList(svc); if (fetched.length === 0) {