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: Use $api client for requests from Hasura → API #2297

Merged
merged 4 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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 api.planx.uk/admin/session/html.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
});
jest.mock("../../client", () => {
return {
$admin: {
$api: {
export: {
csvData: () => mockGenerateHTMLData(),
},
Expand Down Expand Up @@ -46,7 +46,7 @@
.expect(403);
});

it.skip("returns a HTML-formatted payload", () => {

Check warning on line 49 in api.planx.uk/admin/session/html.test.ts

View workflow job for this annotation

GitHub Actions / Run API Tests

Disabled test
return supertest(app)
.get(endpoint`123`)
.set(authHeader({ role: "platformAdmin" }))
Expand Down
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({
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not totally thrilled with the name of this at it feels a little unspecific, but it follows the existing pattern of $<ROLE> being the client for that role.

We could rename getClient() -> getUserClient() which returns a $user, but similarly this feels a little unclear as you could end up with $user.user.addUser(newUser) 🫠

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;
};
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
15 changes: 13 additions & 2 deletions api.planx.uk/modules/auth/service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { sign } from "jsonwebtoken";
import { $admin } from "../../client";
import { $api } from "../../client";
import { User, Role } from "@opensystemslab/planx-core/types";

export const buildJWT = async (email: string): Promise<string | undefined> => {
const user = await $admin.user.getByEmail(email);
const user = await $api.user.getByEmail(email);
if (!user) return;

const data = {
Expand All @@ -16,6 +16,17 @@ export const buildJWT = async (email: string): Promise<string | undefined> => {
return jwt;
};

export const buildJWTForAPIRole = () =>
sign(
{
"https://hasura.io/jwt/claims": {
"x-hasura-allowed-roles": ["api"],
"x-hasura-default-role": "api",
},
},
process.env.JWT_SECRET!,
);

const generateHasuraClaimsForUser = (user: User) => ({
"x-hasura-allowed-roles": getAllowedRolesForUser(user),
"x-hasura-default-role": getDefaultRoleForUser(user),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const mockRunSQL = runSQL as jest.MockedFunction<typeof runSQL>;
const mockFindSession = jest.fn();
jest.mock("../../../../client", () => {
return {
$admin: {
$api: {
session: {
find: jest.fn().mockImplementation(() => mockFindSession()),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import supertest from "supertest";
import app from "../../../../server";
import SlackNotify from "slack-notify";
import { BOPSBody, EmailBody, UniformBody } from "./types";
import { $admin } from "../../../../client";
import { $api } from "../../../../client";
import { CoreDomainClient } from "@opensystemslab/planx-core";

const mockSessionWithFee = {
Expand Down Expand Up @@ -36,7 +36,7 @@ const mockSessionWithResubmissionExemption = {
};

jest.mock<CoreDomainClient>("../../../../client");
const mockAdmin = jest.mocked($admin);
const mockAdmin = jest.mocked($api);

const mockSend = jest.fn();
jest.mock<typeof SlackNotify>("slack-notify", () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
EventType,
UniformEventData,
} from "./types";
import { $admin } from "../../../../client";
import { $api } from "../../../../client";

export const sendSlackNotification = async (
data: EventData,
Expand Down Expand Up @@ -51,7 +51,7 @@ const getSessionIdFromEvent = (data: EventData, type: EventType) =>
})[type];

const getExemptionStatusesForSession = async (sessionId: string) => {
const session = await $admin.session.find(sessionId);
const session = await $api.session.find(sessionId);
if (!session) throw Error(`Unable to find session with ID ${sessionId}`);

const passport = new Passport(session.data.passport);
Expand Down
4 changes: 2 additions & 2 deletions api.planx.uk/pay/pay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import SlackNotify from "slack-notify";
import { logPaymentStatus } from "../send/helpers";
import { usePayProxy } from "./proxy";
import { addGovPayPaymentIdToPaymentRequest } from "../inviteToPay";
import { $admin } from "../client";
import { $api } from "../client";
import { ServerError } from "../errors";
import { GovUKPayment } from "@opensystemslab/planx-core/types";

Expand Down Expand Up @@ -44,7 +44,7 @@ export async function makePaymentViaProxy(
);
}

const session = await $admin.session.findDetails(sessionId);
const session = await $api.session.findDetails(sessionId);

if (session?.lockedAt) {
return next(
Expand Down
4 changes: 2 additions & 2 deletions api.planx.uk/send/bops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { adminGraphQLClient as adminClient } from "../hasura";
import { markSessionAsSubmitted } from "../saveAndReturn/utils";
import { NextFunction, Request, Response } from "express";
import { gql } from "graphql-request";
import { $admin } from "../client";
import { $api } from "../client";
import { ServerError } from "../errors";

interface SendToBOPSRequest {
Expand Down Expand Up @@ -70,7 +70,7 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => {
);
}
const target = `${bopsSubmissionURL}/api/v1/planning_applications`;
const { exportData } = await $admin.export.bopsPayload(payload?.sessionId);
const { exportData } = await $api.export.bopsPayload(payload?.sessionId);

try {
const bopsResponse = await axios({
Expand Down
2 changes: 1 addition & 1 deletion api.planx.uk/send/exportZip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const mockGenerateOneAppXML = jest

jest.mock("../client", () => {
return {
$admin: {
$api: {
generateOneAppXML: () => mockGenerateOneAppXML(),
getDocumentTemplateNamesForSession: jest
.fn()
Expand Down
11 changes: 5 additions & 6 deletions api.planx.uk/send/exportZip.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os from "os";
import path from "path";
import { $admin } from "../client";
import { $api } from "../client";
import { stringify } from "csv-stringify";
import fs from "fs";
import str from "string-to-stream";
Expand Down Expand Up @@ -28,7 +28,7 @@ export async function buildSubmissionExportZip({
const zip = new ExportZip(sessionId);

// fetch session data
const sessionData = await $admin.session.find(sessionId);
const sessionData = await $api.session.find(sessionId);
if (!sessionData) {
throw new Error(
`session ${sessionId} not found so could not create Uniform submission zip`,
Expand All @@ -39,7 +39,7 @@ export async function buildSubmissionExportZip({
// add OneApp XML to the zip
if (includeOneAppXML) {
try {
const xml = await $admin.generateOneAppXML(sessionId);
const xml = await $api.generateOneAppXML(sessionId);
const xmlStream = str(xml.trim());
await zip.addStream({
name: "proposal.xml", // must be named "proposal.xml" to be processed by Uniform
Expand All @@ -65,8 +65,7 @@ export async function buildSubmissionExportZip({
}

// generate csv data
const { responses, redactedResponses } =
await $admin.export.csvData(sessionId);
const { responses, redactedResponses } = await $api.export.csvData(sessionId);

// write csv to the zip
try {
Expand All @@ -84,7 +83,7 @@ export async function buildSubmissionExportZip({

// add template files to zip
const templateNames =
await $admin.getDocumentTemplateNamesForSession(sessionId);
await $api.getDocumentTemplateNamesForSession(sessionId);
for (const templateName of templateNames || []) {
try {
const isTemplateSupported = hasRequiredDataForTemplate({
Expand Down
4 changes: 2 additions & 2 deletions api.planx.uk/send/uniform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import fs from "fs";
import { adminGraphQLClient as adminClient } from "../hasura";
import { markSessionAsSubmitted } from "../saveAndReturn/utils";
import { gql } from "graphql-request";
import { $admin } from "../client";
import { $api } from "../client";
import { buildSubmissionExportZip } from "./exportZip";

interface UniformClient {
Expand Down Expand Up @@ -373,7 +373,7 @@ const createUniformApplicationAuditRecord = async ({
localAuthority: string;
submissionDetails: UniformSubmissionResponse;
}): Promise<UniformApplication> => {
const xml = await $admin.generateOneAppXML(payload?.sessionId);
const xml = await $api.generateOneAppXML(payload?.sessionId);

const application: Record<
"insert_uniform_applications_one",
Expand Down
2 changes: 1 addition & 1 deletion api.planx.uk/session/files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const mockFindSession = jest.fn();

jest.mock("../client", () => {
return {
$admin: {
$api: {
session: {
find: jest.fn().mockImplementation(() => mockFindSession()),
},
Expand Down
4 changes: 2 additions & 2 deletions api.planx.uk/session/files.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Passport } from "@opensystemslab/planx-core";
import { $admin } from "../client";
import { $api } from "../client";

export const getFilesForSession = async (
sessionId: string,
): Promise<string[]> => {
const session = await $admin.session.find(sessionId);
const session = await $api.session.find(sessionId);
if (!session?.data.passport?.data) return [];

const files = new Passport(session.data.passport).files();
Expand Down
Loading