Skip to content

Commit

Permalink
Merge pull request #568 from Klimatbyran/staging
Browse files Browse the repository at this point in the history
Update production with latest changes: Nullable fields + returning Ids  and schema/types cleanup
hugo-nl authored Jan 22, 2025
2 parents 35d66da + 60dff7f commit 30461ad
Showing 17 changed files with 627 additions and 535 deletions.
324 changes: 324 additions & 0 deletions src/api/args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
import { Prisma } from '@prisma/client'

export const emissionsArgs = {
include: {
scope1: { select: { id: true } },
scope2: { select: { id: true } },
scope3: { select: { id: true } },
biogenicEmissions: { select: { id: true } },
scope1And2: { select: { id: true } },
statedTotalEmissions: { select: { id: true } },
},
} satisfies Prisma.EmissionsDefaultArgs

export const economyArgs = {
include: {
employees: { select: { id: true } },
turnover: { select: { id: true } },
},
} satisfies Prisma.EconomyDefaultArgs

export const reportingPeriodArgs = {
include: {
emissions: emissionsArgs,
economy: economyArgs,
company: { select: { wikidataId: true } },
},
} satisfies Prisma.ReportingPeriodDefaultArgs

const metadataArgs = {
orderBy: {
updatedAt: 'desc' as const,
},
take: 1,
select: {
id: true,
comment: true,
source: true,
updatedAt: true,
user: {
select: {
name: true,
},
},
verifiedBy: {
select: {
name: true,
},
},
} satisfies Prisma.MetadataDefaultArgs['select'],
}

const minimalMetadataArgs = {
orderBy: {
updatedAt: 'desc' as const,
},
take: 1,
select: {
verifiedBy: {
select: {
name: true,
},
},
},
}

export const detailedCompanyArgs = {
select: {
wikidataId: true,
name: true,
description: true,
reportingPeriods: {
select: {
id: true,
startDate: true,
endDate: true,
reportURL: true,
economy: {
select: {
id: true,
turnover: {
select: {
id: true,
value: true,
currency: true,
metadata: metadataArgs,
},
},
employees: {
select: {
id: true,
value: true,
unit: true,
metadata: metadataArgs,
},
},
},
},
emissions: {
select: {
id: true,
scope1: {
select: {
id: true,
total: true,
unit: true,
metadata: metadataArgs,
},
},
scope2: {
select: {
id: true,
lb: true,
mb: true,
unknown: true,
unit: true,
metadata: metadataArgs,
},
},
scope3: {
select: {
id: true,
statedTotalEmissions: {
select: {
id: true,
total: true,
unit: true,
metadata: metadataArgs,
},
},
categories: {
select: {
id: true,
category: true,
total: true,
unit: true,
metadata: metadataArgs,
},
orderBy: {
category: 'asc',
},
},
metadata: metadataArgs,
},
},
biogenicEmissions: {
select: {
id: true,
total: true,
unit: true,
metadata: metadataArgs,
},
},
scope1And2: {
select: {
id: true,
total: true,
unit: true,
metadata: metadataArgs,
},
},
statedTotalEmissions: {
select: {
id: true,
total: true,
unit: true,
metadata: metadataArgs,
},
},
},
},
metadata: metadataArgs,
},
orderBy: {
startDate: 'desc',
},
},
industry: {
select: {
id: true,
industryGics: {
select: {
sectorCode: true,
groupCode: true,
industryCode: true,
subIndustryCode: true,
},
},
metadata: metadataArgs,
},
},
goals: {
select: {
id: true,
description: true,
year: true,
baseYear: true,
target: true,
metadata: metadataArgs,
},
orderBy: {
year: 'desc',
},
},
initiatives: {
select: {
id: true,
title: true,
description: true,
year: true,
scope: true,
metadata: metadataArgs,
},
orderBy: {
year: 'desc',
},
},
},
} satisfies Prisma.CompanyDefaultArgs

