Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Setup structure for simple three-tier architecture #42

Merged
merged 7 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 3 additions & 203 deletions app/api/route.ts
Original file line number Diff line number Diff line change
@@ -1,218 +1,18 @@
import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";
import { Calculation, calculationSchema } from "../schemas/calculationSchema";
import * as calculationService from "../services/calculationService";

const prisma = new PrismaClient();

// define and export the GET handler function
export async function POST(req: Request) {
try {
// Parse and validate user input
const data = await req.json();
const input: Calculation = calculationSchema.parse(data);

// data are going to be queried at different levels of granularity based on the postcode
const postcode = input.housePostcode;
const postcodeArea = postcode.area; // extract only the characters for the area, e.g SE
const postcodeDistrict = postcode.district; // extract only characters for the district, SE17
const postcodeSector = postcode.sector; // extract only the characters for the sector, SE17 1

// create the progressive queries
const minimumNumberPostcodes = 30; // minimum number of entries to create the average
let pricesPaid; // declare the variable for prices paid
let numberOfTransactions; // declare the variable for numbers of transactions retrieved
let granularityPostcode; // declare the granularity of the postcode
let averagePrice;

const pricesPaidSector = await prisma.pricesPaid.aggregate({
where: {
propertyType: {
equals: input.houseType,
},
postcode: {
startsWith: postcodeSector,
},
},
_count: {
id: true,
},
_avg: {
price: true,
},
});

const numberPerSector = pricesPaidSector._count.id;
const isMinMetBySector = numberPerSector >= minimumNumberPostcodes;

if (!isMinMetBySector) {
const pricesPaidDistrict = await prisma.pricesPaid.aggregate({
where: {
propertyType: {
equals: input.houseType,
},
postcode: {
startsWith: postcodeDistrict,
},
},
_count: {
id: true,
},
_avg: {
price: true,
},
});

const numberPerDistrict = pricesPaidDistrict._count.id;
const isMinMetByDistrict = numberPerDistrict >= minimumNumberPostcodes;

if (!isMinMetByDistrict) {
const pricesPaidArea = await prisma.pricesPaid.aggregate({
where: {
propertyType: {
equals: input.houseType,
},
postcode: {
startsWith: postcodeArea,
},
},
_count: {
id: true,
},
_avg: {
price: true,
},
});
const numberPerArea = pricesPaidArea._count.id;

pricesPaid = pricesPaidArea; // if condition is met, the granularity is appropriate
numberOfTransactions = numberPerArea; // check the granularity
granularityPostcode = postcodeArea; // granularity of the postcode when performing the average price search
averagePrice = pricesPaidArea._avg.price;
} else {
pricesPaid = pricesPaidDistrict; // if condition is met, the granularity is appropriate
numberOfTransactions = numberPerDistrict; // check the granularity
granularityPostcode = postcodeDistrict; // granularity of the postcode
averagePrice = pricesPaidDistrict._avg.price;
}
} else {
pricesPaid = pricesPaidSector; // if condition is met, the granularity is appropriate
numberOfTransactions = numberPerSector; // check the granularity
granularityPostcode = postcodeSector; // granularity of the postcode
averagePrice = pricesPaidSector._avg.price;
}

if (averagePrice === null) {
throw new Error("Unable to calculate average price");
}

const { priceMid: buildPrice } = await prisma.buildPrices.findFirstOrThrow({
where: {
houseType: { equals: input.houseType },
},
select: { priceMid: true },
});
// TODO: Make columns non-nullable
if (!buildPrice) throw Error("Missing buildPrice");

const { itl3 } = await prisma.itlLookup.findFirstOrThrow({
where: {
postcode: postcodeDistrict,
itl3: {
not: null,
},
},
select: {
itl3: true,
},
});
if (!itl3) throw Error("Missing itl3");

const { gdhi2020: gdhi } = await prisma.gDHI.findFirstOrThrow({
where: {
itl3: { equals: itl3 },
},
select: { gdhi2020: true },
});
if (!gdhi) throw Error("Missing gdhi");

const {
_avg: { monthlyMeanRent: averageRentMonthly },
} = await prisma.rent.aggregate({
where: { itl3 },
_avg: {
monthlyMeanRent: true,
},
});
if (!averageRentMonthly) throw Error("Missing averageRentMonthly");

const socialRentAdjustments = await prisma.socialRentAdjustments.findMany();
const itl3Prefix = itl3.substring(0, 4);

const {
_avg: { earningsPerWeek: socialRentAveEarning },
} = await prisma.socialRent.aggregate({
where: {
itl3: {
startsWith: itl3Prefix,
},
},
_avg: {
earningsPerWeek: true,
},
});

if (!socialRentAveEarning) throw Error("Missing socialRentAveEarning");

const {
_avg: { hpi2020: averageHpi },
} = await prisma.hPI.aggregate({
where: {
itl3: {
endsWith: itl3,
},
},
_avg: {
hpi2020: true,
},
});
if (!averageHpi) throw Error("Missing averageHpi");

const { bill: gasBillYearly } = await prisma.gasBills.findFirstOrThrow({
where: {
itl: {
startsWith: itl3.substring(0, 3),
},
},
select: {
bill: true,
},
});
if (!gasBillYearly) throw Error("Missing gasBillYearly");

return NextResponse.json({
postcode: input.housePostcode,
houseType: input.houseType,
houseAge: input.houseAge,
houseBedrooms: input.houseBedrooms,
houseSize: input.houseSize,
averagePrice: parseFloat(averagePrice.toFixed(2)),
itl3,
gdhi,
hpi: averageHpi,
buildPrice,
averageRentMonthly,
socialRentAdjustments,
socialRentAveEarning,
numberOfTransactions,
granularityPostcode,
pricesPaid,
gasBillYearly,
});
const householdData = await calculationService.getHouseholdData(input);
return NextResponse.json(householdData);
Comment on lines +11 to +12
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This POST() function is now much simpler, we only care about handing HTTP input and output etc.

} catch (err) {
console.log("ERROR: API - ", (err as Error).message);
const response = { error: (err as Error).message };
return NextResponse.json(response, { status: 500 });
} finally {
await prisma.$disconnect();
}
}
15 changes: 15 additions & 0 deletions app/data/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { PrismaClient } from "@prisma/client";

