diff --git a/package.json b/package.json index f20a6486..2571399d 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "dev": "concurrently \"npm run dev-board\" \"npm run dev-workers\"", "import": "node --import=tsx scripts/import-spreadsheet-companies.ts", "update-report-url": "node --import=tsx scripts/update-spreadsheet-companies-urls.ts", - "clean-migrate": "node --import tsx --env-file .env scripts/migrateDataToNewSchema.ts", + "clean-migrate": "node --import tsx scripts/migrateDataToNewSchema.ts", "test": "jest", "prisma": "prisma", "reset": "node --import tsx scripts/dev-reset.ts" diff --git a/prisma/migrations/20250113142336_remove_old_structure/migration.sql b/prisma/migrations/20250113142336_remove_old_structure/migration.sql new file mode 100644 index 00000000..7c1b683f --- /dev/null +++ b/prisma/migrations/20250113142336_remove_old_structure/migration.sql @@ -0,0 +1,186 @@ +/* + Warnings: + + - You are about to drop the `BaseYear` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `BiogenicEmissions` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Company` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Economy` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Emissions` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Employees` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Goal` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Industry` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `IndustryGics` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Initiative` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Metadata` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `ReportingPeriod` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Scope1` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Scope1And2` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Scope2` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Scope3` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Scope3Category` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `StatedTotalEmissions` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Turnover` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "BaseYear" DROP CONSTRAINT "BaseYear_companyId_fkey"; + +-- DropForeignKey +ALTER TABLE "BaseYear" DROP CONSTRAINT "BaseYear_metadataId_fkey"; + +-- DropForeignKey +ALTER TABLE "BiogenicEmissions" DROP CONSTRAINT "BiogenicEmissions_metadataId_fkey"; + +-- DropForeignKey +ALTER TABLE "Economy" DROP CONSTRAINT "Economy_employeesId_fkey"; + +-- DropForeignKey +ALTER TABLE "Economy" DROP CONSTRAINT "Economy_turnoverId_fkey"; + +-- DropForeignKey +ALTER TABLE "Emissions" DROP CONSTRAINT "Emissions_biogenicEmissionsId_fkey"; + +-- DropForeignKey +ALTER TABLE "Emissions" DROP CONSTRAINT "Emissions_scope1And2Id_fkey"; + +-- DropForeignKey +ALTER TABLE "Emissions" DROP CONSTRAINT "Emissions_scope1Id_fkey"; + +-- DropForeignKey +ALTER TABLE "Emissions" DROP CONSTRAINT "Emissions_scope2Id_fkey"; + +-- DropForeignKey +ALTER TABLE "Emissions" DROP CONSTRAINT "Emissions_scope3Id_fkey"; + +-- DropForeignKey +ALTER TABLE "Emissions" DROP CONSTRAINT "Emissions_statedTotalEmissionsId_fkey"; + +-- DropForeignKey +ALTER TABLE "Employees" DROP CONSTRAINT "Employees_metadataId_fkey"; + +-- DropForeignKey +ALTER TABLE "Goal" DROP CONSTRAINT "Goal_companyId_fkey"; + +-- DropForeignKey +ALTER TABLE "Goal" DROP CONSTRAINT "Goal_metadataId_fkey"; + +-- DropForeignKey +ALTER TABLE "Industry" DROP CONSTRAINT "Industry_companyWikidataId_fkey"; + +-- DropForeignKey +ALTER TABLE "Industry" DROP CONSTRAINT "Industry_gicsSubIndustryCode_fkey"; + +-- DropForeignKey +ALTER TABLE "Industry" DROP CONSTRAINT "Industry_metadataId_fkey"; + +-- DropForeignKey +ALTER TABLE "Initiative" DROP CONSTRAINT "Initiative_companyId_fkey"; + +-- DropForeignKey +ALTER TABLE "Initiative" DROP CONSTRAINT "Initiative_metadataId_fkey"; + +-- DropForeignKey +ALTER TABLE "Metadata" DROP CONSTRAINT "Metadata_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "Metadata" DROP CONSTRAINT "Metadata_verifiedByUserId_fkey"; + +-- DropForeignKey +ALTER TABLE "ReportingPeriod" DROP CONSTRAINT "ReportingPeriod_companyId_fkey"; + +-- DropForeignKey +ALTER TABLE "ReportingPeriod" DROP CONSTRAINT "ReportingPeriod_economyId_fkey"; + +-- DropForeignKey +ALTER TABLE "ReportingPeriod" DROP CONSTRAINT "ReportingPeriod_emissionsId_fkey"; + +-- DropForeignKey +ALTER TABLE "ReportingPeriod" DROP CONSTRAINT "ReportingPeriod_metadataId_fkey"; + +-- DropForeignKey +ALTER TABLE "Scope1" DROP CONSTRAINT "Scope1_metadataId_fkey"; + +-- DropForeignKey +ALTER TABLE "Scope1And2" DROP CONSTRAINT "Scope1And2_metadataId_fkey"; + +-- DropForeignKey +ALTER TABLE "Scope2" DROP CONSTRAINT "Scope2_metadataId_fkey"; + +-- DropForeignKey +ALTER TABLE "Scope3" DROP CONSTRAINT "Scope3_metadataId_fkey"; + +-- DropForeignKey +ALTER TABLE "Scope3Category" DROP CONSTRAINT "Scope3Category_metadataId_fkey"; + +-- DropForeignKey +ALTER TABLE "Scope3Category" DROP CONSTRAINT "Scope3Category_scope3Id_fkey"; + +-- DropForeignKey +ALTER TABLE "StatedTotalEmissions" DROP CONSTRAINT "StatedTotalEmissions_metadataId_fkey"; + +-- DropForeignKey +ALTER TABLE "StatedTotalEmissions" DROP CONSTRAINT "StatedTotalEmissions_scope3Id_fkey"; + +-- DropForeignKey +ALTER TABLE "Turnover" DROP CONSTRAINT "Turnover_metadataId_fkey"; + +-- DropTable +DROP TABLE "BaseYear"; + +-- DropTable +DROP TABLE "BiogenicEmissions"; + +-- DropTable +DROP TABLE "Company"; + +-- DropTable +DROP TABLE "Economy"; + +-- DropTable +DROP TABLE "Emissions"; + +-- DropTable +DROP TABLE "Employees"; + +-- DropTable +DROP TABLE "Goal"; + +-- DropTable +DROP TABLE "Industry"; + +-- DropTable +DROP TABLE "IndustryGics"; + +-- DropTable +DROP TABLE "Initiative"; + +-- DropTable +DROP TABLE "Metadata"; + +-- DropTable +DROP TABLE "ReportingPeriod"; + +-- DropTable +DROP TABLE "Scope1"; + +-- DropTable +DROP TABLE "Scope1And2"; + +-- DropTable +DROP TABLE "Scope2"; + +-- DropTable +DROP TABLE "Scope3"; + +-- DropTable +DROP TABLE "Scope3Category"; + +-- DropTable +DROP TABLE "StatedTotalEmissions"; + +-- DropTable +DROP TABLE "Turnover"; + +-- DropTable +DROP TABLE "User"; diff --git a/prisma/migrations/20250113153942_update_name/migration.sql b/prisma/migrations/20250113153942_update_name/migration.sql new file mode 100644 index 00000000..8ec6cf89 --- /dev/null +++ b/prisma/migrations/20250113153942_update_name/migration.sql @@ -0,0 +1,373 @@ +-- CreateTable +CREATE TABLE "Company" ( + "wikidataId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "url" TEXT, + "internalComment" TEXT, + "tags" TEXT[], + + CONSTRAINT "Company_pkey" PRIMARY KEY ("wikidataId") +); + +-- CreateTable +CREATE TABLE "BaseYear" ( + "id" SERIAL NOT NULL, + "year" INTEGER NOT NULL, + "scope" INTEGER NOT NULL, + "companyId" TEXT NOT NULL, + + CONSTRAINT "BaseYear_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Industry" ( + "id" SERIAL NOT NULL, + "gicsSubIndustryCode" TEXT NOT NULL, + "companyWikidataId" TEXT NOT NULL, + + CONSTRAINT "Industry_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "IndustryGics" ( + "subIndustryCode" TEXT NOT NULL, + "subIndustryName" TEXT NOT NULL, + "subIndustryDescription" TEXT NOT NULL, + "sectorCode" TEXT NOT NULL, + "sectorName" TEXT NOT NULL, + "groupCode" TEXT NOT NULL, + "groupName" TEXT NOT NULL, + "industryCode" TEXT NOT NULL, + "industryName" TEXT NOT NULL, + + CONSTRAINT "IndustryGics_pkey" PRIMARY KEY ("subIndustryCode") +); + +-- CreateTable +CREATE TABLE "ReportingPeriod" ( + "id" SERIAL NOT NULL, + "startDate" TIMESTAMP(3) NOT NULL, + "endDate" TIMESTAMP(3) NOT NULL, + "year" TEXT NOT NULL, + "reportURL" TEXT, + "companyId" TEXT NOT NULL, + + CONSTRAINT "ReportingPeriod_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Emissions" ( + "id" SERIAL NOT NULL, + "reportingPeriodId" INTEGER, + + CONSTRAINT "Emissions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "StatedTotalEmissions" ( + "id" SERIAL NOT NULL, + "total" DOUBLE PRECISION, + "scope3Id" INTEGER, + "unit" TEXT NOT NULL, + "emissionsId" INTEGER, + + CONSTRAINT "StatedTotalEmissions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Scope1And2" ( + "id" SERIAL NOT NULL, + "total" DOUBLE PRECISION, + "emissionsId" INTEGER, + "unit" TEXT NOT NULL, + + CONSTRAINT "Scope1And2_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "BiogenicEmissions" ( + "id" SERIAL NOT NULL, + "total" DOUBLE PRECISION, + "unit" TEXT NOT NULL, + "emissionsId" INTEGER, + + CONSTRAINT "BiogenicEmissions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Scope1" ( + "id" SERIAL NOT NULL, + "total" DOUBLE PRECISION, + "unit" TEXT NOT NULL, + "emissionsId" INTEGER, + + CONSTRAINT "Scope1_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Scope2" ( + "id" SERIAL NOT NULL, + "mb" DOUBLE PRECISION, + "lb" DOUBLE PRECISION, + "unknown" DOUBLE PRECISION, + "unit" TEXT NOT NULL, + "emissionsId" INTEGER, + + CONSTRAINT "Scope2_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Scope3" ( + "id" SERIAL NOT NULL, + "emissionsId" INTEGER, + "statedTotalEmissionsId" INTEGER, + + CONSTRAINT "Scope3_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Scope3Category" ( + "id" SERIAL NOT NULL, + "category" INTEGER NOT NULL, + "total" DOUBLE PRECISION, + "scope3Id" INTEGER NOT NULL, + "unit" TEXT NOT NULL, + + CONSTRAINT "Scope3Category_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Economy" ( + "id" SERIAL NOT NULL, + "reportingPeriodId" INTEGER, + "turnoverId" INTEGER, + + CONSTRAINT "Economy_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Turnover" ( + "id" SERIAL NOT NULL, + "value" DOUBLE PRECISION, + "currency" TEXT, + "economyId" INTEGER NOT NULL, + + CONSTRAINT "Turnover_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Employees" ( + "id" SERIAL NOT NULL, + "value" DOUBLE PRECISION, + "unit" TEXT, + "economyId" INTEGER, + + CONSTRAINT "Employees_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Goal" ( + "id" SERIAL NOT NULL, + "description" TEXT NOT NULL, + "year" TEXT, + "target" DOUBLE PRECISION, + "baseYear" TEXT, + "companyId" TEXT NOT NULL, + + CONSTRAINT "Goal_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Initiative" ( + "id" SERIAL NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "year" TEXT, + "scope" TEXT, + "companyId" TEXT NOT NULL, + + CONSTRAINT "Initiative_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Metadata" ( + "id" SERIAL NOT NULL, + "comment" TEXT, + "source" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" INTEGER NOT NULL, + "verifiedByUserId" INTEGER, + "goalId" INTEGER, + "initiativeId" INTEGER, + "scope1Id" INTEGER, + "scope2Id" INTEGER, + "scope3Id" INTEGER, + "scope1And2Id" INTEGER, + "reportingPeriodId" INTEGER, + "baseYearId" INTEGER, + "biogenicEmissionsId" INTEGER, + "statedTotalEmissionsId" INTEGER, + "industryId" INTEGER, + "categoryId" INTEGER, + "turnoverId" INTEGER, + "employeesId" INTEGER, + + CONSTRAINT "Metadata_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Industry_companyWikidataId_key" ON "Industry"("companyWikidataId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ReportingPeriod_companyId_year_key" ON "ReportingPeriod"("companyId", "year"); + +-- CreateIndex +CREATE UNIQUE INDEX "Emissions_reportingPeriodId_key" ON "Emissions"("reportingPeriodId"); + +-- CreateIndex +CREATE UNIQUE INDEX "StatedTotalEmissions_scope3Id_key" ON "StatedTotalEmissions"("scope3Id"); + +-- CreateIndex +CREATE UNIQUE INDEX "StatedTotalEmissions_emissionsId_key" ON "StatedTotalEmissions"("emissionsId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Scope1And2_emissionsId_key" ON "Scope1And2"("emissionsId"); + +-- CreateIndex +CREATE UNIQUE INDEX "BiogenicEmissions_emissionsId_key" ON "BiogenicEmissions"("emissionsId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Scope1_emissionsId_key" ON "Scope1"("emissionsId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Scope2_emissionsId_key" ON "Scope2"("emissionsId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Scope3_emissionsId_key" ON "Scope3"("emissionsId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Scope3_statedTotalEmissionsId_key" ON "Scope3"("statedTotalEmissionsId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Economy_reportingPeriodId_key" ON "Economy"("reportingPeriodId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Turnover_economyId_key" ON "Turnover"("economyId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Employees_economyId_key" ON "Employees"("economyId"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- AddForeignKey +ALTER TABLE "BaseYear" ADD CONSTRAINT "BaseYear_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("wikidataId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Industry" ADD CONSTRAINT "Industry_companyWikidataId_fkey" FOREIGN KEY ("companyWikidataId") REFERENCES "Company"("wikidataId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Industry" ADD CONSTRAINT "Industry_gicsSubIndustryCode_fkey" FOREIGN KEY ("gicsSubIndustryCode") REFERENCES "IndustryGics"("subIndustryCode") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ReportingPeriod" ADD CONSTRAINT "ReportingPeriod_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("wikidataId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Emissions" ADD CONSTRAINT "Emissions_reportingPeriodId_fkey" FOREIGN KEY ("reportingPeriodId") REFERENCES "ReportingPeriod"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StatedTotalEmissions" ADD CONSTRAINT "StatedTotalEmissions_emissionsId_fkey" FOREIGN KEY ("emissionsId") REFERENCES "Emissions"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StatedTotalEmissions" ADD CONSTRAINT "StatedTotalEmissions_scope3Id_fkey" FOREIGN KEY ("scope3Id") REFERENCES "Scope3"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Scope1And2" ADD CONSTRAINT "Scope1And2_emissionsId_fkey" FOREIGN KEY ("emissionsId") REFERENCES "Emissions"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BiogenicEmissions" ADD CONSTRAINT "BiogenicEmissions_emissionsId_fkey" FOREIGN KEY ("emissionsId") REFERENCES "Emissions"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Scope1" ADD CONSTRAINT "Scope1_emissionsId_fkey" FOREIGN KEY ("emissionsId") REFERENCES "Emissions"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Scope2" ADD CONSTRAINT "Scope2_emissionsId_fkey" FOREIGN KEY ("emissionsId") REFERENCES "Emissions"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Scope3" ADD CONSTRAINT "Scope3_emissionsId_fkey" FOREIGN KEY ("emissionsId") REFERENCES "Emissions"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Scope3Category" ADD CONSTRAINT "Scope3Category_scope3Id_fkey" FOREIGN KEY ("scope3Id") REFERENCES "Scope3"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Economy" ADD CONSTRAINT "Economy_reportingPeriodId_fkey" FOREIGN KEY ("reportingPeriodId") REFERENCES "ReportingPeriod"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Turnover" ADD CONSTRAINT "Turnover_economyId_fkey" FOREIGN KEY ("economyId") REFERENCES "Economy"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Employees" ADD CONSTRAINT "Employees_economyId_fkey" FOREIGN KEY ("economyId") REFERENCES "Economy"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Goal" ADD CONSTRAINT "Goal_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("wikidataId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Initiative" ADD CONSTRAINT "Initiative_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("wikidataId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Metadata" ADD CONSTRAINT "Metadata_goalId_fkey" FOREIGN KEY ("goalId") REFERENCES "Goal"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Metadata" ADD CONSTRAINT "Metadata_initiativeId_fkey" FOREIGN KEY ("initiativeId") REFERENCES "Initiative"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Metadata" ADD CONSTRAINT "Metadata_scope1Id_fkey" FOREIGN KEY ("scope1Id") REFERENCES "Scope1"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Metadata" ADD CONSTRAINT "Metadata_scope2Id_fkey" FOREIGN KEY ("scope2Id") REFERENCES "Scope2"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Metadata" ADD CONSTRAINT "Metadata_scope3Id_fkey" FOREIGN KEY ("scope3Id") REFERENCES "Scope3"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Metadata" ADD CONSTRAINT "Metadata_scope1And2Id_fkey" FOREIGN KEY ("scope1And2Id") REFERENCES "Scope1And2"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Metadata" ADD CONSTRAINT "Metadata_reportingPeriodId_fkey" FOREIGN KEY ("reportingPeriodId") REFERENCES "ReportingPeriod"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Metadata" ADD CONSTRAINT "Metadata_baseYearId_fkey" FOREIGN KEY ("baseYearId") REFERENCES "BaseYear"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Metadata" ADD CONSTRAINT "Metadata_biogenicEmissionsId_fkey" FOREIGN KEY ("biogenicEmissionsId") REFERENCES "BiogenicEmissions"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Metadata" ADD CONSTRAINT "Metadata_statedTotalEmissionsId_fkey" FOREIGN KEY ("statedTotalEmissionsId") REFERENCES "StatedTotalEmissions"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Metadata" ADD CONSTRAINT "Metadata_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Metadata" ADD CONSTRAINT "Metadata_verifiedByUserId_fkey" FOREIGN KEY ("verifiedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Metadata" ADD CONSTRAINT "Metadata_industryId_fkey" FOREIGN KEY ("industryId") REFERENCES "Industry"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Metadata" ADD CONSTRAINT "Metadata_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Scope3Category"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Metadata" ADD CONSTRAINT "Metadata_turnoverId_fkey" FOREIGN KEY ("turnoverId") REFERENCES "Turnover"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Metadata" ADD CONSTRAINT "Metadata_employeesId_fkey" FOREIGN KEY ("employeesId") REFERENCES "Employees"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20250113160200_move_data/migration.sql b/prisma/migrations/20250113160200_move_data/migration.sql new file mode 100644 index 00000000..8289534c --- /dev/null +++ b/prisma/migrations/20250113160200_move_data/migration.sql @@ -0,0 +1,22 @@ +-- Move all data to the new structure + +INSERT INTO "Company" SELECT * FROM "Company2"; +INSERT INTO "BaseYear" SELECT * FROM "BaseYear2"; +INSERT INTO "IndustryGics" SELECT * FROM "IndustryGics2"; +INSERT INTO "Industry" SELECT * FROM "Industry2"; +INSERT INTO "ReportingPeriod" SELECT * FROM "ReportingPeriod2"; +INSERT INTO "Emissions" SELECT * FROM "Emissions2"; +INSERT INTO "Scope1" SELECT * FROM "Scope12"; +INSERT INTO "Scope1And2" SELECT * FROM "Scope1And22"; +INSERT INTO "Scope2" SELECT * FROM "Scope22"; +INSERT INTO "Scope3" SELECT * FROM "Scope32"; +INSERT INTO "Scope3Category" SELECT * FROM "Scope3Category2"; +INSERT INTO "StatedTotalEmissions" SELECT * FROM "StatedTotalEmissions2"; +INSERT INTO "BiogenicEmissions" SELECT * FROM "BiogenicEmissions2"; +INSERT INTO "Economy" SELECT * FROM "Economy2"; +INSERT INTO "Turnover" SELECT * FROM "Turnover2"; +INSERT INTO "Employees" SELECT * FROM "Employees2"; +INSERT INTO "Goal" SELECT * FROM "Goal2"; +INSERT INTO "Initiative" SELECT * FROM "Initiative2"; +INSERT INTO "User" SELECT * FROM "User2"; +INSERT INTO "Metadata" SELECT * FROM "Metadata2"; \ No newline at end of file diff --git a/prisma/migrations/20250113162855_remove_tmp_tables/migration.sql b/prisma/migrations/20250113162855_remove_tmp_tables/migration.sql new file mode 100644 index 00000000..edb3453e --- /dev/null +++ b/prisma/migrations/20250113162855_remove_tmp_tables/migration.sql @@ -0,0 +1,186 @@ +/* + Warnings: + + - You are about to drop the `BaseYear2` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `BiogenicEmissions2` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Company2` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Economy2` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Emissions2` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Employees2` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Goal2` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Industry2` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `IndustryGics2` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Initiative2` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Metadata2` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `ReportingPeriod2` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Scope12` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Scope1And22` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Scope22` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Scope32` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Scope3Category2` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `StatedTotalEmissions2` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Turnover2` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `User2` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "BaseYear2" DROP CONSTRAINT "BaseYear2_companyId_fkey"; + +-- DropForeignKey +ALTER TABLE "BiogenicEmissions2" DROP CONSTRAINT "BiogenicEmissions2_emissionsId_fkey"; + +-- DropForeignKey +ALTER TABLE "Economy2" DROP CONSTRAINT "Economy2_reportingPeriodId_fkey"; + +-- DropForeignKey +ALTER TABLE "Emissions2" DROP CONSTRAINT "Emissions2_reportingPeriodId_fkey"; + +-- DropForeignKey +ALTER TABLE "Employees2" DROP CONSTRAINT "Employees2_economyId_fkey"; + +-- DropForeignKey +ALTER TABLE "Goal2" DROP CONSTRAINT "Goal2_companyId_fkey"; + +-- DropForeignKey +ALTER TABLE "Industry2" DROP CONSTRAINT "Industry2_companyWikidataId_fkey"; + +-- DropForeignKey +ALTER TABLE "Industry2" DROP CONSTRAINT "Industry2_gicsSubIndustryCode_fkey"; + +-- DropForeignKey +ALTER TABLE "Initiative2" DROP CONSTRAINT "Initiative2_companyId_fkey"; + +-- DropForeignKey +ALTER TABLE "Metadata2" DROP CONSTRAINT "Metadata2_baseYearId_fkey"; + +-- DropForeignKey +ALTER TABLE "Metadata2" DROP CONSTRAINT "Metadata2_biogenicEmissionsId_fkey"; + +-- DropForeignKey +ALTER TABLE "Metadata2" DROP CONSTRAINT "Metadata2_categoryId_fkey"; + +-- DropForeignKey +ALTER TABLE "Metadata2" DROP CONSTRAINT "Metadata2_employeesId_fkey"; + +-- DropForeignKey +ALTER TABLE "Metadata2" DROP CONSTRAINT "Metadata2_goalId_fkey"; + +-- DropForeignKey +ALTER TABLE "Metadata2" DROP CONSTRAINT "Metadata2_industryId_fkey"; + +-- DropForeignKey +ALTER TABLE "Metadata2" DROP CONSTRAINT "Metadata2_initiativeId_fkey"; + +-- DropForeignKey +ALTER TABLE "Metadata2" DROP CONSTRAINT "Metadata2_reportingPeriodId_fkey"; + +-- DropForeignKey +ALTER TABLE "Metadata2" DROP CONSTRAINT "Metadata2_scope1And2Id_fkey"; + +-- DropForeignKey +ALTER TABLE "Metadata2" DROP CONSTRAINT "Metadata2_scope1Id_fkey"; + +-- DropForeignKey +ALTER TABLE "Metadata2" DROP CONSTRAINT "Metadata2_scope2Id_fkey"; + +-- DropForeignKey +ALTER TABLE "Metadata2" DROP CONSTRAINT "Metadata2_scope3Id_fkey"; + +-- DropForeignKey +ALTER TABLE "Metadata2" DROP CONSTRAINT "Metadata2_statedTotalEmissionsId_fkey"; + +-- DropForeignKey +ALTER TABLE "Metadata2" DROP CONSTRAINT "Metadata2_turnoverId_fkey"; + +-- DropForeignKey +ALTER TABLE "Metadata2" DROP CONSTRAINT "Metadata2_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "Metadata2" DROP CONSTRAINT "Metadata2_verifiedByUserId_fkey"; + +-- DropForeignKey +ALTER TABLE "ReportingPeriod2" DROP CONSTRAINT "ReportingPeriod2_companyId_fkey"; + +-- DropForeignKey +ALTER TABLE "Scope12" DROP CONSTRAINT "Scope12_emissionsId_fkey"; + +-- DropForeignKey +ALTER TABLE "Scope1And22" DROP CONSTRAINT "Scope1And22_emissionsId_fkey"; + +-- DropForeignKey +ALTER TABLE "Scope22" DROP CONSTRAINT "Scope22_emissionsId_fkey"; + +-- DropForeignKey +ALTER TABLE "Scope32" DROP CONSTRAINT "Scope32_emissionsId_fkey"; + +-- DropForeignKey +ALTER TABLE "Scope3Category2" DROP CONSTRAINT "Scope3Category2_scope3Id_fkey"; + +-- DropForeignKey +ALTER TABLE "StatedTotalEmissions2" DROP CONSTRAINT "StatedTotalEmissions2_emissionsId_fkey"; + +-- DropForeignKey +ALTER TABLE "StatedTotalEmissions2" DROP CONSTRAINT "StatedTotalEmissions2_scope3Id_fkey"; + +-- DropForeignKey +ALTER TABLE "Turnover2" DROP CONSTRAINT "Turnover2_economyId_fkey"; + +-- DropTable +DROP TABLE "BaseYear2"; + +-- DropTable +DROP TABLE "BiogenicEmissions2"; + +-- DropTable +DROP TABLE "Company2"; + +-- DropTable +DROP TABLE "Economy2"; + +-- DropTable +DROP TABLE "Emissions2"; + +-- DropTable +DROP TABLE "Employees2"; + +-- DropTable +DROP TABLE "Goal2"; + +-- DropTable +DROP TABLE "Industry2"; + +-- DropTable +DROP TABLE "IndustryGics2"; + +-- DropTable +DROP TABLE "Initiative2"; + +-- DropTable +DROP TABLE "Metadata2"; + +-- DropTable +DROP TABLE "ReportingPeriod2"; + +-- DropTable +DROP TABLE "Scope12"; + +-- DropTable +DROP TABLE "Scope1And22"; + +-- DropTable +DROP TABLE "Scope22"; + +-- DropTable +DROP TABLE "Scope32"; + +-- DropTable +DROP TABLE "Scope3Category2"; + +-- DropTable +DROP TABLE "StatedTotalEmissions2"; + +-- DropTable +DROP TABLE "Turnover2"; + +-- DropTable +DROP TABLE "User2"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1e9b2565..50a88333 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -49,389 +49,31 @@ model Company { // TODO: Improve structure of base years to handle the case where we have the same value for all scopes. // The simple solution is to always store three BaseYears, even if they have the same value. This adds more data, to be processed and transferred, but will be simpler to understand and work with. model BaseYear { - id Int @id @default(autoincrement()) - // IDEA: maybe the year here should be in relation to a specific reporting period, since we need to account for special cases where reporting periods are not just one year - year Int - scope Int // 1-3 - companyId String - metadataId Int - - metadata Metadata @relation(fields: [metadataId], references: [id]) - company Company @relation(fields: [companyId], references: [wikidataId]) -} - -/// Connecting a company to a specific industry and metadata -/// This also gives us the flexibility to add more ways to keep track of which industry a company belongs to. -model Industry { - id Int @id @default(autoincrement()) - gicsSubIndustryCode String - metadataId Int - companyWikidataId String @unique - - company Company @relation(fields: [companyWikidataId], references: [wikidataId]) - metadata Metadata @relation(fields: [metadataId], references: [id]) - industryGics IndustryGics @relation(fields: [gicsSubIndustryCode], references: [subIndustryCode]) -} - -/// A table containing the standardised GICS codes for categorizing companies into various industries. -model IndustryGics { - sectorCode String - groupCode String - industryCode String - subIndustryCode String @id - - industries Industry[] -} - -/// A reporting period is a timespan for accounting emissions as well as financial data -model ReportingPeriod { - startDate DateTime - endDate DateTime - /// The year needs to be the same year as the endDate. - year String - /// Save URLs to the sustainability- and potentially also the yearly report for this reporting period. - /// This needs to be separate from the source URLs for each datapoint, since the data might be updated in a more recent report. - /// At the same time, we also want to refer back to the actual reports from a given reporting period for comparisons. - reportURL String? - - companyId String - emissionsId Int? @unique - economyId Int? @unique - metadataId Int - - metadata Metadata @relation(fields: [metadataId], references: [id]) - economy Economy? @relation(fields: [economyId], references: [id]) - emissions Emissions? @relation(fields: [emissionsId], references: [id]) - company Company @relation(fields: [companyId], references: [wikidataId]) - - /// Compound id based on meaningful data - @@id(name: "reportingPeriodId", [companyId, year]) -} - -/// Reported emissions for a specific reporting period -model Emissions { - id Int @id @default(autoincrement()) - // NOTE: If we remove the @unique requirement here, we could perhaps add more scope1 values for the same ReportingPeriod, but only show one at a time - // This might help us if we want to keep one scope1 extracted by garbo, one estimated by exiobase and one manually entered. However, this would get complicated. - scope1Id Int? @unique - scope2Id Int? @unique - scope3Id Int? @unique - biogenicEmissionsId Int? @unique - scope1And2Id Int? @unique - statedTotalEmissionsId Int? @unique - - statedTotalEmissions StatedTotalEmissions? @relation(fields: [statedTotalEmissionsId], references: [id]) - scope1And2 Scope1And2? @relation(fields: [scope1And2Id], references: [id]) - biogenicEmissions BiogenicEmissions? @relation(fields: [biogenicEmissionsId], references: [id]) - reportingPeriod ReportingPeriod? - scope1 Scope1? @relation(fields: [scope1Id], references: [id]) - scope2 Scope2? @relation(fields: [scope2Id], references: [id]) - scope3 Scope3? @relation(fields: [scope3Id], references: [id]) -} - -/// This is used when companies only report a total number for either overall- or scope 3 emissions. -/// TODO: Maybe this should be simplified to just be a `statedTotal` property for the Emissions, and `statedTotal` for Scope3? -/// However, the main reason behind using a separate entity for StatedTotalEmissions is because then we can keep track of metadata -/// specifically for the stated totals from the report. For example if the company changes an incorrect statedTotal that does not match the actual calculated total. -/// Not sure if this is worth it, in this structure though. Might be too complicated. -model StatedTotalEmissions { - id Int @id @default(autoincrement()) - total Float? - metadataId Int - scope3Id Int? @unique - - unit String - emissions Emissions? - metadata Metadata @relation(fields: [metadataId], references: [id]) - scope3 Scope3? @relation(fields: [scope3Id], references: [id]) -} - -/// This is used when companies have bad reporting where they have combined scope 1+2 as one value -model Scope1And2 { - id Int @id @default(autoincrement()) - total Float? - metadataId Int - - unit String - emissions Emissions? - metadata Metadata @relation(fields: [metadataId], references: [id]) -} - -/// Biogenic emissions are reported separately from scope 1-3 -/// If we want to save a more detailed breakdown (when companies reported this), we can use the metadata comment to save this context -model BiogenicEmissions { - id Int @id @default(autoincrement()) - /// Sometimes companies break it down into scope 1-3 - however these should always be stored as a total number according to the GHG protocol. - total Float? - metadataId Int - - unit String - emissions Emissions? - metadata Metadata @relation(fields: [metadataId], references: [id]) -} - -model Scope1 { - id Int @id @default(autoincrement()) - total Float? - metadataId Int - - unit String - emissions Emissions? - metadata Metadata @relation(fields: [metadataId], references: [id]) -} - -/// For scope 2 emissions, we choose either market-based, location-based or unknown (if the company didn't specify if mb or lb) -/// We generally prefer using market-based emissions, but if that doesn't exist we could use location-based ones, and finally unknown. -model Scope2 { - id Int @id @default(autoincrement()) - /// Market-based emissions - mb Float? - /// Location-based emissions - lb Float? - /// Unknown scope 2 emissions could be either market-based or location-based - unknown Float? - metadataId Int - - unit String - emissions Emissions? - metadata Metadata @relation(fields: [metadataId], references: [id]) -} - -// TODO: We need to separate scope3 into separate categories, because we need metadata for each scope 3 category value. - -/// Scope 3 emissions according to the GHG protocol. -model Scope3 { - id Int @id @default(autoincrement()) - - /// Sometimes, companies only report a total value for scope 3 emissions without disclosing the scope 3 categories. - /// Other times, they might report both, but their stated total scope 3 emissions might be different than the actual sum of their scope 3 categories. - /// To get around this, we separate statedTotalEmissions from the actual mathematical total that we summarize during runtime. - statedTotalEmissionsId Int? @unique - metadataId Int - - // The scope 3 categories, both reported and estimated. - // TODO: Add validation so there can only be one for each category, and max 16 entries. - categories Scope3Category[] - statedTotalEmissions StatedTotalEmissions? - emissions Emissions? - metadata Metadata @relation(fields: [metadataId], references: [id]) -} - -/// Details about scope 3 categories. Here's a list of valid categories and their names: -/// -/// 1. purchasedGoods -/// 2. capitalGoods -/// 3. fuelAndEnergyRelatedActivities -/// 4. upstreamTransportationAndDistribution -/// 5. wasteGeneratedInOperations -/// 6. businessTravel -/// 7. employeeCommuting -/// 8. upstreamLeasedAssets -/// 9. downstreamTransportationAndDistribution -/// 10. processingOfSoldProducts -/// 11. useOfSoldProducts -/// 12. endOfLifeTreatmentOfSoldProducts -/// 13. downstreamLeasedAssets -/// 14. franchises -/// 15. investments -/// 16. other -model Scope3Category { - id Int @id @default(autoincrement()) - /// Int from 1-15 defining the scope 3 category. - /// 16 is a special value for "other", which is not included in the GHG protocol, - /// but useful to handle companies who invent their own scope 3 "categories". - category Int - - // IDEA: When we want to keep track of detailed emissions for each scope 3 category, we could - // add statedTotalEmissions (to this DB schema) and calculatedTotalEmissions (during runtime). - // This distinction would help us keep track of detailed emissions within each scope 3 category, for example - // knowing the composition of cat 6. business travel, and how it changes across different years. - // This would also make easier to detect discrepancies between stated emissions and the actual calculated total value - // based on the composition for each scope 3 category. This would simplify the review process if we could automatically find errors. - total Float? - scope3Id Int - metadataId Int - - unit String - scope3 Scope3 @relation(fields: [scope3Id], references: [id]) - metadata Metadata @relation(fields: [metadataId], references: [id]) -} - -model Economy { - id Int @id @default(autoincrement()) - turnoverId Int? @unique - employeesId Int? @unique - - turnover Turnover? @relation(fields: [turnoverId], references: [id]) - employees Employees? @relation(fields: [employeesId], references: [id]) - reportingPeriod ReportingPeriod? -} - -model Turnover { - id Int @id @default(autoincrement()) - // IDEA: Should we store turnover with another datatype to prevent rounding errors? Money doesn't seem to be a good fit for storing as floats. - value Float? - currency String? - metadataId Int - - economy Economy? - metadata Metadata @relation(fields: [metadataId], references: [id]) -} - -model Employees { - id Int @id @default(autoincrement()) - /// Number of employees (using various methods) - value Float? - /// How the number of employees were calculated, e.g. Full-time equivalents (FTE) or similar. - unit String? - metadataId Int - - economy Economy? - metadata Metadata @relation(fields: [metadataId], references: [id]) -} - -model Goal { - id Int @id @default(autoincrement()) - description String - year String? - target Float? - // TODO: Maybe reference ReportingPeriod instead - // However, baseYears for goals might be different than other reporting periods for the company. - // Thus, we in some cases need to reference something else than the reporting periods - // For now, let's store them as strings, but in the future maybe use another representation. - baseYear String? - metadataId Int - companyId String - - metadata Metadata @relation(fields: [metadataId], references: [id]) - company Company @relation(fields: [companyId], references: [wikidataId]) -} - -model Initiative { - id Int @id @default(autoincrement()) - title String - description String? - year String? - scope String? - companyId String - metadataId Int - - company Company @relation(fields: [companyId], references: [wikidataId]) - metadata Metadata @relation(fields: [metadataId], references: [id]) -} - -/// Every datapoint has associated metadata about who changed it, when, and using what source -model Metadata { - id Int @id @default(autoincrement()) - /// The comment is used to add relevant information about why the datapoint looks like it does - comment String? - source String? - updatedAt DateTime @default(now()) @updatedAt - /// The userId who last updated the associated datapoint - userId Int - /// The userId who verified the associated datapoint. - /// Should only be set after human verification, and null if it has not been verified. - /// Verified datapoints should not be automatically updated by AI extracted or estimatated data, but always go through manual review going forward. - verifiedByUserId Int? - - /// Where the data originated from, e.g. manual entry, AI extraction or estimated with a tool like Exiobase. - /// Value is a string enum with vlaues like `garbo`, `manual`, `estimated:exiobase` - dataOrigin String? - - goal Goal[] - initiative Initiative[] - scope1 Scope1[] - scope2 Scope2[] - scope3 Scope3[] - reportingPeriod ReportingPeriod[] - baseYear BaseYear[] - biogenicEmissions BiogenicEmissions[] - scope1And2 Scope1And2[] - statedTotalEmissions StatedTotalEmissions[] - user User @relation("metadata_user_id", fields: [userId], references: [id]) - verifiedBy User? @relation("metadata_verified_by", fields: [verifiedByUserId], references: [id]) - industries Industry[] - categories Scope3Category[] - turnover Turnover[] - employees Employees[] -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - name String - - // TODO: connect with github ID - // TODO: store github profile image - or get it via API - updated Metadata[] @relation("metadata_user_id") - verified Metadata[] @relation("metadata_verified_by") -} - -////////////////////////////////// NEW SCHEMA BELOW /////////////////////////////////////// - -model Company2 { - // // NOTE: Maybe just use an integer as the ID for companies, and use the unique hash instead. - // // We want to separate the company id from the URL slug - // id String @id @default(cuid()) - /// wikidataId is our unique identifier to link to companies, also used to export to or import from wikidata - wikidataId String @id - name String - description String? - /// Company website URL - url String? - - /// TODO: Save Swedish org number, which might be the same as LEI or ISIN - // swedishOrgNumber - - // TODO: save country and city/region for each company - - industry Industry2? - reportingPeriods ReportingPeriod2[] - goals Goal2[] - initiatives Initiative2[] - baseYears BaseYear2[] - - /// A comment only visible for reviewers to help them work with the company data and remember important info about this company. - internalComment String? - - /// Tags to help categorize and filter companies - tags String[] -} - -// TODO: Handle broken reporting periods which start in one year and end the next year. -// Maybe reference reporting periods directly in order to only store the dates in one place. - -// TODO: question for Alex: Can we assume that base years always are the first year with reported emissions? -// E.g. if they started reporting in 2020, would that be the base year? Or can they say 2015 as base year, without knowing anything about their emissions from that year? - -// TODO: Improve structure of base years to handle the case where we have the same value for all scopes. -// The simple solution is to always store three BaseYears, even if they have the same value. This adds more data, to be processed and transferred, but will be simpler to understand and work with. -model BaseYear2 { id Int @id @default(autoincrement()) // IDEA: maybe the year here should be in relation to a specific reporting period, since we need to account for special cases where reporting periods are not just one year year Int scope Int // 1-3 companyId String - metadata Metadata2[] - company Company2 @relation(fields: [companyId], references: [wikidataId], onDelete: Cascade) + metadata Metadata[] + company Company @relation(fields: [companyId], references: [wikidataId], onDelete: Cascade) } /// Connecting a company to a specific industry and metadata /// This also gives us the flexibility to add more ways to keep track of which industry a company belongs to. -model Industry2 { +model Industry { id Int @id @default(autoincrement()) gicsSubIndustryCode String companyWikidataId String @unique - company Company2 @relation(fields: [companyWikidataId], references: [wikidataId], onDelete: Cascade) - metadata Metadata2[] - industryGics IndustryGics2 @relation(fields: [gicsSubIndustryCode], references: [subIndustryCode]) + company Company @relation(fields: [companyWikidataId], references: [wikidataId], onDelete: Cascade) + metadata Metadata[] + industryGics IndustryGics @relation(fields: [gicsSubIndustryCode], references: [subIndustryCode]) } /// A table containing the standardised GICS codes for categorizing companies into various industries. /// These are added by the DB seeding script. -model IndustryGics2 { +model IndustryGics { subIndustryCode String @id subIndustryName String subIndustryDescription String @@ -445,11 +87,11 @@ model IndustryGics2 { industryCode String industryName String - industries Industry2[] + industries Industry[] } /// A reporting period is a timespan for accounting emissions as well as financial data -model ReportingPeriod2 { +model ReportingPeriod { id Int @id @default(autoincrement()) startDate DateTime endDate DateTime @@ -462,26 +104,26 @@ model ReportingPeriod2 { companyId String - metadata Metadata2[] - economy Economy2? - emissions Emissions2? - company Company2 @relation(fields: [companyId], references: [wikidataId], onDelete: Cascade) + metadata Metadata[] + economy Economy? + emissions Emissions? + company Company @relation(fields: [companyId], references: [wikidataId], onDelete: Cascade) @@unique([companyId, year]) } /// Reported emissions for a specific reporting period -model Emissions2 { +model Emissions { id Int @id @default(autoincrement()) reportingPeriodId Int? @unique - scope1 Scope12? - scope2 Scope22? - scope3 Scope32? - scope1And2 Scope1And22? - statedTotalEmissions StatedTotalEmissions2? - biogenicEmissions BiogenicEmissions2? - reportingPeriod ReportingPeriod2? @relation(fields: [reportingPeriodId], references: [id], onDelete: Cascade) + scope1 Scope1? + scope2 Scope2? + scope3 Scope3? + scope1And2 Scope1And2? + statedTotalEmissions StatedTotalEmissions? + biogenicEmissions BiogenicEmissions? + reportingPeriod ReportingPeriod? @relation(fields: [reportingPeriodId], references: [id], onDelete: Cascade) } /// This is used when companies only report a total number for either overall- or scope 3 emissions. @@ -489,54 +131,54 @@ model Emissions2 { /// However, the main reason behind using a separate entity for StatedTotalEmissions is because then we can keep track of metadata /// specifically for the stated totals from the report. For example if the company changes an incorrect statedTotal that does not match the actual calculated total. /// Not sure if this is worth it, in this structure though. Might be too complicated. -model StatedTotalEmissions2 { +model StatedTotalEmissions { id Int @id @default(autoincrement()) total Float? scope3Id Int? @unique unit String emissionsId Int? @unique - emissions Emissions2? @relation(fields: [emissionsId], references: [id], onDelete: Cascade) - metadata Metadata2[] - scope3 Scope32? @relation(fields: [scope3Id], references: [id], onDelete: Cascade) + emissions Emissions? @relation(fields: [emissionsId], references: [id], onDelete: Cascade) + metadata Metadata[] + scope3 Scope3? @relation(fields: [scope3Id], references: [id], onDelete: Cascade) } /// This is used when companies have bad reporting where they have combined scope 1+2 as one value -model Scope1And22 { +model Scope1And2 { id Int @id @default(autoincrement()) total Float? emissionsId Int? @unique unit String - emissions Emissions2? @relation(fields: [emissionsId], references: [id], onDelete: Cascade) - metadata Metadata2[] + emissions Emissions? @relation(fields: [emissionsId], references: [id], onDelete: Cascade) + metadata Metadata[] } /// Biogenic emissions are reported separately from scope 1-3 -model BiogenicEmissions2 { +model BiogenicEmissions { id Int @id @default(autoincrement()) /// Sometimes companies break it down into scope 1-3 - however these should always be stored as a total number according to the GHG protocol. total Float? unit String emissionsId Int? @unique - emissions Emissions2? @relation(fields: [emissionsId], references: [id], onDelete: Cascade) - metadata Metadata2[] + emissions Emissions? @relation(fields: [emissionsId], references: [id], onDelete: Cascade) + metadata Metadata[] } -model Scope12 { +model Scope1 { id Int @id @default(autoincrement()) total Float? unit String emissionsId Int? @unique - emissions Emissions2? @relation(fields: [emissionsId], references: [id], onDelete: Cascade) - metadata Metadata2[] + emissions Emissions? @relation(fields: [emissionsId], references: [id], onDelete: Cascade) + metadata Metadata[] } /// For scope 2 emissions, we choose either market-based, location-based or unknown (if the company didn't specify if mb or lb) /// We generally prefer using market-based emissions, but if that doesn't exist we could use location-based ones, and finally unknown. -model Scope22 { +model Scope2 { id Int @id @default(autoincrement()) /// Market-based emissions mb Float? @@ -547,11 +189,11 @@ model Scope22 { unit String emissionsId Int? @unique - emissions Emissions2? @relation(fields: [emissionsId], references: [id], onDelete: Cascade) - metadata Metadata2[] + emissions Emissions? @relation(fields: [emissionsId], references: [id], onDelete: Cascade) + metadata Metadata[] } -model Scope32 { +model Scope3 { id Int @id @default(autoincrement()) /// Sometimes, companies only report a total value for scope 3 emissions without disclosing the scope 3 categories. @@ -562,10 +204,10 @@ model Scope32 { // The scope 3 categories, both reported and estimated. // TODO: Add validation so there can only be one for each category, and max 16 entries. - statedTotalEmissions StatedTotalEmissions2? - emissions Emissions2? @relation(fields: [emissionsId], references: [id], onDelete: Cascade) - categories Scope3Category2[] - metadata Metadata2[] + statedTotalEmissions StatedTotalEmissions? + emissions Emissions? @relation(fields: [emissionsId], references: [id], onDelete: Cascade) + categories Scope3Category[] + metadata Metadata[] } /// Details about scope 3 categories. Here's a list of valid categories and their names: @@ -586,7 +228,7 @@ model Scope32 { /// 14. franchises /// 15. investments /// 16. other -model Scope3Category2 { +model Scope3Category { id Int @id @default(autoincrement()) /// Int from 1-15 defining the scope 3 category. /// 16 is a special value for "other", which is not included in the GHG protocol, @@ -603,32 +245,32 @@ model Scope3Category2 { scope3Id Int unit String - scope3 Scope32 @relation(fields: [scope3Id], references: [id], onDelete: Cascade) - metadata Metadata2[] + scope3 Scope3 @relation(fields: [scope3Id], references: [id], onDelete: Cascade) + metadata Metadata[] } -model Economy2 { +model Economy { id Int @id @default(autoincrement()) reportingPeriodId Int? @unique - turnover Turnover2? - employees Employees2? - reportingPeriod ReportingPeriod2? @relation(fields: [reportingPeriodId], references: [id], onDelete: Cascade) + turnover Turnover? + employees Employees? + reportingPeriod ReportingPeriod? @relation(fields: [reportingPeriodId], references: [id], onDelete: Cascade) turnoverId Int? } -model Turnover2 { +model Turnover { id Int @id @default(autoincrement()) // IDEA: Should we store turnover with another datatype to prevent rounding errors? Money doesn't seem to be a good fit for storing as floats. value Float? currency String? economyId Int @unique - economy Economy2 @relation(fields: [economyId], references: [id], onDelete: Cascade) - metadata Metadata2[] + economy Economy @relation(fields: [economyId], references: [id], onDelete: Cascade) + metadata Metadata[] } -model Employees2 { +model Employees { id Int @id @default(autoincrement()) /// Number of employees value Float? @@ -636,11 +278,11 @@ model Employees2 { unit String? economyId Int? @unique - economy Economy2? @relation(fields: [economyId], references: [id], onDelete: Cascade) - metadata Metadata2[] + economy Economy? @relation(fields: [economyId], references: [id], onDelete: Cascade) + metadata Metadata[] } -model Goal2 { +model Goal { id Int @id @default(autoincrement()) description String year String? @@ -652,11 +294,11 @@ model Goal2 { baseYear String? companyId String - metadata Metadata2[] - company Company2 @relation(fields: [companyId], references: [wikidataId], onDelete: Cascade) + metadata Metadata[] + company Company @relation(fields: [companyId], references: [wikidataId], onDelete: Cascade) } -model Initiative2 { +model Initiative { id Int @id @default(autoincrement()) title String description String? @@ -664,12 +306,12 @@ model Initiative2 { scope String? companyId String - company Company2 @relation(fields: [companyId], references: [wikidataId], onDelete: Cascade) - metadata Metadata2[] + company Company @relation(fields: [companyId], references: [wikidataId], onDelete: Cascade) + metadata Metadata[] } /// Every datapoint has associated metadata about who changed it, when, and using what source -model Metadata2 { +model Metadata { id Int @id @default(autoincrement()) /// The comment is used to add relevant information about why the datapoint looks like it does comment String? @@ -697,31 +339,31 @@ model Metadata2 { turnoverId Int? employeesId Int? - goal Goal2? @relation(fields: [goalId], references: [id]) - initiative Initiative2? @relation(fields: [initiativeId], references: [id]) - scope1 Scope12? @relation(fields: [scope1Id], references: [id]) - scope2 Scope22? @relation(fields: [scope2Id], references: [id]) - scope3 Scope32? @relation(fields: [scope3Id], references: [id]) - scope1And2 Scope1And22? @relation(fields: [scope1And2Id], references: [id]) - reportingPeriod ReportingPeriod2? @relation(fields: [reportingPeriodId], references: [id]) - baseYear BaseYear2? @relation(fields: [baseYearId], references: [id]) - biogenicEmissions BiogenicEmissions2? @relation(fields: [biogenicEmissionsId], references: [id]) - statedTotalEmissions StatedTotalEmissions2? @relation(fields: [statedTotalEmissionsId], references: [id]) - user User2 @relation("metadata_user_id", fields: [userId], references: [id]) - verifiedBy User2? @relation("metadata_verified_by", fields: [verifiedByUserId], references: [id]) - industry Industry2? @relation(fields: [industryId], references: [id]) - category Scope3Category2? @relation(fields: [categoryId], references: [id]) - turnover Turnover2? @relation(fields: [turnoverId], references: [id]) - employees Employees2? @relation(fields: [employeesId], references: [id]) + goal Goal? @relation(fields: [goalId], references: [id]) + initiative Initiative? @relation(fields: [initiativeId], references: [id]) + scope1 Scope1? @relation(fields: [scope1Id], references: [id]) + scope2 Scope2? @relation(fields: [scope2Id], references: [id]) + scope3 Scope3? @relation(fields: [scope3Id], references: [id]) + scope1And2 Scope1And2? @relation(fields: [scope1And2Id], references: [id]) + reportingPeriod ReportingPeriod? @relation(fields: [reportingPeriodId], references: [id]) + baseYear BaseYear? @relation(fields: [baseYearId], references: [id]) + biogenicEmissions BiogenicEmissions? @relation(fields: [biogenicEmissionsId], references: [id]) + statedTotalEmissions StatedTotalEmissions? @relation(fields: [statedTotalEmissionsId], references: [id]) + user User @relation("metadata_user_id", fields: [userId], references: [id]) + verifiedBy User? @relation("metadata_verified_by", fields: [verifiedByUserId], references: [id]) + industry Industry? @relation(fields: [industryId], references: [id]) + category Scope3Category? @relation(fields: [categoryId], references: [id]) + turnover Turnover? @relation(fields: [turnoverId], references: [id]) + employees Employees? @relation(fields: [employeesId], references: [id]) } -model User2 { +model User { id Int @id @default(autoincrement()) email String @unique name String // TODO: connect with github ID // TODO: store github profile image - or get it via API - updated Metadata2[] @relation("metadata_user_id") - verified Metadata2[] @relation("metadata_verified_by") + updated Metadata[] @relation("metadata_user_id") + verified Metadata[] @relation("metadata_verified_by") } diff --git a/prisma/seed.ts b/prisma/seed.ts index 748af29c..b0325549 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,21 +1,29 @@ +import 'dotenv/config' import { PrismaClient } from '@prisma/client' import { seedGicsCodes } from '../scripts/add-gics' const prisma = new PrismaClient() async function seedUsers() { - return prisma.user2.createMany({ - data: [ - { - email: 'hej@klimatkollen.se', - name: 'Garbo (Klimatkollen)', - }, - { - email: 'alex@klimatkollen.se', - name: 'Alex (Klimatkollen)', - }, - ], - }) + const users = [ + { + email: 'hej@klimatkollen.se', + name: 'Garbo (Klimatkollen)', + }, + { + email: 'alex@klimatkollen.se', + name: 'Alex (Klimatkollen)', + }, + ] + + for (const user of users) { + await prisma.user.upsert({ + where: { email: user.email }, + create: user, + update: user, + select: { id: true }, + }) + } } async function main() { diff --git a/scripts/add-gics.ts b/scripts/add-gics.ts index 78204125..dffba6e7 100644 --- a/scripts/add-gics.ts +++ b/scripts/add-gics.ts @@ -1203,6 +1203,8 @@ function getGicsTranslationFile(codes: IndustryGicsWithTranslations[]) { } export async function seedGicsCodes() { + const count = await prisma.industryGics2.count() + if (count > 0) return await prisma.industryGics2.createMany({ data: gicsCodes }) } diff --git a/scripts/migrateDataToNewSchema.ts b/scripts/migrateDataToNewSchema.ts index 7ac629fb..3b740b2b 100644 --- a/scripts/migrateDataToNewSchema.ts +++ b/scripts/migrateDataToNewSchema.ts @@ -13,8 +13,8 @@ async function migrateData() { ) const [garbo, alex] = await Promise.all([ - prisma.user2.findFirstOrThrow({ where: { email: 'hej@klimatkollen.se' } }), - prisma.user2.findFirstOrThrow({ where: { email: 'alex@klimatkollen.se' } }), + prisma.user.findFirstOrThrow({ where: { email: 'hej@klimatkollen.se' } }), + prisma.user.findFirstOrThrow({ where: { email: 'alex@klimatkollen.se' } }), ]) const userIds = { @@ -40,12 +40,12 @@ async function migrateData() { const { reportingPeriods, industry, goals, initiatives, ...companyData } = company - const createdCompany = await prisma.company2.create({ + const createdCompany = await prisma.company.create({ data: { ...companyData }, }) if (industry) { - await prisma.industry2.create({ + await prisma.industry.create({ data: { gicsSubIndustryCode: industry.industryGics.subIndustryCode, companyWikidataId: createdCompany.wikidataId, @@ -55,7 +55,7 @@ async function migrateData() { } for (const period of reportingPeriods) { - const createdPeriod = await prisma.reportingPeriod2.create({ + const createdPeriod = await prisma.reportingPeriod.create({ data: { startDate: new Date(period.startDate), endDate: new Date(period.endDate), @@ -66,7 +66,7 @@ async function migrateData() { }) if (period.economy) { - await prisma.economy2.create({ + await prisma.economy.create({ data: { reportingPeriodId: createdPeriod.id, turnover: period.economy.turnover @@ -94,7 +94,7 @@ async function migrateData() { } if (period.emissions) { - await prisma.emissions2.create({ + await prisma.emissions.create({ data: { reportingPeriodId: createdPeriod.id, scope1: period.emissions.scope1 @@ -199,7 +199,7 @@ async function migrateData() { } for (const goal of goals) { - await prisma.goal2.create({ + await prisma.goal.create({ data: { ...goal, companyId: createdCompany.wikidataId, @@ -209,7 +209,7 @@ async function migrateData() { } for (const initiative of initiatives) { - await prisma.initiative2.create({ + await prisma.initiative.create({ data: { ...initiative, companyId: createdCompany.wikidataId, diff --git a/src/api/routes/company.read.ts b/src/api/routes/company.read.ts index b06bc1a1..ddb36b84 100644 --- a/src/api/routes/company.read.ts +++ b/src/api/routes/company.read.ts @@ -1,15 +1,20 @@ import express, { Request, Response, NextFunction } from 'express' import { getGics } from '../../lib/gics' -import { validateRequestParams } from '../middlewares/zod-middleware' import { cache, enableCors } from '../middlewares/middlewares' -import { wikidataIdParamSchema } from '../schemas' -import { prisma } from '../../lib/prisma' import { GarboAPIError } from '../../lib/garbo-api-error' +import { prisma } from '../../lib/prisma' +import apiConfig from '../../config/api' + +import { Prisma } from '@prisma/client' const router = express.Router() const metadata = { + orderBy: { + updatedAt: 'desc' as const, + }, + take: 1, select: { comment: true, source: true, @@ -28,6 +33,10 @@ const metadata = { } const minimalMetadata = { + orderBy: { + updatedAt: 'desc' as const, + }, + take: 1, select: { verifiedBy: { select: { @@ -41,42 +50,56 @@ function removeEmptyValues(obj: Record) { return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v != null)) } -// ## DÖLJ DESSA från API:et -const HIDDEN_FROM_API = new Set([ - 'Q22629259', // GARO - 'Q37562781', // GARO - 'Q489097', // Ernst & Young - 'Q10432209', // Prisma Properties - 'Q5168854', // Copperstone Resources AB - 'Q115167497', // Specialfastigheter - 'Q549624', // RISE AB - 'Q34', // Swedish Logistic Property AB, - - // OLD pages: - - 'Q8301325', // SJ - 'Q112055015', // BONESUPPORT - 'Q97858523', // Almi - 'Q2438127', // Dynavox - 'Q117352880', // BioInvent - 'Q115167497', // Specialfastigheter -]) - -const unwantedWikidataIds = Array.from(HIDDEN_FROM_API) - function isNumber(n: unknown): n is number { return Number.isFinite(n) } -const origins = - process.env.NODE_ENV === 'development' - ? ['http://localhost:4321'] - : ['https://beta.klimatkollen.se', 'https://klimatkollen.se'] +router.use(enableCors(apiConfig.corsAllowOrigins as unknown as string[])) -router.use(enableCors(origins)) +function transformMetadata(data: any): any { + if (Array.isArray(data)) { + return data.map((item) => transformMetadata(item)) + } else if (data && typeof data === 'object') { + return Object.entries(data).reduce((acc, [key, value]) => { + if (key === 'metadata' && Array.isArray(value)) { + acc[key] = value[0] || null + } else if (value instanceof Date) { + // Leave Date fields untouched + acc[key] = value + } else if (typeof value === 'object' && value !== null) { + acc[key] = transformMetadata(value) + } else { + acc[key] = value + } + return acc + }, {} as Record) + } + return data +} // TODO: Find a way to re-use the same logic to process companies both for GET /companies and GET /companies/:wikidataId +/** + * @swagger + * /companies: + * get: + * summary: Get all companies + * description: Retrieve a list of all companies with their emissions, economic data, industry classification, goals, and initiatives + * tags: [Companies] + * responses: + * 200: + * description: List of companies + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CompanyList' + * 500: + * description: Server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.get( '/', cache(), @@ -194,14 +217,14 @@ router.get( }, }, }, - where: { - wikidataId: { - notIn: unwantedWikidataIds, - }, - }, }) + + const transformedCompanies = Array.isArray(companies) + ? companies.map((company) => transformMetadata(company)) + : transformMetadata(companies) + res.json( - companies + transformedCompanies // Calculate total emissions for each scope type .map((company) => ({ ...company, @@ -227,7 +250,7 @@ router.get( ...removeEmptyValues(reportingPeriod.emissions.scope3), calculatedTotalEmissions: reportingPeriod.emissions.scope3.categories.some( - (c) => Boolean(c.metadata.verifiedBy) + (c) => Boolean(c.metadata?.verifiedBy) ) ? reportingPeriod.emissions.scope3.categories.reduce( (total, category) => @@ -241,7 +264,7 @@ router.get( }) || undefined, }, - metadata: reportingPeriod.metadata[0], + metadata: reportingPeriod.metadata, }) ), })) @@ -293,9 +316,42 @@ router.get( } ) +/** + * @swagger + * /companies/{wikidataId}: + * get: + * summary: Get a specific company + * description: Retrieve detailed information about a specific company + * tags: [Companies] + * parameters: + * - in: path + * name: wikidataId + * required: true + * schema: + * type: string + * description: Wikidata ID of the company + * responses: + * 200: + * description: Company details + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CompanyDetails' + * 404: + * description: Company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 500: + * description: Server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ router.get( '/:wikidataId', - validateRequestParams(wikidataIdParamSchema), cache(), async (req: Request, res: Response, next: NextFunction) => { try { @@ -303,11 +359,6 @@ router.get( const company = await prisma.company.findFirst({ where: { wikidataId, - AND: { - wikidataId: { - notIn: unwantedWikidataIds, - }, - }, }, select: { wikidataId: true, @@ -452,8 +503,9 @@ router.get( return next(new GarboAPIError('Company not found', { statusCode: 404 })) } + const transformedCompany = transformMetadata(company) res.json( - [company] + [transformedCompany] // Calculate total emissions for each scope type .map((company) => ({ ...company, @@ -479,7 +531,7 @@ router.get( ...removeEmptyValues(reportingPeriod.emissions.scope3), calculatedTotalEmissions: reportingPeriod.emissions.scope3.categories.some( - (c) => Boolean(c.metadata.verifiedBy) + (c) => Boolean(c.metadata?.verifiedBy) ) ? reportingPeriod.emissions.scope3.categories.reduce( (total, category) => @@ -493,7 +545,7 @@ router.get( }) || undefined, }, - metadata: reportingPeriod.metadata[0], + metadata: reportingPeriod.metadata, }) ), // Add translations for GICS data @@ -546,24 +598,30 @@ router.get( .at(0) ) } catch (error) { - next( - new GarboAPIError('Failed to load company', { - original: error, - statusCode: 500, - }) - ) + if (error instanceof Prisma.PrismaClientValidationError) { + next( + new GarboAPIError('Invalid company data format', { + original: error, + statusCode: 422, + }) + ) + } else if (error instanceof Prisma.PrismaClientKnownRequestError) { + next( + new GarboAPIError('Database error while loading company', { + original: error, + statusCode: 500, + }) + ) + } else { + next( + new GarboAPIError('Failed to load company', { + original: error, + statusCode: 500, + }) + ) + } } } ) -// Error handler middleware -router.use( - (err: GarboAPIError, req: Request, res: Response, next: NextFunction) => { - res.status(err.statusCode || 500).json({ - error: err.message, - details: err.original || null, - }) - } -) - export default router