export const companyListArgs = {
select: {
wikidataId: true,
name: true,
description: true,
reportingPeriods: {
select: {
startDate: true,
endDate: true,
reportURL: true,
economy: {
select: {
turnover: {
select: {
value: true,
currency: true,
metadata: minimalMetadataArgs,
},
},
employees: {
select: {
value: true,
unit: true,
metadata: minimalMetadataArgs,
},
},
},
},
emissions: {
select: {
scope1: {
select: {
total: true,
unit: true,
metadata: minimalMetadataArgs,
},
},
scope2: {
select: {
lb: true,
mb: true,
unknown: true,
unit: true,
metadata: minimalMetadataArgs,
},
},
scope3: {
select: {
statedTotalEmissions: {
select: {
total: true,
unit: true,
metadata: minimalMetadataArgs,
},
},
categories: {
select: {
category: true,
total: true,
unit: true,
metadata: minimalMetadataArgs,
},
orderBy: {
category: 'asc',
},
},
metadata: minimalMetadataArgs,
},
},
scope1And2: {
select: {
total: true,
unit: true,
metadata: minimalMetadataArgs,
},
},
statedTotalEmissions: {
select: {
total: true,
unit: true,
metadata: minimalMetadataArgs,
},
},
},
},
},
orderBy: {
startDate: 'desc',
},
},
industry: {
select: {
industryGics: {
select: {
sectorCode: true,
groupCode: true,
industryCode: true,
subIndustryCode: true,
},
},
metadata: minimalMetadataArgs,
},
},
},
} satisfies Prisma.CompanyDefaultArgs
24 changes: 12 additions & 12 deletions src/api/routes/company.delete.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ import { emissionsService } from '../services/emissionsService'
import {
wikidataIdParamSchema,
emptyBodySchema,
garboEntitySchema,
garboEntityIdSchema,
} from '../schemas'
import { getTags } from '../../config/openapi'
import { GarboEntityId, WikidataIdParams } from '../types'
@@ -45,7 +45,7 @@ export async function companyDeleteRoutes(app: FastifyInstance) {
summary: 'Delete a goal',
description: 'Deletes a goal by id',
tags: getTags('Goals'),
params: garboEntitySchema,
params: garboEntityIdSchema,
response: {
204: emptyBodySchema,
},
@@ -68,7 +68,7 @@ export async function companyDeleteRoutes(app: FastifyInstance) {
summary: 'Delete industry',
description: 'Delete a company industry',
tags: getTags('Industry'),
params: garboEntitySchema,
params: garboEntityIdSchema,
response: {
204: emptyBodySchema,
},
@@ -91,7 +91,7 @@ export async function companyDeleteRoutes(app: FastifyInstance) {
summary: 'Delete an initiative',
description: 'Deletes an initiative by id',
tags: getTags('Initiatives'),
params: garboEntitySchema,
params: garboEntityIdSchema,
response: {
204: emptyBodySchema,
},
@@ -114,7 +114,7 @@ export async function companyDeleteRoutes(app: FastifyInstance) {
summary: 'Delete a reporting period',
description: 'Deletes a reporting period by id',
tags: getTags('ReportingPeriods'),
params: garboEntitySchema,
params: garboEntityIdSchema,
response: {
204: emptyBodySchema,
},
@@ -137,7 +137,7 @@ export async function companyDeleteRoutes(app: FastifyInstance) {
summary: 'Delete stated total emissions',
description: 'Deletes stated total emissions by id',
tags: getTags('Emissions'),
params: garboEntitySchema,
params: garboEntityIdSchema,
response: {
204: emptyBodySchema,
},
@@ -160,7 +160,7 @@ export async function companyDeleteRoutes(app: FastifyInstance) {
summary: 'Delete biogenic emissions',
description: 'Deletes biogenic emissions by id',
tags: getTags('Emissions'),
params: garboEntitySchema,
params: garboEntityIdSchema,
response: {
204: emptyBodySchema,
},
@@ -183,7 +183,7 @@ export async function companyDeleteRoutes(app: FastifyInstance) {
summary: 'Delete Scope1',
description: 'Deletes the Scope1 emissions by id',
tags: getTags('Emissions'),
params: garboEntitySchema,
params: garboEntityIdSchema,
response: {
204: emptyBodySchema,
},
@@ -206,7 +206,7 @@ export async function companyDeleteRoutes(app: FastifyInstance) {
summary: 'Delete scope1and2',
description: 'Deletes a scope1and2 by id',
tags: getTags('Emissions'),
params: garboEntitySchema,
params: garboEntityIdSchema,
response: {
204: emptyBodySchema,
},
@@ -229,7 +229,7 @@ export async function companyDeleteRoutes(app: FastifyInstance) {
summary: 'Delete scope2',
description: 'Deletes a scope2 by id',
tags: getTags('Emissions'),
params: garboEntitySchema,
params: garboEntityIdSchema,
response: {
204: emptyBodySchema,
},
@@ -252,7 +252,7 @@ export async function companyDeleteRoutes(app: FastifyInstance) {
summary: 'Delete scope3',
description: 'Deletes a scope3 by id',
tags: getTags('Emissions'),
params: garboEntitySchema,
params: garboEntityIdSchema,
response: {
204: emptyBodySchema,
},
@@ -275,7 +275,7 @@ export async function companyDeleteRoutes(app: FastifyInstance) {
summary: 'Delete a scope3 category',
description: 'Deletes a scope3 category by id',
tags: getTags('Emissions'),
params: garboEntitySchema,
params: garboEntityIdSchema,
response: {
204: emptyBodySchema,
},
4 changes: 2 additions & 2 deletions src/api/routes/company.goals.ts
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ import {
postGoalSchema,
postGoalsSchema,
okResponseSchema,
garboEntitySchema,
garboEntityIdSchema,
} from '../schemas'
import {
PostGoalsBody,
@@ -65,7 +65,7 @@ export async function companyGoalsRoutes(app: FastifyInstance) {
summary: 'Update company goal',
description: 'Update a goal for a company',
tags: getTags('Goals'),
params: garboEntitySchema,
params: garboEntityIdSchema,
body: postGoalSchema,
response: {
200: okResponseSchema,
4 changes: 2 additions & 2 deletions src/api/routes/company.initiatives.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ import {
okResponseSchema,
postInitiativeSchema,
postInitiativesSchema,
garboEntitySchema,
garboEntityIdSchema,
} from '../schemas'
import { initiativeService } from '../services/initiativeService'
import {
@@ -64,7 +64,7 @@ export async function companyInitiativesRoutes(app: FastifyInstance) {
summary: 'Update a company initiative',
description: 'Update an existing initiative for a company',
tags: getTags('Initiatives'),
params: garboEntitySchema,
params: garboEntityIdSchema,
body: postInitiativeSchema,
response: {
200: okResponseSchema,
293 changes: 10 additions & 283 deletions src/api/routes/company.read.ts
Original file line number Diff line number Diff line change
@@ -5,46 +5,13 @@ import { getGics } from '../../lib/gics'
import { GarboAPIError } from '../../lib/garbo-api-error'
import { prisma } from '../../lib/prisma'
import { getTags } from '../../config/openapi'
import { wikidataIdParamSchema, CompanyList, CompanyDetails } from '../schemas'
import { WikidataIdParams } from '../types'
import { cachePlugin } from '../plugins/cache'
import { z } from 'zod'

const metadata = {
orderBy: {
updatedAt: 'desc' as const,
},
take: 1,
select: {
comment: true,
source: true,
updatedAt: true,
user: {
select: {
name: true,
},
},
verifiedBy: {
select: {
name: true,
},
},
},
}

const minimalMetadata = {
orderBy: {
updatedAt: 'desc' as const,
},
take: 1,
select: {
verifiedBy: {
select: {
name: true,
},
},
},
}
import { companyListArgs, detailedCompanyArgs } from '../args'
import { CompanyList } from '../schemas'
import { wikidataIdParamSchema } from '../schemas'
import { CompanyDetails } from '../schemas'
import { emptyBodySchema } from '../schemas'

function isNumber(n: unknown): n is number {
return Number.isFinite(n)
@@ -167,111 +134,7 @@ export async function companyReadRoutes(app: FastifyInstance) {
},
async (request, reply) => {
try {
const companies = await prisma.company.findMany({
select: {
wikidataId: true,
name: true,
description: true,
reportingPeriods: {
select: {
startDate: true,
endDate: true,
reportURL: true,
economy: {
select: {
turnover: {
select: {
value: true,
currency: true,
metadata: minimalMetadata,
},
},
employees: {
select: {
value: true,
unit: true,
metadata: minimalMetadata,
},
},
},
},
emissions: {
select: {
scope1: {
select: {
total: true,
unit: true,
metadata: minimalMetadata,
},
},
scope2: {
select: {
lb: true,
mb: true,
unknown: true,
unit: true,
metadata: minimalMetadata,
},
},
scope3: {
select: {
statedTotalEmissions: {
select: {
total: true,
unit: true,
metadata: minimalMetadata,
},
},
categories: {
select: {
category: true,
total: true,
unit: true,
metadata: minimalMetadata,
},
orderBy: {
category: 'asc',
},
},
metadata: minimalMetadata,
},
},
scope1And2: {
select: {
total: true,
unit: true,
metadata: minimalMetadata,
},
},
statedTotalEmissions: {
select: {
total: true,
unit: true,
metadata,
},
},
},
},
},
orderBy: {
startDate: 'desc',
},
},
industry: {
select: {
industryGics: {
select: {
sectorCode: true,
groupCode: true,
industryCode: true,
subIndustryCode: true,
},
},
metadata: minimalMetadata,
},
},
},
})
const companies = await prisma.company.findMany(companyListArgs)

const transformedCompanies = addCalculatedTotalEmissions(
companies.map(transformMetadata)
@@ -297,7 +160,8 @@ export async function companyReadRoutes(app: FastifyInstance) {
tags: getTags('Companies'),
params: wikidataIdParamSchema,
response: {
200: z.union([CompanyDetails, z.null()]),
200: CompanyDetails,
404: emptyBodySchema,
},
},
},
@@ -306,151 +170,14 @@ export async function companyReadRoutes(app: FastifyInstance) {
const { wikidataId } = request.params

const company = await prisma.company.findFirst({
...detailedCompanyArgs,
where: {
wikidataId,
},
select: {
wikidataId: true,
name: true,
description: true,
reportingPeriods: {
select: {
startDate: true,
endDate: true,
reportURL: true,
economy: {
select: {
turnover: {
select: {
value: true,
currency: true,
metadata,
},
},
employees: {
select: {
value: true,
unit: true,
metadata,
},
},
},
},
emissions: {
select: {
scope1: {
select: {
total: true,
unit: true,
metadata,
},
},
scope2: {
select: {
lb: true,
mb: true,
unknown: true,
unit: true,
metadata,
},
},
scope3: {
select: {
statedTotalEmissions: {
select: {
total: true,
unit: true,
metadata,
},
},
categories: {
select: {
category: true,
total: true,
unit: true,
metadata,
},
orderBy: {
category: 'asc',
},
},
metadata,
},
},
biogenicEmissions: {
select: {
total: true,
unit: true,
metadata,
},
},
scope1And2: {
select: {
total: true,
unit: true,
metadata,
},
},
statedTotalEmissions: {
select: {
total: true,
unit: true,
metadata,
},
},
},
},
metadata,
},
orderBy: {
startDate: 'desc',
},
},
industry: {
select: {
industryGics: {
select: {
sectorCode: true,
groupCode: true,
industryCode: true,
subIndustryCode: true,
},
},
metadata,
},
},
goals: {
select: {
id: true,
description: true,
year: true,
baseYear: true,
target: true,
metadata,
},
orderBy: {
year: 'desc',
},
},
initiatives: {
select: {
id: true,
title: true,
description: true,
year: true,
scope: true,
metadata,
},
orderBy: {
year: 'desc',
},
},
},
})

if (!company) {
reply.send(null)
return
return reply.status(404).send()
}

const [transformedCompany] = addCalculatedTotalEmissions([
14 changes: 14 additions & 0 deletions src/api/schemas/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { z } from 'zod'

export const wikidataIdSchema = z.string().regex(/Q\d+/)

export const wikidataIdParamSchema = z.object({ wikidataId: wikidataIdSchema })

export const garboEntityIdSchema = z.object({ id: z.string() })

/**
* This allows reporting periods like 2022-2023
*/
export const yearSchema = z.string().regex(/\d{4}(?:-\d{4})?/)

export const yearParamSchema = z.object({ year: yearSchema })
3 changes: 3 additions & 0 deletions src/api/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './common'
export * from './request'
export * from './response'
168 changes: 168 additions & 0 deletions src/api/schemas/request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { z } from 'zod'
import { wikidataIdSchema } from './common'

const createMetadataSchema = z.object({
metadata: z
.object({
source: z.string().optional(),
comment: z.string().optional(),
})
.optional(),
})

export const postCompanyBodySchema = z.object({
wikidataId: wikidataIdSchema,
name: z.string(),
description: z.string().optional(),
url: z.string().url().optional(),
internalComment: z.string().optional(),
tags: z.array(z.string()).optional(),
})

export const reportingPeriodBodySchema = z
.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
reportURL: z.string().optional(),
})
.refine(({ startDate, endDate }) => startDate.getTime() < endDate.getTime(), {
message: 'startDate must be earlier than endDate',
})

export const goalSchema = z.object({
description: z.string(),
year: z.string().optional(),
target: z.number().optional(),
baseYear: z.string().optional(),
})

export const postGoalSchema = z
.object({
goal: goalSchema,
})
.merge(createMetadataSchema)

export const postGoalsSchema = z
.object({
goals: z.array(goalSchema),
})
.merge(createMetadataSchema)

export const initiativeSchema = z.object({
title: z.string(),
description: z.string().optional(),
year: z.string().optional(),
scope: z.string().optional(),
})

export const postInitiativeSchema = z
.object({ initiative: initiativeSchema })
.merge(createMetadataSchema)

export const postInitiativesSchema = z
.object({
initiatives: z.array(initiativeSchema),
})
.merge(createMetadataSchema)

export const industrySchema = z.object({
subIndustryCode: z.string(),
})

export const postIndustrySchema = z
.object({ industry: industrySchema })
.merge(createMetadataSchema)

export const statedTotalEmissionsSchema = z
.object({ total: z.number() })
.optional()

export const emissionsSchema = z
.object({
scope1: z
.object({
total: z.number(),
})
.optional(),
scope2: z
.object({
mb: z
.number({ description: 'Market-based scope 2 emissions' })
.optional(),
lb: z
.number({ description: 'Location-based scope 2 emissions' })
.optional(),
unknown: z
.number({ description: 'Unspecified Scope 2 emissions' })
.optional(),
})
.refine(
({ mb, lb, unknown }) =>
mb !== undefined || lb !== undefined || unknown !== undefined,
{
message:
'At least one property of `mb`, `lb` and `unknown` must be defined if scope2 is provided',
}
)
.optional(),
scope3: z
.object({
categories: z
.array(
z.object({
category: z.number().int().min(1).max(16),
total: z.number(),
})
)
.optional(),
statedTotalEmissions: statedTotalEmissionsSchema,
})
.optional(),
biogenic: z.object({ total: z.number() }).optional(),
statedTotalEmissions: statedTotalEmissionsSchema,
scope1And2: z.object({ total: z.number() }).optional(),
})
.optional()

export const economySchema = z
.object({
turnover: z
.object({
value: z.number().optional(),
currency: z.string().optional(),
})
.optional(),
employees: z
.object({
value: z.number().optional(),
unit: z.string().optional(),
})
.optional(),
})
.optional()

export const postEconomySchema = z.object({
economy: economySchema,
})

export const postEmissionsSchema = z.object({
emissions: emissionsSchema,
})

export const reportingPeriodSchema = z
.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
reportURL: z.string().optional(),
emissions: emissionsSchema,
economy: economySchema,
})
.refine(({ startDate, endDate }) => startDate.getTime() < endDate.getTime(), {
message: 'startDate must be earlier than endDate',
})

export const postReportingPeriodsSchema = z
.object({
reportingPeriods: z.array(reportingPeriodSchema),
})
.merge(createMetadataSchema)
215 changes: 34 additions & 181 deletions src/api/schemas.ts → src/api/schemas/response.ts
Original file line number Diff line number Diff line change
@@ -1,191 +1,14 @@
import { z } from 'zod'
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'
import { wikidataIdSchema } from './common'

extendZodWithOpenApi(z)

export const wikidataIdSchema = z.string().regex(/Q\d+/)

export const wikidataIdParamSchema = z.object({ wikidataId: wikidataIdSchema })

export const garboEntitySchema = z.object({ id: z.string() })

/**
* This allows reporting periods like 2022-2023
*/
export const yearSchema = z.string().regex(/\d{4}(?:-\d{4})?/)

export const yearParamSchema = z.object({ year: yearSchema })

const createMetadataSchema = z.object({
metadata: z
.object({
source: z.string().optional(),
comment: z.string().optional(),
})
.optional(),
})

export const postCompanyBodySchema = z.object({
wikidataId: wikidataIdSchema,
name: z.string(),
description: z.string().optional(),
url: z.string().url().optional(),
internalComment: z.string().optional(),
tags: z.array(z.string()).optional(),
})

export const reportingPeriodBodySchema = z
.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
reportURL: z.string().optional(),
})
.refine(({ startDate, endDate }) => startDate.getTime() < endDate.getTime(), {
message: 'startDate must be earlier than endDate',
})

export const goalSchema = z.object({
description: z.string(),
year: z.string().optional(),
target: z.number().optional(),
baseYear: z.string().optional(),
})

export const postGoalSchema = z
.object({
goal: goalSchema,
})
.merge(createMetadataSchema)

export const postGoalsSchema = z
.object({
goals: z.array(goalSchema),
})
.merge(createMetadataSchema)

export const initiativeSchema = z.object({
title: z.string(),
description: z.string().optional(),
year: z.string().optional(),
scope: z.string().optional(),
})

export const postInitiativeSchema = z
.object({ initiative: initiativeSchema })
.merge(createMetadataSchema)

export const postInitiativesSchema = z
.object({
initiatives: z.array(initiativeSchema),
})
.merge(createMetadataSchema)

export const industrySchema = z.object({
subIndustryCode: z.string(),
})

export const postIndustrySchema = z
.object({ industry: industrySchema })
.merge(createMetadataSchema)

export const statedTotalEmissionsSchema = z
.object({ total: z.number() })
.optional()

export const emissionsSchema = z
.object({
scope1: z
.object({
total: z.number(),
})
.optional(),
scope2: z
.object({
mb: z
.number({ description: 'Market-based scope 2 emissions' })
.optional(),
lb: z
.number({ description: 'Location-based scope 2 emissions' })
.optional(),
unknown: z
.number({ description: 'Unspecified Scope 2 emissions' })
.optional(),
})
.refine(
({ mb, lb, unknown }) =>
mb !== undefined || lb !== undefined || unknown !== undefined,
{
message:
'At least one property of `mb`, `lb` and `unknown` must be defined if scope2 is provided',
}
)
.optional(),
scope3: z
.object({
categories: z
.array(
z.object({
category: z.number().int().min(1).max(16),
total: z.number(),
})
)
.optional(),
statedTotalEmissions: statedTotalEmissionsSchema,
})
.optional(),
biogenic: z.object({ total: z.number() }).optional(),
statedTotalEmissions: statedTotalEmissionsSchema,
scope1And2: z.object({ total: z.number() }).optional(),
})
.optional()

export const economySchema = z
.object({
turnover: z
.object({
value: z.number().optional(),
currency: z.string().optional(),
})
.optional(),
employees: z
.object({
value: z.number().optional(),
unit: z.string().optional(),
})
.optional(),
})
.optional()

export const postEconomySchema = z.object({
economy: economySchema,
})

export const postEmissionsSchema = z.object({
emissions: emissionsSchema,
})

export const reportingPeriodSchema = z
.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
reportURL: z.string().optional(),
emissions: emissionsSchema,
economy: economySchema,
})
.refine(({ startDate, endDate }) => startDate.getTime() < endDate.getTime(), {
message: 'startDate must be earlier than endDate',
})

export const postReportingPeriodsSchema = z
.object({
reportingPeriods: z.array(reportingPeriodSchema),
})
.merge(createMetadataSchema)

export const okResponseSchema = z.object({ ok: z.boolean() })
export const emptyBodySchema = z.undefined()

export const MetadataSchema = z.object({
id: z.string(),
comment: z
.string()
.nullable()
@@ -226,24 +49,28 @@ const CompanyBaseSchema = z.object({
})

export const StatedTotalEmissionsSchema = z.object({
id: z.string(),
total: z.number().openapi({ description: 'Total emissions value' }),
unit: z.string().openapi({ description: 'Unit of measurement' }),
metadata: MetadataSchema,
})

export const BiogenicSchema = z.object({
id: z.string(),
total: z.number().openapi({ description: 'Total biogenic emissions' }),
unit: z.string().openapi({ description: 'Unit of measurement' }),
metadata: MetadataSchema,
})

export const Scope1Schema = z.object({
id: z.string(),
total: z.number().openapi({ description: 'Total scope 1 emissions' }),
unit: z.string().openapi({ description: 'Unit of measurement' }),
metadata: MetadataSchema,
})

export const Scope2BaseSchema = z.object({
id: z.string(),
mb: z
.number()
.nullable()
@@ -278,6 +105,7 @@ const withScope2Refinement = <T extends z.ZodRawShape>(
export const Scope2Schema = withScope2Refinement(Scope2BaseSchema)

export const Scope3CategorySchema = z.object({
id: z.string(),
category: z
.number()
.int()
@@ -292,6 +120,7 @@ export const Scope3CategorySchema = z.object({
})

export const Scope3Schema = z.object({
id: z.string(),
categories: z.array(Scope3CategorySchema),
statedTotalEmissions: StatedTotalEmissionsSchema.nullable(),
calculatedTotalEmissions: z
@@ -301,12 +130,14 @@ export const Scope3Schema = z.object({
})

export const Scope1And2Schema = z.object({
id: z.string(),
total: z.number(),
unit: z.string(),
metadata: MetadataSchema,
})

export const EmissionsSchema = z.object({
id: z.string(),
scope1: Scope1Schema.nullable(),
scope2: Scope2Schema.nullable(),
scope3: Scope3Schema.nullable(),
@@ -319,18 +150,21 @@ export const EmissionsSchema = z.object({
})

export const TurnoverSchema = z.object({
id: z.string(),
value: z.number().nullable().openapi({ description: 'Turnover value' }),
currency: z.string().nullable().openapi({ description: 'Currency code' }),
metadata: MetadataSchema,
})

export const EmployeesSchema = z.object({
id: z.string(),
value: z.number().nullable().openapi({ description: 'Number of employees' }),
unit: z.string().nullable().openapi({ description: 'Unit of measurement' }),
metadata: MetadataSchema,
})

export const EconomySchema = z.object({
id: z.string(),
turnover: TurnoverSchema.nullable(),
employees: EmployeesSchema.nullable(),
})
@@ -364,6 +198,7 @@ export const MinimalIndustryGicsSchema = IndustryGicsSchema.omit({
})

export const IndustrySchema = z.object({
id: z.string(),
industryGics: IndustryGicsSchema,
metadata: MetadataSchema,
})
@@ -374,6 +209,7 @@ export const MinimalIndustrySchema = z.object({
})

export const GoalSchema = z.object({
id: z.string(),
description: z.string().openapi({ description: 'Goal description' }),
year: z.string().nullable().openapi({ description: 'Target year' }),
baseYear: z.string().nullable().openapi({ description: 'Base year' }),
@@ -382,6 +218,7 @@ export const GoalSchema = z.object({
})

export const InitiativeSchema = z.object({
id: z.string(),
title: z.string().openapi({ description: 'Initiative title' }),
description: z
.string()
@@ -393,6 +230,7 @@ export const InitiativeSchema = z.object({
})

export const ReportingPeriodSchema = z.object({
id: z.string(),
startDate: z
.date()
.openapi({ description: 'Start date of reporting period' }),
@@ -405,15 +243,22 @@ export const ReportingPeriodSchema = z.object({
economy: EconomySchema.nullable(),
})

const MinimalTurnoverSchema = TurnoverSchema.omit({ metadata: true }).extend({
const MinimalTurnoverSchema = TurnoverSchema.omit({
id: true,
metadata: true,
}).extend({
metadata: MinimalMetadataSchema,
})

const MinimalEmployeeSchema = EmployeesSchema.omit({ metadata: true }).extend({
const MinimalEmployeeSchema = EmployeesSchema.omit({
id: true,
metadata: true,
}).extend({
metadata: MinimalMetadataSchema,
})

const MinimalEconomySchema = EconomySchema.omit({
id: true,
employees: true,
turnover: true,
}).extend({
@@ -422,24 +267,29 @@ const MinimalEconomySchema = EconomySchema.omit({
})

const MinimalScope1Schema = Scope1Schema.omit({
id: true,
metadata: true,
}).extend({ metadata: MinimalMetadataSchema })

const MinimalScope2Schema = withScope2Refinement(
Scope2BaseSchema.omit({
id: true,
metadata: true,
}).extend({ metadata: MinimalMetadataSchema })
)

const MinimalStatedTotalEmissionsSchema = StatedTotalEmissionsSchema.omit({
id: true,
metadata: true,
}).extend({ metadata: MinimalMetadataSchema })

const MinimalScope3CategorySchema = Scope3CategorySchema.omit({
id: true,
metadata: true,
}).extend({ metadata: MinimalMetadataSchema })

const MinimalScope3Schema = Scope3Schema.omit({
id: true,
metadata: true,
categories: true,
statedTotalEmissions: true,
@@ -450,10 +300,12 @@ const MinimalScope3Schema = Scope3Schema.omit({
})

const MinimalScope1And2Schema = Scope1And2Schema.omit({
id: true,
metadata: true,
}).extend({ metadata: MinimalMetadataSchema })

const MinimalEmissionsSchema = EmissionsSchema.omit({
id: true,
scope1: true,
scope2: true,
scope3: true,
@@ -469,6 +321,7 @@ const MinimalEmissionsSchema = EmissionsSchema.omit({
})

const MinimalReportingPeriodSchema = ReportingPeriodSchema.omit({
id: true,
emissions: true,
economy: true,
}).extend({
5 changes: 3 additions & 2 deletions src/api/services/companyService.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Economy, Employees, Metadata, Turnover } from '@prisma/client'

import { OptionalNullable } from '../../lib/type-utils'
import { DefaultEconomyArgs, economyArgs } from '../types'
import { DefaultEconomyType } from '../types'
import { prisma } from '../../lib/prisma'
import { GarboAPIError } from '../../lib/garbo-api-error'
import { economyArgs } from '../args'

class CompanyService {
async getCompany(wikidataId: string) {
@@ -107,7 +108,7 @@ class CompanyService {
employees,
metadata,
}: {
economy: DefaultEconomyArgs
economy: DefaultEconomyType
employees: OptionalNullable<
Omit<Employees, 'id' | 'metadataId' | 'economyId'>
>
3 changes: 2 additions & 1 deletion src/api/services/emissionsService.ts
Original file line number Diff line number Diff line change
@@ -9,9 +9,10 @@ import {
StatedTotalEmissions,
} from '@prisma/client'
import { OptionalNullable } from '../../lib/type-utils'
import { DefaultEmissions, emissionsArgs } from '../types'
import { DefaultEmissions } from '../types'
import { prisma } from '../../lib/prisma'
import { GarboAPIError } from '../../lib/garbo-api-error'
import { emissionsArgs } from '../args'

const TONNES_CO2_UNIT = 'tCO2e'

2 changes: 1 addition & 1 deletion src/api/services/reportingPeriodService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Company, Prisma, ReportingPeriod } from '@prisma/client'
import { reportingPeriodArgs } from '../types'
import { prisma } from '../../lib/prisma'
import { GarboAPIError } from '../../lib/garbo-api-error'
import { reportingPeriodArgs } from '../args'

class ReportingPeriodService {
async upsertReportingPeriod(
45 changes: 3 additions & 42 deletions src/api/types.ts
Original file line number Diff line number Diff line change
@@ -2,52 +2,13 @@ import { Prisma } from '@prisma/client'
import { z } from 'zod'

import * as schemas from './schemas'
import { economyArgs, emissionsArgs, reportingPeriodArgs } from './args'

export type WikidataIdParams = z.infer<typeof schemas.wikidataIdParamSchema>

export const emissionsArgs = {
include: {
scope1: { select: { id: true } },
scope2: { select: { id: true } },
scope3: { select: { id: true } },
biogenicEmissions: { select: { id: true } },
scope1And2: { select: { id: true } },
statedTotalEmissions: { select: { id: true } },
},
} satisfies Prisma.EmissionsDefaultArgs

export type DefaultEmissions = Prisma.EmissionsGetPayload<typeof emissionsArgs>

export const economyArgs = {
include: {
employees: { select: { id: true } },
turnover: { select: { id: true } },
},
} satisfies Prisma.EconomyDefaultArgs

export type DefaultEconomyArgs = Prisma.EconomyGetPayload<typeof economyArgs>

export const reportingPeriodArgs = {
include: {
emissions: {
include: {
biogenicEmissions: { select: { id: true } },
scope1: { select: { id: true } },
scope1And2: { select: { id: true } },
scope2: { select: { id: true } },
scope3: { select: { id: true } },
statedTotalEmissions: { select: { id: true } },
},
},
economy: {
include: {
employees: { select: { id: true } },
turnover: { select: { id: true } },
},
},
company: { select: { wikidataId: true } },
},
} satisfies Prisma.ReportingPeriodDefaultArgs
export type DefaultEconomyType = Prisma.EconomyGetPayload<typeof economyArgs>

export type DefaultReportingPeriod = Prisma.ReportingPeriodGetPayload<
typeof reportingPeriodArgs
@@ -64,4 +25,4 @@ export type PostReportingPeriodsBody = z.infer<
>
export type PostCompanyBody = z.infer<typeof schemas.postCompanyBodySchema>

export type GarboEntityId = z.infer<typeof schemas.garboEntitySchema>
export type GarboEntityId = z.infer<typeof schemas.garboEntityIdSchema>
13 changes: 8 additions & 5 deletions src/prompts/followUp/economy.ts
Original file line number Diff line number Diff line change
@@ -8,17 +8,20 @@ export const schema = z.object({
.object({
turnover: z
.object({
value: z.number().optional(),
currency: z.string().optional(),
value: z.number().nullable().optional(),
currency: z.string().nullable().optional(),
})
.nullable()
.optional(),
employees: z
.object({
value: z.number().optional(),
unit: z.string().optional(),
value: z.number().nullable().optional(),
unit: z.string().nullable().optional(),
})
.nullable()
.optional(),
})
.nullable()
.optional(),
})
),
@@ -28,7 +31,7 @@ export const schema = z.object({

export const prompt = `
*** Golden Rule ***
- Extract values only if explicitly available in the context. Do not infer or create data. Leave optional fields absent if no data is provided.
- Extract values only if explicitly available in the context. Do not infer or create data. Leave optional fields absent or explicitly set to null if no data is provided.
*** Turnover ***
- Extract turnover as a numerical value. Use the turnover field to specify the turnover (intäkter, omsättning) of the company. If the currency is not specified, assume SEK.
Be as accurate as possible. Extract this data for all available years.
7 changes: 6 additions & 1 deletion src/prompts/followUp/scope12.ts
Original file line number Diff line number Diff line change
@@ -8,17 +8,21 @@ const schema = z.object({
.object({
total: z.number(),
})
.nullable()
.optional(),
scope2: z
.object({
mb: z
.number({ description: 'Market-based scope 2 emissions' })
.nullable()
.optional(),
lb: z
.number({ description: 'Location-based scope 2 emissions' })
.nullable()
.optional(),
unknown: z
.number({ description: 'Unspecified Scope 2 emissions' })
.nullable()
.optional(),
})
.refine(
@@ -29,14 +33,15 @@ const schema = z.object({
'At least one property of `mb`, `lb` and `unknown` must be defined if scope2 is provided',
}
)
.nullable()
.optional(),
})
),
})

const prompt = `
*** Golden Rule ***
- Extract values only if explicitly available in the context. Do not infer or create data. Leave optional fields absent if no data is provided.
- Extract values only if explicitly available in the context. Do not infer or create data. Leave optional fields absent or explicitly set to null if no data is provided.
Extract scope 1 and 2 emissions according to the GHG protocol (CO2e). Include all years you can find and never exclude the latest year.
Include market-based and location-based in scope 2.
9 changes: 7 additions & 2 deletions src/prompts/followUp/scope3.ts
Original file line number Diff line number Diff line change
@@ -13,9 +13,14 @@ export const schema = z.object({
total: z.number(),
})
)
.nullable()
.optional(),
statedTotalEmissions: z
.object({ total: z.number() })
.nullable()
.optional(),
statedTotalEmissions: z.object({ total: z.number() }).optional(),
})
.nullable()
.optional(),
})
),
@@ -48,7 +53,7 @@ Extract scope 3 emissions according to the GHG Protocol and organize them by yea
16: Other
2. **Missing Or Incomplete Data**:
If data is missing or unclear, explicitly report it as \`null\`. Do not make assumptions or attempt to infer missing values.
- Extract values only if explicitly available in the context. Do not infer or create data. Leave optional fields absent or explicitly set to null if no data is provided.
3. **Units**:
Report all emissions in metric tons of CO2 equivalent. If the data is provided in a different unit (kton = 1000 tCO2, Mton = 1000000 tCO2), convert it. This is the only permitted calculation.
29 changes: 28 additions & 1 deletion src/workers/saveToAPI.ts
Original file line number Diff line number Diff line change
@@ -36,9 +36,36 @@ export const saveToAPI = new DiscordWorker<SaveToApiJob>(
})
}

function removeNullValuesFromGarbo(data: any): any {
if (Array.isArray(data)) {
return data
.map((item) => removeNullValuesFromGarbo(item))
.filter((item) => item !== null && item !== undefined)
} else if (typeof data === 'object' && data !== null) {
const sanitizedObject = Object.entries(data).reduce(
(acc, [key, value]) => {
const sanitizedValue = removeNullValuesFromGarbo(value)
if (sanitizedValue !== null && sanitizedValue !== undefined) {
acc[key] = sanitizedValue
}
return acc
},
{} as Record<string, any>
)

return Object.keys(sanitizedObject).length > 0
? sanitizedObject
: null
} else {
return data
}
}

if (!requiresApproval || approved) {
console.log(`Saving approved data for ${wikidataId} to API`)
await apiFetch(`/companies/${wikidataId}/${apiSubEndpoint}`, { body })
await apiFetch(`/companies/${wikidataId}/${apiSubEndpoint}`, {
body: removeNullValuesFromGarbo(body),
})
return { success: true }
}

0 comments on commit 30461ad

Please sign in to comment.