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) {