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

Production deploy #2310

Merged
merged 5 commits into from
Oct 16, 2023
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
2 changes: 1 addition & 1 deletion .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ jobs:
- [Editor](https://${{ env.FULL_DOMAIN }})
- [Storybook](https://storybook.${{ env.FULL_DOMAIN }})
- [Hasura](https://hasura.${{ env.FULL_DOMAIN }})
- [API](https://api.${{ env.FULL_DOMAIN }})
- [API](https://api.${{ env.FULL_DOMAIN }}/docs)
- [ShareDB](https://sharedb.${{ env.FULL_DOMAIN }})

healthcheck:
Expand Down
11 changes: 4 additions & 7 deletions api.planx.uk/admin/session/bops.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Request, Response, NextFunction } from "express";
import { getClient } from "../../client";
import { NextFunction, Request, Response } from "express";
import { $api } from "../../client";

/**
* @swagger
* /admin/session/{sessionId}/bops:
* get:
* summary: Generates a Back Office Planning System (BOPS) payload
* description: Generates a BOPS payload, relies on a submission record in `bops_applications`
* description: Generates a BOPS payload
* tags:
* - admin
* parameters:
Expand All @@ -20,10 +20,7 @@ export const getBOPSPayload = async (
next: NextFunction,
) => {
try {
const $client = getClient();
const { exportData } = await $client.export.bopsPayload(
req.params.sessionId,
);
const { exportData } = await $api.export.bopsPayload(req.params.sessionId);
res.set("content-type", "application/json");
return res.send(exportData);
} catch (error) {
Expand Down
2 changes: 1 addition & 1 deletion api.planx.uk/admin/session/html.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const mockGenerateHTMLData = jest.fn().mockResolvedValue({
});
jest.mock("../../client", () => {
return {
$admin: {
$api: {
export: {
csvData: () => mockGenerateHTMLData(),
},
Expand Down
52 changes: 15 additions & 37 deletions api.planx.uk/admin/session/oneAppXML.test.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,26 @@
import supertest from "supertest";
import app from "../../server";
import { queryMock } from "../../tests/graphqlQueryMock";
import { authHeader } from "../../tests/mockJWT";

const endpoint = (strings: TemplateStringsArray) =>
`/admin/session/${strings[0]}/xml`;

describe("OneApp XML endpoint", () => {
beforeEach(() => {
queryMock.mockQuery({
name: "GetMostRecentUniformApplicationBySessionID",
variables: {
submission_reference: "abc123",
},
data: {
uniform_applications: [{ xml: "<dummy:xml></dummy:xml>" }],
},
});

queryMock.mockQuery({
name: "GetMostRecentUniformApplicationBySessionID",
variables: {
submission_reference: "xyz789",
},
data: {
uniform_applications: [],
},
});
});
const mockGenerateOneAppXML = jest
.fn()
.mockResolvedValue("<dummy:xml></dummy:xml>");

afterEach(() => jest.clearAllMocks());
const auth = authHeader({ role: "platformAdmin" });
jest.mock("../../client", () => {
return {
$api: {
generateOneAppXML: () => mockGenerateOneAppXML(),
},
};
});

describe("OneApp XML endpoint", () => {
it("requires a user to be logged in", async () => {
await supertest(app)
.get(endpoint`abc123`)
.get(endpoint`123`)
.expect(401)
.then((res) =>
expect(res.body).toEqual({
Expand All @@ -45,23 +31,15 @@ describe("OneApp XML endpoint", () => {

it("requires a user to have the 'platformAdmin' role", async () => {
await supertest(app)
.get(endpoint`abc123`)
.get(endpoint`123`)
.set(authHeader({ role: "teamEditor" }))
.expect(403);
});

it("returns an error if sessionID is invalid", async () => {
await supertest(app)
.get(endpoint`xyz789`)
.set(auth)
.expect(500)
.then((res) => expect(res.body.error).toMatch(/Invalid sessionID/));
});

it("returns XML", async () => {
await supertest(app)
.get(endpoint`abc123`)
.set(auth)
.get(endpoint`123`)
.set(authHeader({ role: "platformAdmin" }))
.expect(200)
.expect("content-type", "text/xml; charset=utf-8")
.then((res) => {
Expand Down
34 changes: 3 additions & 31 deletions api.planx.uk/admin/session/oneAppXML.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { gql } from "graphql-request";
import { Request, Response, NextFunction } from "express";
import { adminGraphQLClient as client } from "../../hasura";
import { $api } from "../../client";

/**
* @swagger
* /admin/session/{sessionId}/xml:
* get:
* summary: Generates a OneApp XML
* description: Generates a OneApp XML, relies on a submission record in `uniform_applications`
* description: Generates a OneApp XML
* tags:
* - admin
* parameters:
Expand All @@ -21,7 +20,7 @@ export const getOneAppXML = async (
next: NextFunction,
) => {
try {
const xml = await fetchSessionXML(req.params.sessionId);
const xml = await $api.generateOneAppXML(req.params.sessionId);
res.set("content-type", "text/xml");
return res.send(xml);
} catch (error) {
Expand All @@ -30,30 +29,3 @@ export const getOneAppXML = async (
});
}
};

const fetchSessionXML = async (sessionId: string) => {
try {
const query = gql`
query GetMostRecentUniformApplicationBySessionID(
$submission_reference: String
) {
uniform_applications(
where: { submission_reference: { _eq: $submission_reference } }
order_by: { created_at: desc }
limit: 1
) {
xml
}
}
`;
const { uniform_applications } = await client.request(query, {
submission_reference: sessionId,
});

if (!uniform_applications?.length) throw Error("Invalid sessionID");

return uniform_applications[0].xml;
} catch (error) {
throw Error("Unable to fetch session XML: " + (error as Error).message);
}
};
21 changes: 13 additions & 8 deletions api.planx.uk/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { CoreDomainClient } from "@opensystemslab/planx-core";
import { userContext } from "../modules/auth/middleware";
import { ServerError } from "../errors";
import { buildJWTForAPIRole } from "../modules/auth/service";

/**
* @deprecated This client's permissions set are higher than required.
* Should only be used by trusted service-to-service calls (e.g Hasura -> API).
* Calls made by users should be directed through $public or the role-scoped getClient().
* Connects to Hasura using the "api" role
*
* Consider removing this and replacing with an "api" role using "backend-only" operations in Hasura
* Should be used when a request is not initiated by a user, but another PlanX service (e.g. Hasura events).
* Can also be used for "side effects" triggered by a user (e.g. writing audit logs)
*/
export const $admin = new CoreDomainClient({
auth: { adminSecret: process.env.HASURA_GRAPHQL_ADMIN_SECRET! },
export const $api = new CoreDomainClient({
auth: {
jwt: buildJWTForAPIRole(),
},
targetURL: process.env.HASURA_GRAPHQL_URL!,
});

/**
* Connects to Hasura using the "public" role
*/
export const $public = new CoreDomainClient({
targetURL: process.env.HASURA_GRAPHQL_URL!,
});
Expand All @@ -31,12 +36,12 @@ export const getClient = () => {
message: "Missing user context",
});

const client = new CoreDomainClient({
const $client = new CoreDomainClient({
targetURL: process.env.HASURA_GRAPHQL_URL!,
auth: {
jwt: store.user.jwt,
},
});

return client;
return $client;
};
5 changes: 5 additions & 0 deletions api.planx.uk/docs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ const securitySchemes = {
scheme: "bearer",
bearerFormat: "JWT",
},
hasuraAuth: {
type: "apiKey",
in: "header",
name: "Authorization",
},
};

const parameters = {
Expand Down
15 changes: 15 additions & 0 deletions api.planx.uk/gis/digitalLand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,21 @@ async function go(
formattedResult[broads] = { fn: broads, value: false };
}

// FLOODING
if (formattedResult["flood"] && formattedResult["flood"].value) {
["flood.zone.1", "flood.zone.2", "flood.zone.3"].forEach(
(zone) =>
(formattedResult[zone] = {
fn: zone,
value: Boolean(
formattedResult["flood"].data?.filter(
(entity) => entity["flood-risk-level"] === zone.split(".").pop(),
).length,
),
}),
);
}

// --- LISTED BUILDINGS ---
// TODO add granular variables to reflect grade (eg `listed.grade1`), not reflected in content yet though

Expand Down
21 changes: 5 additions & 16 deletions api.planx.uk/gis/local_authorities/metadata/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,22 +133,11 @@ const baseSchema: PlanningConstraintsBaseSchema = {
"digital-land-datasets": ["ancient-woodland"],
category: "Ecology",
},
"flood.zone1": {
active: false,
neg: "is not within a Flood Zone 1 (low risk)",
pos: "is within a Flood Zone 1 (low risk)",
category: "Flooding",
},
"flood.zone2": {
active: false,
neg: "is not within a Flood Zone 2 (medium risk)",
pos: "is within a Flood Zone 2 (medium risk)",
category: "Flooding",
},
"flood.zone3": {
active: false,
neg: "is not within a Flood Zone 3 (high risk)",
pos: "is within a Flood Zone 3 (high risk)",
flood: {
active: true,
neg: "is not in a Flood Risk Zone",
pos: "is in a Flood Risk Zone",
"digital-land-datasets": ["flood-risk-zone"],
category: "Flooding",
},
"defence.explosives": {
Expand Down
7 changes: 1 addition & 6 deletions api.planx.uk/hasura/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,4 @@ const adminGraphQLClient = new GraphQLClient(process.env.HASURA_GRAPHQL_URL!, {
},
});

/**
* Connect to Hasura using the "Public" role
*/
const publicGraphQLClient = new GraphQLClient(process.env.HASURA_GRAPHQL_URL!);

export { adminGraphQLClient, publicGraphQLClient };
export { adminGraphQLClient };
4 changes: 2 additions & 2 deletions api.planx.uk/inviteToPay/createPaymentSendEvents.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ComponentType } from "@opensystemslab/planx-core/types";
import { NextFunction, Request, Response } from "express";
import { gql } from "graphql-request";
import { $admin } from "../client";
import { $api } from "../client";
import { adminGraphQLClient as adminClient } from "../hasura";
import {
ScheduledEventResponse,
Expand Down Expand Up @@ -40,7 +40,7 @@ const createPaymentSendEvents = async (
const now = new Date();
const combinedResponse: CombinedResponse = {};

const session = await $admin.getSessionById(payload.sessionId);
const session = await $api.getSessionById(payload.sessionId);
if (!session) {
return next({
status: 400,
Expand Down
8 changes: 4 additions & 4 deletions api.planx.uk/inviteToPay/inviteToPay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { NextFunction, Request, Response } from "express";
import type { PaymentRequest, KeyPath } from "@opensystemslab/planx-core/types";

import { ServerError } from "../errors";
import { $admin } from "../client";
import { $api } from "../client";

export async function inviteToPay(
req: Request,
Expand Down Expand Up @@ -39,7 +39,7 @@ export async function inviteToPay(
}

// lock session before creating a payment request
const locked = await $admin.lockSession(sessionId);
const locked = await $api.lockSession(sessionId);
if (locked === null) {
return next(
new ServerError({
Expand All @@ -63,7 +63,7 @@ export async function inviteToPay(

let paymentRequest: PaymentRequest | undefined;
try {
paymentRequest = await $admin.createPaymentRequest({
paymentRequest = await $api.createPaymentRequest({
sessionId,
applicantName,
payeeName,
Expand All @@ -72,7 +72,7 @@ export async function inviteToPay(
});
} catch (e: unknown) {
// revert the session lock on failure
await $admin.unlockSession(sessionId);
await $api.unlockSession(sessionId);
return next(
new ServerError({
message:
Expand Down
4 changes: 2 additions & 2 deletions api.planx.uk/inviteToPay/sendConfirmationEmail.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { $public, $admin } from "../client";
import { $public, $api } from "../client";
import { sendEmail } from "../notify";
import { gql } from "graphql-request";
import { convertSlugToName } from "../saveAndReturn/utils";
Expand Down Expand Up @@ -85,7 +85,7 @@ async function getDataForPayeeAndAgentEmails(
applicantName: string;
}[];
}[];
} = await $admin.client.request(query, { sessionId });
} = await $api.client.request(query, { sessionId });
const data = response.lowcal_sessions[0];
const { emailReplyToId, helpEmail, helpOpeningHours, helpPhone } =
data.flow.team.notifyPersonalisation;
Expand Down
13 changes: 4 additions & 9 deletions api.planx.uk/inviteToPay/sendPaymentEmail.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,11 @@ import {
validatePaymentRequestNotFoundQueryMock,
validatePaymentRequestQueryMock,
} from "../tests/mocks/inviteToPayMocks";
import { CoreDomainClient } from "@opensystemslab/planx-core";

jest.mock("@opensystemslab/planx-core", () => {
return {
CoreDomainClient: jest.fn().mockImplementation(() => ({
formatRawProjectTypes: jest
.fn()
.mockResolvedValue(["New office premises"]),
})),
};
});
jest
.spyOn(CoreDomainClient.prototype, "formatRawProjectTypes")
.mockResolvedValue("New office premises");

const TEST_PAYMENT_REQUEST_ID = "09655c28-3f34-4619-9385-cd57312acc44";

Expand Down
Loading