Skip to content

Commit

Permalink
Merge pull request #58 from asibs/jms/democracy_club_lookup
Browse files Browse the repository at this point in the history
Jms/democracy club lookup
  • Loading branch information
jms301 authored Mar 9, 2024
2 parents 46623a5 + 07b1b6d commit bd65914
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 42 deletions.
107 changes: 100 additions & 7 deletions app/api/constituency_lookup/[postcode]/[[...address]]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,62 @@ import { validatePostcode, normalizePostcode } from "@/utils/Postcodes";

// Force using 'nodejs' rather than 'edge' - edge won't have the filesystem containing SQLite
export const runtime = "nodejs";
export const revalidate = 3600;

type DCData = {
boundary_changes?: {
current_constituencies_official_identifier: string;
current_constituencies_name: string;
new_constituencies_official_identifier: string;
new_constituencies_name: string;
CHANGE_TYPE: string;
};
addresses?: {
address: string;
postcode: string;
slug: string;
url: string;
}[];
};

const dc_base_url = "https://developers.democracyclub.org.uk/api/v1/";
const dc_params =
"/?" +
new URLSearchParams({
auth_token: process.env.DC_API_KEY || "",
parl_boundaries: "1",
}).toString();

async function fetch_dc_api(
postcode: string,
addressSlug?: string,
): Promise<DCData | null> {
const dc_url =
dc_base_url +
(addressSlug ? "address/" + addressSlug : "postcode/" + postcode) +
dc_params;

const dc_res = await fetch(dc_url, { signal: AbortSignal.timeout(5000) });

if (dc_res.ok) {
const dc_json = await dc_res.json();
if (
!addressSlug &&
dc_json.address_picker &&
dc_json.addresses.length > 0
) {
return { addresses: dc_json.addresses };
} else if (dc_json.parl_boundary_changes) {
return { boundary_changes: dc_json.parl_boundary_changes };
} else {
console.log("DC No Useful Response", await dc_res.json());
return null;
}
} else {
console.log("DC ERROR", await dc_res.text());
return null;
}
}