const prismaClientSingleton = () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was reading about the Singleton pattern 'cause I hadn't encountered it before--to ensure that the client is truly only created once, would we need some sort of if-then check here to only instantiate if there is no PrismaClient currently in existence?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep that's exactly correct 🙌

This is happening below on line 11 - when we import prisma we check if one exists, or instantiate a new one.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I missed that--still getting used to the reviews GUI 😅 thanks!

return new PrismaClient();
};

declare const globalThis: {
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;

const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();

export default prisma;

if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma;
26 changes: 26 additions & 0 deletions app/data/gdhiRepo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import prisma from "./db";

const getGDHI2020ByITL3 = async (
itl3: string
): Promise<number> => {
try {
const { gdhi2020 } = await prisma.gDHI.findFirstOrThrow({
where: {
AND: {
itl3: { equals: itl3 },
// TODO: Add `NOT NULL` constraint to column
gdhi2020: { not: null }
},
},
select: { gdhi2020: true },
});

return gdhi2020 as number;
} catch (error) {
throw Error(`Data error: Unable to find gdhi2020 for itl3 ${itl3}`);
}
};

export const gdhiRepo = {
getGDHI2020ByITL3,
}
28 changes: 28 additions & 0 deletions app/data/itlRepo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import prisma from "./db";

const getItl3ByPostcodeDistrict = async (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this "layer" we're only concerned about fetching data from the DB, and handing the correct value to consumers.

postcodeDistrict: string
): Promise<string> => {
try {
const { itl3 } = await prisma.itlLookup.findFirstOrThrow({
where: {
postcode: postcodeDistrict,
itl3: {
not: null,
},
},
select: {
itl3: true,
},
});

// Cast to string as 'not: null' clause in Prisma query does not type narrow
return itl3 as string;
} catch (error) {
throw new Error(`Data error: Unable get get itl3 for postcode district ${postcodeDistrict}`);
}
};

export const itlRepo = {
getItl3ByPostcodeDistrict,
}
Loading
Loading