diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7aa983240..2061c1dab 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,6 +91,9 @@ jobs: - name: Install dependencies run: yarn install --immutable + - name: Generate Prisma client + run: yarn db:generate + - name: Setup Turbo cache uses: actions/cache@v4 with: diff --git a/libs/list-types/common/src/mock-list-types.ts b/libs/list-types/common/src/mock-list-types.ts index 99f46a720..1a1def3b4 100644 --- a/libs/list-types/common/src/mock-list-types.ts +++ b/libs/list-types/common/src/mock-list-types.ts @@ -3,6 +3,7 @@ export interface ListType { name: string; englishFriendlyName: string; welshFriendlyName: string; + provenance: string; urlPath?: string; } @@ -12,6 +13,7 @@ export const mockListTypes: ListType[] = [ name: "CIVIL_DAILY_CAUSE_LIST", englishFriendlyName: "Civil Daily Cause List", welshFriendlyName: "Civil Daily Cause List", + provenance: "CFT_IDAM", urlPath: "civil-daily-cause-list" }, { @@ -19,6 +21,7 @@ export const mockListTypes: ListType[] = [ name: "FAMILY_DAILY_CAUSE_LIST", englishFriendlyName: "Family Daily Cause List", welshFriendlyName: "Family Daily Cause List", + provenance: "CFT_IDAM", urlPath: "family-daily-cause-list" }, { @@ -26,6 +29,7 @@ export const mockListTypes: ListType[] = [ name: "CRIME_DAILY_LIST", englishFriendlyName: "Crime Daily List", welshFriendlyName: "Crime Daily List", + provenance: "CFT_IDAM", urlPath: "crime-daily-list" }, { @@ -33,6 +37,7 @@ export const mockListTypes: ListType[] = [ name: "MAGISTRATES_PUBLIC_LIST", englishFriendlyName: "Magistrates Public List", welshFriendlyName: "Magistrates Public List", + provenance: "CFT_IDAM", urlPath: "magistrates-public-list" }, { @@ -40,6 +45,7 @@ export const mockListTypes: ListType[] = [ name: "CROWN_WARNED_LIST", englishFriendlyName: "Crown Warned List", welshFriendlyName: "Crown Warned List", + provenance: "CFT_IDAM", urlPath: "crown-warned-list" }, { @@ -47,6 +53,7 @@ export const mockListTypes: ListType[] = [ name: "CROWN_DAILY_LIST", englishFriendlyName: "Crown Daily List", welshFriendlyName: "Crown Daily List", + provenance: "CFT_IDAM", urlPath: "crown-daily-list" }, { @@ -54,6 +61,7 @@ export const mockListTypes: ListType[] = [ name: "CROWN_FIRM_LIST", englishFriendlyName: "Crown Firm List", welshFriendlyName: "Crown Firm List", + provenance: "CFT_IDAM", urlPath: "crown-firm-list" }, { @@ -61,6 +69,7 @@ export const mockListTypes: ListType[] = [ name: "CIVIL_AND_FAMILY_DAILY_CAUSE_LIST", englishFriendlyName: "Civil and Family Daily Cause List", welshFriendlyName: "Rhestr Achos Dyddiol Sifil a Theulu", + provenance: "CFT_IDAM", urlPath: "civil-and-family-daily-cause-list" } ]; diff --git a/libs/location/src/seed-data.test.ts b/libs/location/src/seed-data.test.ts new file mode 100644 index 000000000..61ece230b --- /dev/null +++ b/libs/location/src/seed-data.test.ts @@ -0,0 +1,371 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +// Mock the prisma client +const mockPrisma = { + region: { + count: vi.fn(), + upsert: vi.fn() + }, + jurisdiction: { + count: vi.fn(), + upsert: vi.fn() + }, + subJurisdiction: { + upsert: vi.fn() + }, + location: { + count: vi.fn(), + upsert: vi.fn() + }, + locationRegion: { + deleteMany: vi.fn(), + createMany: vi.fn() + }, + locationSubJurisdiction: { + deleteMany: vi.fn(), + createMany: vi.fn() + } +}; + +vi.mock("@hmcts/postgres", () => ({ + prisma: mockPrisma +})); + +// Mock location data +const mockLocationData = { + regions: [ + { regionId: 1, name: "Test Region 1", welshName: "Rhanbarth Prawf 1" }, + { regionId: 2, name: "Test Region 2", welshName: "Rhanbarth Prawf 2" } + ], + jurisdictions: [ + { jurisdictionId: 1, name: "Test Jurisdiction 1", welshName: "Awdurdodaeth Prawf 1" }, + { jurisdictionId: 2, name: "Test Jurisdiction 2", welshName: "Awdurdodaeth Prawf 2" } + ], + subJurisdictions: [ + { + subJurisdictionId: 1, + name: "Test Sub-Jurisdiction 1", + welshName: "Is-awdurdodaeth Prawf 1", + jurisdictionId: 1 + }, + { + subJurisdictionId: 2, + name: "Test Sub-Jurisdiction 2", + welshName: "Is-awdurdodaeth Prawf 2", + jurisdictionId: 2 + } + ], + locations: [ + { + locationId: 1, + name: "Test Location 1", + welshName: "Lleoliad Prawf 1", + regions: [1], + subJurisdictions: [1] + }, + { + locationId: 2, + name: "Test Location 2", + welshName: "Lleoliad Prawf 2", + regions: [2], + subJurisdictions: [2] + }, + { + locationId: 3, + name: "Test Location 3", + welshName: "Lleoliad Prawf 3", + regions: [], + subJurisdictions: [] + } + ] +}; + +vi.mock("./location-data.js", () => ({ + locationData: mockLocationData +})); + +describe("seed-data", () => { + let originalEnv: NodeJS.ProcessEnv; + let consoleLogSpy: ReturnType; + + beforeEach(() => { + originalEnv = { ...process.env }; + consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + vi.clearAllMocks(); + }); + + afterEach(() => { + process.env = originalEnv; + consoleLogSpy.mockRestore(); + }); + + describe("shouldSeed", () => { + it("should return false when NODE_ENV is production", async () => { + process.env.NODE_ENV = "production"; + const { seedLocationData } = await import("./seed-data.js"); + + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Checking if location data seeding is needed..."); + expect(consoleLogSpy).toHaveBeenCalledWith("Skipping seed: NODE_ENV is production"); + expect(mockPrisma.region.count).not.toHaveBeenCalled(); + }); + + it("should return false when CI is true", async () => { + process.env.NODE_ENV = "development"; + process.env.CI = "true"; + const { seedLocationData } = await import("./seed-data.js"); + + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Checking if location data seeding is needed..."); + expect(consoleLogSpy).toHaveBeenCalledWith("Skipping seed: Running in CI environment"); + expect(mockPrisma.region.count).not.toHaveBeenCalled(); + }); + + it("should return false when tables already contain data (region not empty)", async () => { + process.env.NODE_ENV = "development"; + process.env.CI = "false"; + mockPrisma.region.count.mockResolvedValue(1); + mockPrisma.jurisdiction.count.mockResolvedValue(0); + mockPrisma.location.count.mockResolvedValue(0); + + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Checking if location data seeding is needed..."); + expect(consoleLogSpy).toHaveBeenCalledWith("Skipping seed: Tables already contain data"); + expect(mockPrisma.region.upsert).not.toHaveBeenCalled(); + }); + + it("should return false when tables already contain data (jurisdiction not empty)", async () => { + process.env.NODE_ENV = "development"; + process.env.CI = "false"; + mockPrisma.region.count.mockResolvedValue(0); + mockPrisma.jurisdiction.count.mockResolvedValue(1); + mockPrisma.location.count.mockResolvedValue(0); + + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Skipping seed: Tables already contain data"); + expect(mockPrisma.region.upsert).not.toHaveBeenCalled(); + }); + + it("should return false when tables already contain data (location not empty)", async () => { + process.env.NODE_ENV = "development"; + process.env.CI = "false"; + mockPrisma.region.count.mockResolvedValue(0); + mockPrisma.jurisdiction.count.mockResolvedValue(0); + mockPrisma.location.count.mockResolvedValue(1); + + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Skipping seed: Tables already contain data"); + expect(mockPrisma.region.upsert).not.toHaveBeenCalled(); + }); + + it("should return true when all tables are empty", async () => { + process.env.NODE_ENV = "development"; + process.env.CI = "false"; + mockPrisma.region.count.mockResolvedValue(0); + mockPrisma.jurisdiction.count.mockResolvedValue(0); + mockPrisma.location.count.mockResolvedValue(0); + mockPrisma.region.upsert.mockResolvedValue({}); + mockPrisma.jurisdiction.upsert.mockResolvedValue({}); + mockPrisma.subJurisdiction.upsert.mockResolvedValue({}); + mockPrisma.location.upsert.mockResolvedValue({}); + mockPrisma.locationRegion.deleteMany.mockResolvedValue({ count: 0 }); + mockPrisma.locationSubJurisdiction.deleteMany.mockResolvedValue({ count: 0 }); + mockPrisma.locationRegion.createMany.mockResolvedValue({ count: 1 }); + mockPrisma.locationSubJurisdiction.createMany.mockResolvedValue({ count: 1 }); + + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Seeding location reference data..."); + expect(mockPrisma.region.upsert).toHaveBeenCalled(); + }); + }); + + describe("seedLocationData", () => { + beforeEach(() => { + process.env.NODE_ENV = "development"; + process.env.CI = "false"; + mockPrisma.region.count.mockResolvedValue(0); + mockPrisma.jurisdiction.count.mockResolvedValue(0); + mockPrisma.location.count.mockResolvedValue(0); + mockPrisma.region.upsert.mockResolvedValue({}); + mockPrisma.jurisdiction.upsert.mockResolvedValue({}); + mockPrisma.subJurisdiction.upsert.mockResolvedValue({}); + mockPrisma.location.upsert.mockResolvedValue({}); + mockPrisma.locationRegion.deleteMany.mockResolvedValue({ count: 0 }); + mockPrisma.locationSubJurisdiction.deleteMany.mockResolvedValue({ count: 0 }); + mockPrisma.locationRegion.createMany.mockResolvedValue({ count: 1 }); + mockPrisma.locationSubJurisdiction.createMany.mockResolvedValue({ count: 1 }); + }); + + it("should seed regions correctly", async () => { + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Seeding regions..."); + expect(mockPrisma.region.upsert).toHaveBeenCalledTimes(mockLocationData.regions.length); + expect(mockPrisma.region.upsert).toHaveBeenCalledWith({ + where: { regionId: 1 }, + create: { + regionId: 1, + name: "Test Region 1", + welshName: "Rhanbarth Prawf 1" + }, + update: { + name: "Test Region 1", + welshName: "Rhanbarth Prawf 1" + } + }); + expect(consoleLogSpy).toHaveBeenCalledWith(`Seeded ${mockLocationData.regions.length} regions`); + }); + + it("should seed jurisdictions correctly", async () => { + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Seeding jurisdictions..."); + expect(mockPrisma.jurisdiction.upsert).toHaveBeenCalledTimes(mockLocationData.jurisdictions.length); + expect(mockPrisma.jurisdiction.upsert).toHaveBeenCalledWith({ + where: { jurisdictionId: 1 }, + create: { + jurisdictionId: 1, + name: "Test Jurisdiction 1", + welshName: "Awdurdodaeth Prawf 1" + }, + update: { + name: "Test Jurisdiction 1", + welshName: "Awdurdodaeth Prawf 1" + } + }); + expect(consoleLogSpy).toHaveBeenCalledWith(`Seeded ${mockLocationData.jurisdictions.length} jurisdictions`); + }); + + it("should seed sub-jurisdictions correctly", async () => { + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Seeding sub-jurisdictions..."); + expect(mockPrisma.subJurisdiction.upsert).toHaveBeenCalledTimes(mockLocationData.subJurisdictions.length); + expect(mockPrisma.subJurisdiction.upsert).toHaveBeenCalledWith({ + where: { subJurisdictionId: 1 }, + create: { + subJurisdictionId: 1, + name: "Test Sub-Jurisdiction 1", + welshName: "Is-awdurdodaeth Prawf 1", + jurisdictionId: 1 + }, + update: { + name: "Test Sub-Jurisdiction 1", + welshName: "Is-awdurdodaeth Prawf 1", + jurisdictionId: 1 + } + }); + expect(consoleLogSpy).toHaveBeenCalledWith(`Seeded ${mockLocationData.subJurisdictions.length} sub-jurisdictions`); + }); + + it("should seed locations correctly", async () => { + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Seeding locations..."); + expect(mockPrisma.location.upsert).toHaveBeenCalledTimes(mockLocationData.locations.length); + expect(mockPrisma.location.upsert).toHaveBeenCalledWith({ + where: { locationId: 1 }, + create: { + locationId: 1, + name: "Test Location 1", + welshName: "Lleoliad Prawf 1", + email: null, + contactNo: null + }, + update: { + name: "Test Location 1", + welshName: "Lleoliad Prawf 1" + } + }); + expect(consoleLogSpy).toHaveBeenCalledWith(`Seeded ${mockLocationData.locations.length} locations`); + }); + + it("should delete and create location region junction records", async () => { + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + // Should delete existing records for each location + expect(mockPrisma.locationRegion.deleteMany).toHaveBeenCalledTimes(mockLocationData.locations.length); + expect(mockPrisma.locationRegion.deleteMany).toHaveBeenCalledWith({ + where: { locationId: 1 } + }); + + // Should create new records only for locations with regions + const locationsWithRegions = mockLocationData.locations.filter((l) => l.regions.length > 0); + expect(mockPrisma.locationRegion.createMany).toHaveBeenCalledTimes(locationsWithRegions.length); + expect(mockPrisma.locationRegion.createMany).toHaveBeenCalledWith({ + data: [{ locationId: 1, regionId: 1 }] + }); + }); + + it("should delete and create location sub-jurisdiction junction records", async () => { + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + // Should delete existing records for each location + expect(mockPrisma.locationSubJurisdiction.deleteMany).toHaveBeenCalledTimes(mockLocationData.locations.length); + expect(mockPrisma.locationSubJurisdiction.deleteMany).toHaveBeenCalledWith({ + where: { locationId: 1 } + }); + + // Should create new records only for locations with sub-jurisdictions + const locationsWithSubJurisdictions = mockLocationData.locations.filter((l) => l.subJurisdictions.length > 0); + expect(mockPrisma.locationSubJurisdiction.createMany).toHaveBeenCalledTimes(locationsWithSubJurisdictions.length); + expect(mockPrisma.locationSubJurisdiction.createMany).toHaveBeenCalledWith({ + data: [{ locationId: 1, subJurisdictionId: 1 }] + }); + }); + + it("should not create junction records for locations with empty arrays", async () => { + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + // Verify deleteMany was called for location 3 (has empty arrays) + expect(mockPrisma.locationRegion.deleteMany).toHaveBeenCalledWith({ + where: { locationId: 3 } + }); + expect(mockPrisma.locationSubJurisdiction.deleteMany).toHaveBeenCalledWith({ + where: { locationId: 3 } + }); + + // But createMany should only be called 2 times (for locations 1 and 2 that have data) + expect(mockPrisma.locationRegion.createMany).toHaveBeenCalledTimes(2); + expect(mockPrisma.locationSubJurisdiction.createMany).toHaveBeenCalledTimes(2); + }); + + it("should log completion message", async () => { + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + expect(consoleLogSpy).toHaveBeenCalledWith("Location reference data seeding completed successfully"); + }); + + it("should complete full seeding workflow in correct order", async () => { + const { seedLocationData } = await import("./seed-data.js"); + await seedLocationData(); + + const calls = consoleLogSpy.mock.calls.map((call) => call[0]); + + expect(calls.indexOf("Checking if location data seeding is needed...")).toBeLessThan(calls.indexOf("Seeding location reference data...")); + expect(calls.indexOf("Seeding location reference data...")).toBeLessThan(calls.indexOf("Seeding regions...")); + expect(calls.indexOf("Seeding regions...")).toBeLessThan(calls.indexOf("Seeding jurisdictions...")); + expect(calls.indexOf("Seeding jurisdictions...")).toBeLessThan(calls.indexOf("Seeding sub-jurisdictions...")); + expect(calls.indexOf("Seeding sub-jurisdictions...")).toBeLessThan(calls.indexOf("Seeding locations...")); + expect(calls.indexOf("Seeding locations...")).toBeLessThan(calls.indexOf("Location reference data seeding completed successfully")); + }); + }); +}); diff --git a/libs/location/src/seed-data.ts b/libs/location/src/seed-data.ts index 0b7945ad4..0e0a573d0 100644 --- a/libs/location/src/seed-data.ts +++ b/libs/location/src/seed-data.ts @@ -1,7 +1,42 @@ import { prisma } from "@hmcts/postgres"; import { locationData } from "./location-data.js"; +async function shouldSeed(): Promise { + // Only seed in local development, not in CI or production + if (process.env.NODE_ENV === "production") { + console.log("Skipping seed: NODE_ENV is production"); + return false; + } + + if (process.env.CI === "true") { + console.log("Skipping seed: Running in CI environment"); + return false; + } + + // Check if tables are empty + const regionCount = await prisma.region.count(); + const jurisdictionCount = await prisma.jurisdiction.count(); + const locationCount = await prisma.location.count(); + + // Only seed if all tables are empty + const isEmpty = regionCount === 0 && jurisdictionCount === 0 && locationCount === 0; + + if (!isEmpty) { + console.log("Skipping seed: Tables already contain data"); + return false; + } + + return true; +} + export async function seedLocationData() { + console.log("Checking if location data seeding is needed..."); + + const needsSeeding = await shouldSeed(); + if (!needsSeeding) { + return; + } + console.log("Seeding location reference data..."); // Seed regions diff --git a/libs/system-admin-pages/src/reference-data-upload/services/download-service.ts b/libs/system-admin-pages/src/reference-data-upload/services/download-service.ts index c5b8f8e60..9e2de5eb0 100644 --- a/libs/system-admin-pages/src/reference-data-upload/services/download-service.ts +++ b/libs/system-admin-pages/src/reference-data-upload/services/download-service.ts @@ -1,5 +1,5 @@ import { prisma } from "@hmcts/postgres"; -import * as Papa from "papaparse"; +import Papa from "papaparse"; export async function generateReferenceDataCsv(): Promise { try { diff --git a/package.json b/package.json index 425af903e..1fabbb059 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,10 @@ "dev:api": "yarn workspace @hmcts/api run dev", "dev:web": "yarn workspace @hmcts/web run dev", "dev:web:nowatch": "yarn workspace @hmcts/web run dev:nowatch", - "dev:up": "docker compose up -d && yarn db:migrate:dev", + "dev:up": "docker compose up -d && yarn db:migrate:dev && yarn db:seed", "dev:down": "docker compose down", "db:migrate": "yarn workspace @hmcts/postgres run migrate", + "db:seed": "tsx libs/location/src/seed-data.ts", "db:migrate:dev": "yarn workspace @hmcts/postgres run migrate:dev", "db:generate": "yarn workspace @hmcts/postgres run generate", "db:studio": "yarn workspace @hmcts/postgres run studio", @@ -62,7 +63,8 @@ "resolutions": { "vite": "7.2.4", "glob": "13.0.0", - "body-parser": "2.2.1" + "body-parser": "2.2.1", + "node-forge": "1.3.2" }, "dependencies": { "@microsoft/microsoft-graph-client": "3.0.7", diff --git a/yarn.lock b/yarn.lock index 7b088b238..a52dd8546 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5372,10 +5372,10 @@ __metadata: languageName: node linkType: hard -"node-forge@npm:^1.2.1": - version: 1.3.1 - resolution: "node-forge@npm:1.3.1" - checksum: 10c0/e882819b251a4321f9fc1d67c85d1501d3004b4ee889af822fd07f64de3d1a8e272ff00b689570af0465d65d6bf5074df9c76e900e0aff23e60b847f2a46fbe8 +"node-forge@npm:1.3.2": + version: 1.3.2 + resolution: "node-forge@npm:1.3.2" + checksum: 10c0/1def35652c93a588718a6d0d0b4f33e3e7de283aa6f4c00d01d1605d6ccce23fb3b59bcbfb6434014acd23a251cfcc2736052b406f53d94e1b19c09d289d0176 languageName: node linkType: hard