// Declare the DB outside the func & lazy-load, so it can be cached across calls
let db: Database | null = null;
Expand Down Expand Up @@ -52,8 +108,9 @@ export async function GET(

// Query the database
console.time("query-postcode-database");
const constituencies = await db.all(
const db_constituencies = await db.all(
`SELECT
pcon.gss,
pcon.slug,
pcon.name
FROM postcode_lookup
Expand All @@ -64,7 +121,7 @@ export async function GET(
);
console.timeEnd("query-postcode-database");

if (!constituencies || constituencies.length == 0) {
if (!db_constituencies || db_constituencies.length == 0) {
console.log(`Postcode ${normalizedPostcode} not found in DB!`);
const response: ConstituencyLookupResponse = {
postcode: params.postcode,
Expand All @@ -75,25 +132,61 @@ export async function GET(
return NextResponse.json(response);
}

if (constituencies.length == 1) {
if (db_constituencies.length == 1) {
console.log(`Single constituency found for postcode ${normalizedPostcode}`);
const response: ConstituencyLookupResponse = {
postcode: params.postcode,
addressSlug: params.address?.[0],
constituencies: constituencies,
constituencies: db_constituencies,
};
return NextResponse.json(response);
} else {
// TODO: Use DemocracyClub API to lookup postcode and populate the addresses array, so users can select their
// specific address, rather than us expecting to know (or find out) their constituency.
console.log(
`Multiple constituencies found for postcode ${normalizedPostcode}`,
);

const response: ConstituencyLookupResponse = {
postcode: params.postcode,
addressSlug: params.address?.[0],
constituencies: constituencies,
constituencies: db_constituencies,
};

//see if DC api will give us anything useful
//TODO remove this if statement, (here so we can test DC API failure!)
let dc_data = null;
if (normalizedPostcode != "DE30GU")
dc_data = await fetch_dc_api(normalizedPostcode, params.address?.[0]);

//const dc_data = await fetch_dc_api(normalizedPostcode, params.address?.[0]);

// See if DC data can return a more useful response.
// Possibly it just matches 1 constituency in which case return that
// Possibly it gives us an address picker which we can return.
if (dc_data?.boundary_changes) {
//Democracy Club data shows this postcode has a single constituency
const gss =
dc_data.boundary_changes.new_constituencies_official_identifier.substring(
4,
);
const dc_constituency = db_constituencies.filter((c) => c.gss === gss);

if (dc_constituency.length === 1) {
response.constituencies = dc_constituency;
} else {
//TODO some way to get vercel to notify us of this error since it shows
//a problem in our or DC data (once DC have fixed their NI & Scottish codes)
console.error(JSON.stringify(dc_data));
console.error(
"DC returned boundary change data but it didn't match a single db record??",
);
}
} else if (dc_data?.addresses && dc_data.addresses.length > 0) {
response.addresses = dc_data.addresses.map((addr) => ({
name: addr.address,
slug: addr.slug,
}));
}

return NextResponse.json(response);
}
}
118 changes: 83 additions & 35 deletions components/constituency_lookup/ConstituencyLookup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,11 @@ const throttledApi = async (
postcode: string,
addressSlug?: string,
): Promise<ConstituencyLookupResponse | null> => {
//TODO handle address lookups in the cache if/when we use DemoClub API
if (lookupCache.hasOwnProperty(postcode)) {
const cached = await lookupCache[postcode];
addressSlug = addressSlug || "";
const cacheKey = postcode + addressSlug;

if (!addressSlug && lookupCache.hasOwnProperty(postcode)) {
const cached = await lookupCache[cacheKey];
if (cached) {
return cached;
}
Expand All @@ -112,16 +114,15 @@ const throttledApi = async (
lookupCache[reqControl.lastLookup] = null;
}

// TODO handle address lookups in the cache.
reqControl.lastLookup = postcode;
reqControl.lastLookup = cacheKey;

if (reqControl.time + reqControl.rateLimit < Date.now()) {
//Last request was more than rate limit ago.
reqControl.time = Date.now();
lookupCache[postcode] = fetchApi(postcode, addressSlug);
lookupCache[cacheKey] = fetchApi(postcode, addressSlug);
} else {
//Need to delay the request.
lookupCache[postcode] = new Promise((resolve) => {
lookupCache[cacheKey] = new Promise((resolve) => {
let cancelled: boolean = true;
reqControl.timerID = setTimeout(
() => {
Expand All @@ -144,7 +145,7 @@ const throttledApi = async (
});
}

return lookupCache[postcode];
return lookupCache[cacheKey];
};

type FormData = {
Expand Down Expand Up @@ -206,6 +207,7 @@ const PostcodeLookup = () => {
setApiResponse(false);
setPostError(null);

console.log("Lookup Constituency", postcode, addressSlug);
const responseJson = await throttledApi(postcode, addressSlug);

if (postcode != validPostcode.current) {
Expand Down Expand Up @@ -260,6 +262,10 @@ const PostcodeLookup = () => {
if (validatePostcode.test(normalizedPostcode)) {
validPostcode.current = normalizedPostcode;
await lookupConstituency(normalizedPostcode);
return;
} else {
//Otherwise reset the display of selected postcode
setApiResponse(null);
}
};

Expand Down Expand Up @@ -362,33 +368,75 @@ const PostcodeLookup = () => {
</InputGroup>

{apiResponse && apiResponse.constituencies.length > 1 && (
<div className="my-3">
<p className="mb-1" style={{ fontSize: "0.75em" }}>
We can&apos;t work out exactly which constituency you&apos;re in -
please select one of the {apiResponse.constituencies.length}{" "}
options:
</p>
<Form.Select
name="constituency"
size="lg"
defaultValue=""
onChange={(e) =>
setFormState({
...formState,
constituencyIndex: parseInt(e.target.value),
})
}
>
<option selected disabled value="" style={{ display: "none" }}>
Select Constituency
</option>
{apiResponse.constituencies.map((c, idx) => (
<option key={c.slug} value={idx}>
{c.name}
</option>
))}
</Form.Select>
</div>
<>
{apiResponse.addresses ? (
<div className="my-3">
<p className="mb-1" style={{ fontSize: "0.75em" }}>
We can&apos;t work out exactly which constituency you&apos;re
in - please select your address:
</p>
<Form.Select
name="address"
size="lg"
defaultValue=""
onChange={(e) =>
lookupConstituency(validPostcode.current, e.target.value)
}
>
<option
selected
disabled
value=""
style={{ display: "none" }}
>
Select Address
</option>
{apiResponse.addresses.map((c) => (
<option key={c.slug} value={c.slug}>
{c.name}
</option>
))}
</Form.Select>
</div>
) : (
<div className="my-3">
<p className="mb-1" style={{ fontSize: "0.75em" }}>
We can&apos;t work out exactly which constituency you&apos;re
in - please select one of the{" "}
{apiResponse.constituencies.length} options:
</p>
<Form.Select
name="constituency"
size="lg"
defaultValue=""
onChange={(e) => {
if (e.target.value.length > 2) {
lookupConstituency(validPostcode.current, e.target.value);
} else {
setFormState({
...formState,
constituencyIndex: parseInt(e.target.value),
});
}
}}
>
<option
selected
disabled
value=""
style={{ display: "none" }}
>
Select Constituency
</option>
{apiResponse.constituencies.map((c, idx) => (
<option key={c.slug} value={idx}>
{c.name}
</option>
))}
</Form.Select>
</div>
)}
</>
)}
{subscribed ? (
<div className="my-3"></div>
Expand Down
1 change: 1 addition & 0 deletions components/constituency_lookup/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
type Constituency = {
name: string;
slug: string;
gss?: string;
};

type Address = {
Expand Down
Binary file modified data/postcodes.db
Binary file not shown.

0 comments on commit bd65914

Please sign in to comment.