diff --git a/api.planx.uk/admin/session/summary.test.ts b/api.planx.uk/admin/session/summary.test.ts index fd35686a41..08c7b2b838 100644 --- a/api.planx.uk/admin/session/summary.test.ts +++ b/api.planx.uk/admin/session/summary.test.ts @@ -14,7 +14,7 @@ describe("Session summary admin endpoint", () => { sessionId: "abc123", }, data: { - lowcal_sessions_by_pk: {}, + lowcalSession: {}, }, }); }); diff --git a/api.planx.uk/admin/session/summary.ts b/api.planx.uk/admin/session/summary.ts index d0975c9e0a..344a6c283b 100644 --- a/api.planx.uk/admin/session/summary.ts +++ b/api.planx.uk/admin/session/summary.ts @@ -6,8 +6,8 @@ import { import { NextFunction, Request, Response } from "express"; import { gql } from "graphql-request"; -import { adminGraphQLClient as adminClient } from "../../hasura"; import { Breadcrumb, Flow, LowCalSession, Passport, Team } from "../../types"; +import { $api } from "../../client"; /** * @swagger @@ -28,9 +28,9 @@ export async function getSessionSummary( next: NextFunction, ) { try { - const data = await getSessionSummaryById(req.params.sessionId); - if (data) { - res.send(data); + const session = await getSessionSummaryById(req.params.sessionId); + if (session) { + res.send(session); } else { res.send({ message: `Cannot find data for this session; double-check your sessionId is correct`, @@ -72,11 +72,13 @@ interface SessionSummary { const getSessionSummaryById = async ( sessionId: Session["id"], -): Promise => { - const data = await adminClient.request( +): Promise => { + const { lowcalSession } = await $api.client.request< + Record<"lowcalSession", SessionSummary | null> + >( gql` query GetSessionSummary($sessionId: uuid!) { - lowcal_sessions_by_pk(id: $sessionId) { + lowcalSession: lowcal_sessions_by_pk(id: $sessionId) { flow { id slug @@ -112,5 +114,5 @@ const getSessionSummaryById = async ( }, ); - return data?.lowcal_sessions_by_pk; + return lowcalSession; }; diff --git a/api.planx.uk/editor/copyFlow.test.ts b/api.planx.uk/editor/copyFlow.test.ts index 7cf73cb670..15c683719f 100644 --- a/api.planx.uk/editor/copyFlow.test.ts +++ b/api.planx.uk/editor/copyFlow.test.ts @@ -10,7 +10,7 @@ beforeEach(() => { name: "GetFlowData", matchOnVariables: false, data: { - flows_by_pk: { + flow: { data: mockFlowData, }, }, @@ -20,7 +20,7 @@ beforeEach(() => { name: "InsertFlow", matchOnVariables: false, data: { - insert_flows_one: { + flow: { id: 2, }, }, @@ -30,7 +30,7 @@ beforeEach(() => { name: "InsertOperation", matchOnVariables: false, data: { - insert_operations_one: { + operation: { id: 1, }, }, diff --git a/api.planx.uk/editor/copyPortalAsFlow.test.ts b/api.planx.uk/editor/copyPortalAsFlow.test.ts index 46b84d0268..a13fe8baa5 100644 --- a/api.planx.uk/editor/copyPortalAsFlow.test.ts +++ b/api.planx.uk/editor/copyPortalAsFlow.test.ts @@ -10,7 +10,7 @@ beforeEach(() => { name: "GetFlowData", matchOnVariables: false, data: { - flows_by_pk: { + flow: { data: mockFlowData, }, }, diff --git a/api.planx.uk/editor/findReplace.test.ts b/api.planx.uk/editor/findReplace.test.ts index 8678d8e476..f1ded631ee 100644 --- a/api.planx.uk/editor/findReplace.test.ts +++ b/api.planx.uk/editor/findReplace.test.ts @@ -10,7 +10,7 @@ beforeEach(() => { name: "GetFlowData", matchOnVariables: false, data: { - flows_by_pk: { + flow: { data: mockFlowData, slug: "test", }, @@ -21,7 +21,7 @@ beforeEach(() => { name: "UpdateFlow", matchOnVariables: false, data: { - update_flows_by_pk: { + flow: { data: replacedFlowData, slug: "test", }, diff --git a/api.planx.uk/editor/findReplace.ts b/api.planx.uk/editor/findReplace.ts index 5e5a2a0f93..a0beaae8eb 100644 --- a/api.planx.uk/editor/findReplace.ts +++ b/api.planx.uk/editor/findReplace.ts @@ -1,8 +1,9 @@ import { Flow } from "./../types"; -import { adminGraphQLClient as adminClient } from "../hasura"; import { gql } from "graphql-request"; import { getFlowData } from "../helpers"; import { Request, Response, NextFunction } from "express"; +import { getClient } from "../client"; +import { FlowGraph } from "@opensystemslab/planx-core/types"; interface MatchResult { matches: Flow["data"]; @@ -49,6 +50,15 @@ const getMatches = ( }; }; +interface UpdateFlow { + flow: { + id: string; + slug: string; + data: FlowGraph; + updatedAt: string; + }; +} + /** * @swagger * /flows/{flowId}/search: @@ -136,14 +146,18 @@ const findAndReplaceInFlow = async ( } // if matches, proceed with mutation to update flow data - const response = await adminClient.request( + const { client: $client } = getClient(); + const response = await $client.request( gql` mutation UpdateFlow($data: jsonb = {}, $id: uuid!) { - update_flows_by_pk(pk_columns: { id: $id }, _set: { data: $data }) { + flow: update_flows_by_pk( + pk_columns: { id: $id } + _set: { data: $data } + ) { id slug data - updated_at + updatedAt: updated_at } } `, @@ -153,8 +167,7 @@ const findAndReplaceInFlow = async ( }, ); - const updatedFlow = - response.update_flows_by_pk && response.update_flows_by_pk.data; + const updatedFlow = response.flow && response.flow.data; res.json({ message: `Found ${ diff --git a/api.planx.uk/editor/moveFlow.test.ts b/api.planx.uk/editor/moveFlow.test.ts index f4be83a537..f8d377777e 100644 --- a/api.planx.uk/editor/moveFlow.test.ts +++ b/api.planx.uk/editor/moveFlow.test.ts @@ -26,7 +26,7 @@ beforeEach(() => { team_id: "1", }, data: { - update_flows_by_pk: { + flow: { id: "1", }, }, diff --git a/api.planx.uk/editor/moveFlow.ts b/api.planx.uk/editor/moveFlow.ts index add5717705..0135f81f37 100644 --- a/api.planx.uk/editor/moveFlow.ts +++ b/api.planx.uk/editor/moveFlow.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { gql } from "graphql-request"; -import { adminGraphQLClient as adminClient } from "../hasura"; import { Flow, Team } from "../types"; +import { $public, getClient } from "../client"; const moveFlow = async ( req: Request, @@ -36,8 +36,12 @@ const moveFlow = async ( } }; +interface GetTeam { + teams: Pick[]; +} + const getTeamIdBySlug = async (slug: Team["slug"]): Promise => { - const data = await adminClient.request( + const data = await $public.client.request( gql` query GetTeam($slug: String!) { teams(where: { slug: { _eq: $slug } }) { @@ -53,14 +57,19 @@ const getTeamIdBySlug = async (slug: Team["slug"]): Promise => { return data?.teams[0].id; }; +interface UpdateFlow { + flow: Pick; +} + const updateFlow = async ( flowId: Flow["id"], teamId: Team["id"], ): Promise => { - const data = await adminClient.request( + const { client: $client } = getClient(); + const { flow } = await $client.request( gql` mutation UpdateFlow($id: uuid!, $team_id: Int!) { - update_flows_by_pk( + flow: update_flows_by_pk( pk_columns: { id: $id } _set: { team_id: $team_id } ) { @@ -74,7 +83,7 @@ const updateFlow = async ( }, ); - return data?.update_flows_by_pk?.id; + return flow.id; }; export { moveFlow }; diff --git a/api.planx.uk/editor/publish.test.ts b/api.planx.uk/editor/publish.test.ts index af840293c7..41f6165887 100644 --- a/api.planx.uk/editor/publish.test.ts +++ b/api.planx.uk/editor/publish.test.ts @@ -1,17 +1,28 @@ import supertest from "supertest"; import { queryMock } from "../tests/graphqlQueryMock"; -import { authHeader } from "../tests/mockJWT"; +import { authHeader, getJWT } from "../tests/mockJWT"; import app from "../server"; import { flowWithInviteToPay } from "../tests/mocks/inviteToPayData"; import { FlowGraph } from "@opensystemslab/planx-core/types"; +import { userContext } from "../modules/auth/middleware"; + +beforeAll(() => { + const getStoreMock = jest.spyOn(userContext, "getStore"); + getStoreMock.mockReturnValue({ + user: { + sub: "123", + jwt: getJWT({ role: "teamEditor" }), + }, + }); +}); beforeEach(() => { queryMock.mockQuery({ name: "GetFlowData", matchOnVariables: false, data: { - flows_by_pk: { + flow: { data: mockFlowData, }, }, @@ -21,8 +32,8 @@ beforeEach(() => { name: "GetMostRecentPublishedFlow", matchOnVariables: false, data: { - flows_by_pk: { - published_flows: [ + flow: { + publishedFlows: [ { data: mockFlowData, }, @@ -35,7 +46,7 @@ beforeEach(() => { name: "PublishFlow", matchOnVariables: false, data: { - insert_published_flows_one: { + publishedFlow: { data: mockFlowData, }, }, @@ -89,7 +100,7 @@ describe("publish", () => { name: "GetFlowData", matchOnVariables: false, data: { - flows_by_pk: { + flow: { data: alteredFlow, }, }, @@ -99,7 +110,7 @@ describe("publish", () => { name: "PublishFlow", matchOnVariables: false, data: { - insert_published_flows_one: { + publishedFlow: { data: alteredFlow, }, }, @@ -147,7 +158,7 @@ describe("sections validation on diff", () => { name: "GetFlowData", matchOnVariables: false, data: { - flows_by_pk: { + flow: { data: alteredFlow, }, }, @@ -182,7 +193,7 @@ describe("sections validation on diff", () => { name: "GetFlowData", matchOnVariables: false, data: { - flows_by_pk: { + flow: { data: flowWithSections, }, }, @@ -214,7 +225,7 @@ describe("invite to pay validation on diff", () => { name: "GetFlowData", matchOnVariables: false, data: { - flows_by_pk: { + flow: { data: invalidatedFlow, }, }, @@ -247,7 +258,7 @@ describe("invite to pay validation on diff", () => { name: "GetFlowData", matchOnVariables: false, data: { - flows_by_pk: { + flow: { data: alteredFlow, }, }, @@ -276,7 +287,7 @@ describe("invite to pay validation on diff", () => { name: "GetFlowData", matchOnVariables: false, data: { - flows_by_pk: { + flow: { data: invalidatedFlow, }, }, @@ -307,7 +318,7 @@ describe("invite to pay validation on diff", () => { name: "GetFlowData", matchOnVariables: false, data: { - flows_by_pk: { + flow: { data: invalidFlow, }, }, @@ -340,7 +351,7 @@ describe("invite to pay validation on diff", () => { name: "GetFlowData", matchOnVariables: false, data: { - flows_by_pk: { + flow: { data: invalidatedFlow, }, }, diff --git a/api.planx.uk/editor/publish.ts b/api.planx.uk/editor/publish.ts index 25efd4b81e..ca4d8976f6 100644 --- a/api.planx.uk/editor/publish.ts +++ b/api.planx.uk/editor/publish.ts @@ -1,6 +1,5 @@ import * as jsondiffpatch from "jsondiffpatch"; import { Request, Response, NextFunction } from "express"; -import { adminGraphQLClient as adminClient } from "../hasura"; import { dataMerged, getMostRecentPublishedFlow } from "../helpers"; import { gql } from "graphql-request"; import intersection from "lodash/intersection"; @@ -11,6 +10,7 @@ import { } from "@opensystemslab/planx-core/types"; import { userContext } from "../modules/auth/middleware"; import type { Entry } from "type-fest"; +import { getClient } from "../client"; const validateAndDiffFlow = async ( req: Request, @@ -69,6 +69,16 @@ const validateAndDiffFlow = async ( } }; +interface PublishFlow { + publishedFlow: { + id: string; + flowId: string; + publisherId: string; + createdAt: string; + data: FlowGraph; + }; +} + const publishFlow = async ( req: Request, res: Response, @@ -83,7 +93,8 @@ const publishFlow = async ( if (!userId) throw Error("User details missing from request"); if (delta) { - const response = await adminClient.request( + const { client: $client } = getClient(); + const response = await $client.request( gql` mutation PublishFlow( $data: jsonb = {} @@ -91,7 +102,7 @@ const publishFlow = async ( $publisher_id: Int $summary: String ) { - insert_published_flows_one( + publishedFlow: insert_published_flows_one( object: { data: $data flow_id: $flow_id @@ -100,9 +111,9 @@ const publishFlow = async ( } ) { id - flow_id - publisher_id - created_at + flowId: flow_id + publisherId: publisher_id + createdAt: created_at data } } @@ -116,8 +127,7 @@ const publishFlow = async ( ); const publishedFlow = - response.insert_published_flows_one && - response.insert_published_flows_one.data; + response.publishedFlow && response.publishedFlow.data; const alteredNodes = Object.keys(delta).map((key) => ({ id: key, diff --git a/api.planx.uk/gis/digitalLand.ts b/api.planx.uk/gis/digitalLand.ts index 14a5644135..908bef56a7 100644 --- a/api.planx.uk/gis/digitalLand.ts +++ b/api.planx.uk/gis/digitalLand.ts @@ -5,9 +5,9 @@ import type { } from "@opensystemslab/planx-core/types"; import { gql } from "graphql-request"; import fetch from "isomorphic-fetch"; -import { adminGraphQLClient as adminClient } from "../hasura"; import { addDesignatedVariable, omitGeometry } from "./helpers"; import { baseSchema } from "./local_authorities/metadata/base"; +import { $api } from "../client"; export interface LocalAuthorityMetadata { planningConstraints: { @@ -73,7 +73,7 @@ async function go( // if analytics are "on", store an audit record of the raw response if (extras?.analytics !== "false") { - const _auditRecord = await adminClient.request( + const _auditRecord = await $api.client.request( gql` mutation CreatePlanningConstraintsRequest( $destination_url: String = "" diff --git a/api.planx.uk/hasura/index.ts b/api.planx.uk/hasura/index.ts deleted file mode 100644 index 0837de56c4..0000000000 --- a/api.planx.uk/hasura/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { GraphQLClient } from "graphql-request"; - -/** - * Connect to Hasura using the "Admin" role - */ -const adminGraphQLClient = new GraphQLClient(process.env.HASURA_GRAPHQL_URL!, { - headers: { - "x-hasura-admin-secret": process.env.HASURA_GRAPHQL_ADMIN_SECRET!, - }, -}); - -export { adminGraphQLClient }; diff --git a/api.planx.uk/helpers.test.ts b/api.planx.uk/helpers.test.ts index 2d10a91201..d862f88a94 100644 --- a/api.planx.uk/helpers.test.ts +++ b/api.planx.uk/helpers.test.ts @@ -1,6 +1,8 @@ import { ComponentType } from "@opensystemslab/planx-core/types"; import { dataMerged, getFormattedEnvironment, isLiveEnv } from "./helpers"; import { queryMock } from "./tests/graphqlQueryMock"; +import { userContext } from "./modules/auth/middleware"; +import { getJWT } from "./tests/mockJWT"; describe("getEnvironment function", () => { const OLD_ENV = process.env; @@ -62,6 +64,14 @@ describe("isLiveEnv() function", () => { }); describe("dataMerged() function", () => { + const getStoreMock = jest.spyOn(userContext, "getStore"); + getStoreMock.mockReturnValue({ + user: { + sub: "123", + jwt: getJWT({ role: "teamEditor" }), + }, + }); + beforeEach(() => { const unflattenedParent = { _root: { @@ -135,7 +145,7 @@ describe("dataMerged() function", () => { id: "child-id", }, data: { - flows_by_pk: { + flow: { slug: "child-flow", data: unflattenedChild, team_id: 123, @@ -149,7 +159,7 @@ describe("dataMerged() function", () => { id: "parent-id", }, data: { - flows_by_pk: { + flow: { slug: "parent-flow", data: unflattenedParent, team_id: 123, diff --git a/api.planx.uk/helpers.ts b/api.planx.uk/helpers.ts index 0ab9bd860c..47e38c9be2 100644 --- a/api.planx.uk/helpers.ts +++ b/api.planx.uk/helpers.ts @@ -1,15 +1,16 @@ import { gql } from "graphql-request"; import { capitalize } from "lodash"; -import { adminGraphQLClient as adminClient } from "./hasura"; import { Flow, Node } from "./types"; import { ComponentType, FlowGraph } from "@opensystemslab/planx-core/types"; +import { $public, getClient } from "./client"; // Get a flow's data (unflattened, without external portal nodes) const getFlowData = async (id: string): Promise => { - const data = await adminClient.request( + const { client: $client } = getClient(); + const { flow } = await $client.request<{ flow: Flow | null }>( gql` query GetFlowData($id: uuid!) { - flows_by_pk(id: $id) { + flow: flows_by_pk(id: $id) { slug data team_id @@ -18,8 +19,9 @@ const getFlowData = async (id: string): Promise => { `, { id }, ); + if (!flow) throw Error(`Unable to get flow with id ${id}`); - return data.flows_by_pk; + return flow; }; // Insert a new flow into the `flows` table @@ -30,7 +32,8 @@ const insertFlow = async ( creatorId?: number, copiedFrom?: Flow["id"], ) => { - const data = await adminClient.request( + const { client: $client } = getClient(); + const data = await $client.request<{ flow: { id: string } }>( gql` mutation InsertFlow( $team_id: Int! @@ -39,7 +42,7 @@ const insertFlow = async ( $creator_id: Int $copied_from: uuid ) { - insert_flows_one( + flow: insert_flows_one( object: { team_id: $team_id slug: $slug @@ -62,16 +65,17 @@ const insertFlow = async ( }, ); - if (data) await createAssociatedOperation(data?.insert_flows_one?.id); - return data?.insert_flows_one; + if (data) await createAssociatedOperation(data?.flow?.id); + return data?.flow; }; // Add a row to `operations` for an inserted flow, otherwise ShareDB throws a silent error when opening the flow in the UI const createAssociatedOperation = async (flowId: Flow["id"]) => { - const data = await adminClient.request( + const { client: $client } = getClient(); + const data = await $client.request<{ operation: { id: string } }>( gql` mutation InsertOperation($flow_id: uuid!, $data: jsonb = {}) { - insert_operations_one( + operation: insert_operations_one( object: { flow_id: $flow_id, version: 1, data: $data } ) { id @@ -83,53 +87,42 @@ const createAssociatedOperation = async (flowId: Flow["id"]) => { }, ); - return data?.insert_operations_one; + return data?.operation; }; +interface PublishedFlows { + flow: { + publishedFlows: { + // TODO: use FlowGraph from planx-core here + data: Flow["data"]; + }[]; + } | null; +} + // Get the most recent version of a published flow's data (flattened, with external portal nodes) const getMostRecentPublishedFlow = async ( id: string, ): Promise => { - const data = await adminClient.request( + const { flow } = await $public.client.request( gql` query GetMostRecentPublishedFlow($id: uuid!) { - flows_by_pk(id: $id) { - published_flows(limit: 1, order_by: { created_at: desc }) { - data - } - } - } - `, - { id }, - ); - - return data.flows_by_pk.published_flows?.[0]?.data; -}; - -// Get the snapshot of the published flow for a certain point in time (flattened, with external portal nodes) -// created_at refers to published date, value passed in as param should be lowcal_session.updated_at -const getPublishedFlowByDate = async (id: string, created_at: string) => { - const data = await adminClient.request( - gql` - query GetPublishedFlowByDate($id: uuid!, $created_at: timestamptz!) { - flows_by_pk(id: $id) { - published_flows( + flow: flows_by_pk(id: $id) { + publishedFlows: published_flows( limit: 1 order_by: { created_at: desc } - where: { created_at: { _lte: $created_at } } ) { data } } } `, - { - id, - created_at, - }, + { id }, ); - return data.flows_by_pk.published_flows?.[0]?.data; + const mostRecent = flow?.publishedFlows?.[0]?.data; + if (!mostRecent) throw Error(`Published flow not found for flow ${id}`); + + return mostRecent; }; // Flatten a flow's data to include main content & portals in a single JSON representation @@ -249,7 +242,6 @@ const getFormattedEnvironment = (): string => { export { getFlowData, getMostRecentPublishedFlow, - getPublishedFlowByDate, dataMerged, getChildren, makeUniqueFlow, diff --git a/api.planx.uk/inviteToPay/createPaymentSendEvents.test.ts b/api.planx.uk/inviteToPay/createPaymentSendEvents.test.ts index cb75bf84b1..2f7cf22b28 100644 --- a/api.planx.uk/inviteToPay/createPaymentSendEvents.test.ts +++ b/api.planx.uk/inviteToPay/createPaymentSendEvents.test.ts @@ -38,8 +38,8 @@ describe("Create payment send events webhook", () => { name: "GetMostRecentPublishedFlow", matchOnVariables: false, data: { - flows_by_pk: { - published_flows: [ + flow: { + publishedFlows: [ { data: flowWithInviteToPay, }, @@ -52,7 +52,7 @@ describe("Create payment send events webhook", () => { name: "GetTeamSlugByFlowId", matchOnVariables: false, data: { - flows_by_pk: { + flow: { team: { slug: "southwark", }, diff --git a/api.planx.uk/inviteToPay/createPaymentSendEvents.ts b/api.planx.uk/inviteToPay/createPaymentSendEvents.ts index 055a884afd..799ab3bbba 100644 --- a/api.planx.uk/inviteToPay/createPaymentSendEvents.ts +++ b/api.planx.uk/inviteToPay/createPaymentSendEvents.ts @@ -1,8 +1,7 @@ import { ComponentType } from "@opensystemslab/planx-core/types"; import { NextFunction, Request, Response } from "express"; import { gql } from "graphql-request"; -import { $api } from "../client"; -import { adminGraphQLClient as adminClient } from "../hasura"; +import { $api, $public } from "../client"; import { ScheduledEventResponse, createScheduledEvent, @@ -121,11 +120,19 @@ const createPaymentSendEvents = async ( } }; +interface GetTeamSlugByFlowId { + flow: { + team: { + slug: string; + }; + }; +} + const getTeamSlugByFlowId = async (id: Flow["id"]): Promise => { - const data = await adminClient.request( + const data = await $public.client.request( gql` query GetTeamSlugByFlowId($id: uuid!) { - flows_by_pk(id: $id) { + flow: flows_by_pk(id: $id) { team { slug } @@ -135,7 +142,7 @@ const getTeamSlugByFlowId = async (id: Flow["id"]): Promise => { { id }, ); - return data.flows_by_pk.team.slug; + return data.flow.team.slug; }; export { createPaymentSendEvents }; diff --git a/api.planx.uk/inviteToPay/paymentRequest.ts b/api.planx.uk/inviteToPay/paymentRequest.ts index e9d35dffd6..39d6321afa 100644 --- a/api.planx.uk/inviteToPay/paymentRequest.ts +++ b/api.planx.uk/inviteToPay/paymentRequest.ts @@ -1,12 +1,27 @@ import { gql } from "graphql-request"; import { NextFunction, Request, Response } from "express"; -import { adminGraphQLClient as client } from "../hasura"; import { ServerError } from "../errors"; import { postPaymentNotificationToSlack, fetchPaymentViaProxyWithCallback, } from "../pay"; import { GovUKPayment } from "@opensystemslab/planx-core/types"; +import { $api } from "../client"; + +interface GetPaymentRequestDetails { + paymentRequest: { + sessionId: string; + paymentAmount: number; + session: { + flowId: string; + flow: { + team: { + slug: string; + }; + }; + }; + } | null; +} // middleware used by routes: // * /payment-request/:paymentRequest/pay @@ -18,11 +33,11 @@ export async function fetchPaymentRequestDetails( ) { const query = gql` query GetPaymentRequestDetails($paymentRequestId: uuid!) { - payment_requests_by_pk(id: $paymentRequestId) { - session_id - payment_amount + paymentRequest: payment_requests_by_pk(id: $paymentRequestId) { + sessionId: session_id + paymentAmount: payment_amount session { - flow_id + flowId: flow_id flow { team { slug @@ -32,10 +47,11 @@ export async function fetchPaymentRequestDetails( } } `; - const { payment_requests_by_pk } = await client.request(query, { - paymentRequestId: req.params.paymentRequest, - }); - if (!payment_requests_by_pk) { + const { paymentRequest } = + await $api.client.request(query, { + paymentRequestId: req.params.paymentRequest, + }); + if (!paymentRequest) { return next( new ServerError({ message: "payment request not found", @@ -43,16 +59,16 @@ export async function fetchPaymentRequestDetails( }), ); } - const sessionId = payment_requests_by_pk.session_id; + const sessionId = paymentRequest.sessionId; if (sessionId) req.query.sessionId = sessionId; - const localAuthority = payment_requests_by_pk.session?.flow?.team?.slug; + const localAuthority = paymentRequest.session?.flow?.team?.slug; if (localAuthority) req.params.localAuthority = localAuthority; - const flowId = payment_requests_by_pk.session?.flow_id; + const flowId = paymentRequest.session?.flowId; if (flowId) req.query.flowId = flowId; - const paymentAmount = payment_requests_by_pk.payment_amount; + const paymentAmount = paymentRequest.paymentAmount.toString(); if (paymentAmount) req.params.paymentAmount = paymentAmount; next(); @@ -109,7 +125,7 @@ export const addGovPayPaymentIdToPaymentRequest = async ( } `; try { - await client.request(query, { + await $api.client.request(query, { paymentRequestId, govPayPaymentId: govUKPayment.payment_id, }); @@ -118,6 +134,15 @@ export const addGovPayPaymentIdToPaymentRequest = async ( } }; +interface MarkPaymentRequestAsPaid { + updatePaymentRequestPaidAt: { + affectedRows: number; + }; + appendGovUKPaymentToSessionData: { + affectedRows: number; + }; +} + export const markPaymentRequestAsPaid = async ( paymentRequestId: string, govUkPayment: GovUKPayment, @@ -147,7 +172,7 @@ export const markPaymentRequestAsPaid = async ( `; try { const { updatePaymentRequestPaidAt, appendGovUKPaymentToSessionData } = - await client.request(query, { + await $api.client.request(query, { paymentRequestId, govUkPayment: { govUkPayment }, }); diff --git a/api.planx.uk/modules/analytics/index.test.ts b/api.planx.uk/modules/analytics/index.test.ts index 8ec20b9d44..2c8d4df4b7 100644 --- a/api.planx.uk/modules/analytics/index.test.ts +++ b/api.planx.uk/modules/analytics/index.test.ts @@ -34,8 +34,8 @@ describe("Logging analytics", () => { user_exit: true, }, data: { - update_analytics_logs_by_pk: { - analytics_id: 12345, + analyticsLog: { + analyticsId: 12345, }, }, }); @@ -57,8 +57,8 @@ describe("Logging analytics", () => { user_exit: false, }, data: { - update_analytics_logs_by_pk: { - analytics_id: 12345, + analyticsLog: { + analyticsId: 12345, }, }, }); @@ -77,8 +77,8 @@ describe("Logging analytics", () => { name: "UpdateAnalyticsLogUserExit", matchOnVariables: false, data: { - update_analytics_logs_by_pk: { - analytics_id: 12345, + analyticsLogs: { + analyticsId: 12345, }, }, graphqlErrors: [ diff --git a/api.planx.uk/modules/analytics/service.ts b/api.planx.uk/modules/analytics/service.ts index 01018a2eb7..9defd29fc5 100644 --- a/api.planx.uk/modules/analytics/service.ts +++ b/api.planx.uk/modules/analytics/service.ts @@ -1,5 +1,13 @@ import { gql } from "graphql-request"; -import { adminGraphQLClient as adminClient } from "../../hasura"; +import { $public } from "../../client"; + +interface UpdateAnalyticsLogUserExit { + analyticsLog: { + id: string; + userExit: boolean; + analyticsId: string; + }; +} export const trackAnalyticsLogExit = async ({ id, @@ -9,16 +17,16 @@ export const trackAnalyticsLogExit = async ({ isUserExit: boolean; }) => { try { - const result = await adminClient.request( + const result = await $public.client.request( gql` mutation UpdateAnalyticsLogUserExit($id: bigint!, $user_exit: Boolean) { - update_analytics_logs_by_pk( + analyticsLog: update_analytics_logs_by_pk( pk_columns: { id: $id } _set: { user_exit: $user_exit } ) { id - user_exit - analytics_id + userExit: user_exit + analyticsId: analytics_id } } `, @@ -28,8 +36,8 @@ export const trackAnalyticsLogExit = async ({ }, ); - const analyticsId = result.update_analytics_logs_by_pk.analytics_id; - await adminClient.request( + const analyticsId = result.analyticsLog.analyticsId; + await $public.client.request( gql` mutation SetAnalyticsEndedDate($id: bigint!, $ended_at: timestamptz) { update_analytics_by_pk( diff --git a/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/mocks/queries.ts b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/mocks/queries.ts index ea4a041122..73be6c851a 100644 --- a/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/mocks/queries.ts +++ b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/mocks/queries.ts @@ -4,7 +4,7 @@ export const mockSanitiseLowcalSessionsMutation = { name: "SanitiseLowcalSessions", matchOnVariables: false, data: { - update_lowcal_sessions: { + sessions: { returning: mockIds, }, }, @@ -14,7 +14,7 @@ export const mockSanitiseUniformApplicationsMutation = { name: "SanitiseUniformApplications", matchOnVariables: false, data: { - update_uniform_applications: { + uniformApplications: { returning: mockIds, }, }, @@ -24,7 +24,7 @@ export const mockSanitiseBOPSApplicationsMutation = { name: "SanitiseBOPSApplications", matchOnVariables: false, data: { - update_bops_applications: { + bopsApplications: { returning: mockIds, }, }, @@ -34,7 +34,7 @@ export const mockSanitiseEmailApplicationsMutation = { name: "SanitiseEmailApplications", matchOnVariables: false, data: { - update_email_applications: { + emailApplications: { returning: mockIds, }, }, @@ -44,7 +44,7 @@ export const mockDeleteReconciliationRequestsMutation = { name: "DeleteReconciliationRequests", matchOnVariables: false, data: { - delete_reconciliation_requests: { + reconciliationRequests: { returning: mockIds, }, }, @@ -54,7 +54,7 @@ export const mockDeletePaymentRequests = { name: "DeletePaymentRequests", matchOnVariables: false, data: { - delete_payment_requests: { + paymentRequests: { returning: mockIds, }, }, diff --git a/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.test.ts b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.test.ts index eba539c049..c6d77a547b 100644 --- a/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.test.ts +++ b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.test.ts @@ -29,12 +29,25 @@ jest.mock("../../../../hasura/schema"); const mockRunSQL = runSQL as jest.MockedFunction; const mockFindSession = jest.fn(); -jest.mock("../../../../client", () => { + +jest.mock("@opensystemslab/planx-core", () => { + const actualCoreDomainClient = jest.requireActual( + "@opensystemslab/planx-core", + ).CoreDomainClient; + + const actualPassport = jest.requireActual( + "@opensystemslab/planx-core", + ).Passport; + return { - $api: { - session: { - find: jest.fn().mockImplementation(() => mockFindSession()), - }, + Passport: actualPassport, + CoreDomainClient: class extends actualCoreDomainClient { + constructor() { + super(); + this.session.find = jest + .fn() + .mockImplementation(() => mockFindSession()); + } }, }; }); diff --git a/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.ts b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.ts index 31d29ed578..b3c19658fd 100644 --- a/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.ts +++ b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.ts @@ -1,11 +1,11 @@ import { gql } from "graphql-request"; import { subMonths } from "date-fns"; -import { Operation, OperationResult } from "./types"; -import { adminGraphQLClient } from "../../../../hasura"; +import { Operation, OperationResult, QueryResult } from "./types"; import { runSQL } from "../../../../hasura/schema"; import { getFilesForSession } from "../../../../session/files"; import { deleteFilesByURL } from "../../../../s3/deleteFile"; +import { $api } from "../../../../client"; const RETENTION_PERIOD_MONTHS = 6; export const getRetentionPeriod = () => @@ -78,10 +78,12 @@ export const getExpiredSessionIds = async (): Promise => { `; const { lowcal_sessions: sessions, - }: { lowcal_sessions: Record<"id", string>[] } = - await adminGraphQLClient.request(query, { + }: { lowcal_sessions: Record<"id", string>[] } = await $api.client.request( + query, + { retentionPeriod: getRetentionPeriod(), - }); + }, + ); const sessionIds = sessions.map((session) => session.id); return sessionIds; }; @@ -105,10 +107,12 @@ export const deleteApplicationFiles: Operation = async () => { return deletedFiles; }; +type Result = { returning: QueryResult }; + export const sanitiseLowcalSessions: Operation = async () => { const mutation = gql` mutation SanitiseLowcalSessions($retentionPeriod: timestamptz) { - update_lowcal_sessions( + sessions: update_lowcal_sessions( _set: { data: {}, email: "", sanitised_at: "now()" } where: { sanitised_at: { _is_null: true } @@ -125,8 +129,8 @@ export const sanitiseLowcalSessions: Operation = async () => { } `; const { - update_lowcal_sessions: { returning: result }, - } = await adminGraphQLClient.request(mutation, { + sessions: { returning: result }, + } = await $api.client.request<{ sessions: Result }>(mutation, { retentionPeriod: getRetentionPeriod(), }); return result; @@ -135,7 +139,7 @@ export const sanitiseLowcalSessions: Operation = async () => { export const sanitiseUniformApplications: Operation = async () => { const mutation = gql` mutation SanitiseUniformApplications($retentionPeriod: timestamptz) { - update_uniform_applications( + uniformApplications: update_uniform_applications( _set: { payload: null, sanitised_at: "now()" } where: { sanitised_at: { _is_null: true } @@ -149,8 +153,8 @@ export const sanitiseUniformApplications: Operation = async () => { } `; const { - update_uniform_applications: { returning: result }, - } = await adminGraphQLClient.request(mutation, { + uniformApplications: { returning: result }, + } = await $api.client.request<{ uniformApplications: Result }>(mutation, { retentionPeriod: getRetentionPeriod(), }); return result; @@ -159,7 +163,7 @@ export const sanitiseUniformApplications: Operation = async () => { export const sanitiseBOPSApplications: Operation = async () => { const mutation = gql` mutation SanitiseBOPSApplications($retentionPeriod: timestamptz) { - update_bops_applications( + bopsApplications: update_bops_applications( _set: { request: {}, sanitised_at: "now()" } where: { sanitised_at: { _is_null: true } @@ -173,8 +177,8 @@ export const sanitiseBOPSApplications: Operation = async () => { } `; const { - update_bops_applications: { returning: result }, - } = await adminGraphQLClient.request(mutation, { + bopsApplications: { returning: result }, + } = await $api.client.request<{ bopsApplications: Result }>(mutation, { retentionPeriod: getRetentionPeriod(), }); return result; @@ -183,7 +187,7 @@ export const sanitiseBOPSApplications: Operation = async () => { export const sanitiseEmailApplications: Operation = async () => { const mutation = gql` mutation SanitiseEmailApplications($retentionPeriod: timestamptz) { - update_email_applications( + emailApplications: update_email_applications( _set: { request: {}, sanitised_at: "now()" } where: { sanitised_at: { _is_null: true } @@ -197,8 +201,8 @@ export const sanitiseEmailApplications: Operation = async () => { } `; const { - update_email_applications: { returning: result }, - } = await adminGraphQLClient.request(mutation, { + emailApplications: { returning: result }, + } = await $api.client.request<{ emailApplications: Result }>(mutation, { retentionPeriod: getRetentionPeriod(), }); return result; @@ -207,7 +211,7 @@ export const sanitiseEmailApplications: Operation = async () => { export const deleteReconciliationRequests: Operation = async () => { const mutation = gql` mutation DeleteReconciliationRequests($retentionPeriod: timestamptz) { - delete_reconciliation_requests( + reconciliationRequests: delete_reconciliation_requests( where: { created_at: { _lt: $retentionPeriod } } ) { returning { @@ -217,8 +221,8 @@ export const deleteReconciliationRequests: Operation = async () => { } `; const { - delete_reconciliation_requests: { returning: result }, - } = await adminGraphQLClient.request(mutation, { + reconciliationRequests: { returning: result }, + } = await $api.client.request<{ reconciliationRequests: Result }>(mutation, { retentionPeriod: getRetentionPeriod(), }); return result; @@ -227,7 +231,7 @@ export const deleteReconciliationRequests: Operation = async () => { export const deletePaymentRequests: Operation = async () => { const mutation = gql` mutation DeletePaymentRequests($retentionPeriod: timestamptz) { - delete_payment_requests( + paymentRequests: delete_payment_requests( where: { created_at: { _lt: $retentionPeriod } } ) { returning { @@ -237,8 +241,8 @@ export const deletePaymentRequests: Operation = async () => { } `; const { - delete_payment_requests: { returning: result }, - } = await adminGraphQLClient.request(mutation, { + paymentRequests: { returning: result }, + } = await $api.client.request<{ paymentRequests: Result }>(mutation, { retentionPeriod: getRetentionPeriod(), }); return result; diff --git a/api.planx.uk/notify/routeSendEmailRequest.test.ts b/api.planx.uk/notify/routeSendEmailRequest.test.ts index 35bfbcbfb7..b96c671be4 100644 --- a/api.planx.uk/notify/routeSendEmailRequest.test.ts +++ b/api.planx.uk/notify/routeSendEmailRequest.test.ts @@ -217,9 +217,7 @@ describe("Send Email endpoint", () => { const softDeleteSessionMock = queryMock .getCalls() .find((mock) => mock.id === "SoftDeleteLowcalSession"); - expect( - softDeleteSessionMock?.response.data.update_lowcal_sessions_by_pk.id, - ).toEqual("123"); + expect(softDeleteSessionMock?.response.data.session.id).toEqual("123"); }); }); }); diff --git a/api.planx.uk/saveAndReturn/resumeApplication.test.ts b/api.planx.uk/saveAndReturn/resumeApplication.test.ts index 4862a74d32..3e74b48c18 100644 --- a/api.planx.uk/saveAndReturn/resumeApplication.test.ts +++ b/api.planx.uk/saveAndReturn/resumeApplication.test.ts @@ -4,31 +4,33 @@ import app from "../server"; import { queryMock } from "../tests/graphqlQueryMock"; import { mockLowcalSession, mockTeam } from "../tests/mocks/saveAndReturnMocks"; import { buildContentFromSessions } from "./resumeApplication"; +import { PartialDeep } from "type-fest"; const ENDPOINT = "/resume-application"; const TEST_EMAIL = "simulate-delivered@notifications.service.gov.uk"; -type DeepPartial = T extends object - ? { - [P in keyof T]?: DeepPartial; - } - : T; - const mockFormatRawProjectTypes = jest .fn() .mockResolvedValue(["New office premises"]); jest.mock("@opensystemslab/planx-core", () => { + const actualCoreDomainClient = jest.requireActual( + "@opensystemslab/planx-core", + ).CoreDomainClient; + return { - CoreDomainClient: jest.fn().mockImplementation(() => ({ - formatRawProjectTypes: () => mockFormatRawProjectTypes(), - })), + CoreDomainClient: class extends actualCoreDomainClient { + constructor() { + super(); + this.formatRawProjectTypes = () => mockFormatRawProjectTypes(); + } + }, }; }); describe("buildContentFromSessions function", () => { it("should return correctly formatted content for a single session", async () => { - const sessions: DeepPartial[] = [ + const sessions: PartialDeep[] = [ { data: { passport: { @@ -62,7 +64,7 @@ describe("buildContentFromSessions function", () => { }); it("should return correctly formatted content for multiple session", async () => { - const sessions: DeepPartial[] = [ + const sessions: PartialDeep[] = [ { data: { passport: { @@ -137,7 +139,7 @@ describe("buildContentFromSessions function", () => { }); it("should handle an empty address field", async () => { - const sessions: DeepPartial[] = [ + const sessions: PartialDeep[] = [ { data: { passport: { @@ -170,7 +172,7 @@ describe("buildContentFromSessions function", () => { it("should handle an empty project type field", async () => { mockFormatRawProjectTypes.mockResolvedValueOnce(""); - const sessions: DeepPartial[] = [ + const sessions: PartialDeep[] = [ { data: { passport: { @@ -227,7 +229,7 @@ describe("Resume Application endpoint", () => { queryMock.mockQuery({ name: "ValidateRequest", - data: { teams: null, lowcal_sessions: null }, + data: { teams: null, sessions: null }, variables: body.payload, }); @@ -249,7 +251,7 @@ describe("Resume Application endpoint", () => { queryMock.mockQuery({ name: "ValidateRequest", data: { - lowcal_sessions: [mockLowcalSession], + sessions: [mockLowcalSession], teams: [mockTeam], }, variables: body.payload, @@ -269,7 +271,7 @@ describe("Resume Application endpoint", () => { queryMock.mockQuery({ name: "ValidateRequest", - data: { teams: [mockTeam], lowcal_sessions: [] }, + data: { teams: [mockTeam], sessions: [] }, variables: body.payload, }); diff --git a/api.planx.uk/saveAndReturn/resumeApplication.ts b/api.planx.uk/saveAndReturn/resumeApplication.ts index 292636fb79..aadc31fc9f 100644 --- a/api.planx.uk/saveAndReturn/resumeApplication.ts +++ b/api.planx.uk/saveAndReturn/resumeApplication.ts @@ -1,11 +1,10 @@ import { NextFunction, Request, Response } from "express"; import { gql } from "graphql-request"; -import { adminGraphQLClient as adminClient } from "../hasura"; import { LowCalSession, Team } from "../types"; import { convertSlugToName, getResumeLink, calculateExpiryDate } from "./utils"; import { sendEmail } from "../notify"; import type { SiteAddress } from "@opensystemslab/planx-core/types"; -import { $public } from "../client"; +import { $api, $public } from "../client"; /** * Send a "Resume" email to an applicant which list all open applications for a given council (team) @@ -42,6 +41,11 @@ const resumeApplication = async ( } }; +interface ValidateRequest { + teams: Team[]; + lowcalSessions: LowCalSession[] | null; +} + /** * Validate that there are sessions matching the request * XXX: Admin role is required here as we are relying on the combination of email @@ -57,7 +61,7 @@ const validateRequest = async ( try { const query = gql` query ValidateRequest($email: String, $teamSlug: String) { - lowcal_sessions( + sessions( where: { email: { _eq: $email } deleted_at: { _is_null: true } @@ -81,16 +85,17 @@ const validateRequest = async ( } } `; - const { lowcal_sessions, teams } = await adminClient.request(query, { - teamSlug, - email: email.toLowerCase(), - }); + const { lowcalSessions, teams } = + await $api.client.request(query, { + teamSlug, + email: email.toLowerCase(), + }); if (!teams?.length) throw Error; return { team: teams[0], - sessions: lowcal_sessions, + sessions: lowcalSessions || [], }; } catch (error) { throw Error("Unable to validate request"); diff --git a/api.planx.uk/saveAndReturn/utils.ts b/api.planx.uk/saveAndReturn/utils.ts index 03fc21adad..7080861d1d 100644 --- a/api.planx.uk/saveAndReturn/utils.ts +++ b/api.planx.uk/saveAndReturn/utils.ts @@ -1,10 +1,9 @@ import { SiteAddress } from "@opensystemslab/planx-core/types"; import { format, addDays } from "date-fns"; import { gql } from "graphql-request"; -import { adminGraphQLClient as adminClient } from "../hasura"; import { LowCalSession, Team } from "../types"; import { Template, getClientForTemplate, sendEmail } from "../notify"; -import { $public } from "../client"; +import { $api, $public } from "../client"; const DAYS_UNTIL_EXPIRY = 28; const REMINDER_DAYS_FROM_EXPIRY = [7, 1]; @@ -201,7 +200,7 @@ const softDeleteSession = async (sessionId: string) => { } } `; - await adminClient.request(mutation, { sessionId }); + await $api.client.request(mutation, { sessionId }); } catch (error) { throw new Error(`Error deleting session ${sessionId}`); } @@ -223,7 +222,7 @@ const markSessionAsSubmitted = async (sessionId: string) => { } } `; - await adminClient.request(mutation, { sessionId }); + await $api.client.request(mutation, { sessionId }); } catch (error) { throw new Error(`Error marking session ${sessionId} as submitted`); } @@ -238,6 +237,10 @@ const getSaveAndReturnPublicHeaders = (sessionId: string, email: string) => ({ "x-hasura-lowcal-email": email.toLowerCase(), }); +interface SetupEmailNotifications { + session: { hasUserSaved: boolean }; +} + // Update lowcal_sessions.has_user_saved column to kick-off the setup_lowcal_expiry_events & // setup_lowcal_reminder_events event triggers in Hasura // Should only run once on initial save of a session @@ -245,18 +248,20 @@ const setupEmailEventTriggers = async (sessionId: string) => { try { const mutation = gql` mutation SetupEmailNotifications($sessionId: uuid!) { - update_lowcal_sessions_by_pk( + session: update_lowcal_sessions_by_pk( pk_columns: { id: $sessionId } _set: { has_user_saved: true } ) { id - has_user_saved + hasUserSaved: has_user_saved } } `; const { - update_lowcal_sessions_by_pk: { has_user_saved: hasUserSaved }, - } = await adminClient.request(mutation, { sessionId }); + session: { hasUserSaved }, + } = await $api.client.request(mutation, { + sessionId, + }); return hasUserSaved; } catch (error) { throw new Error( diff --git a/api.planx.uk/saveAndReturn/validateSession.test.ts b/api.planx.uk/saveAndReturn/validateSession.test.ts index d5707a9c82..d7cd811819 100644 --- a/api.planx.uk/saveAndReturn/validateSession.test.ts +++ b/api.planx.uk/saveAndReturn/validateSession.test.ts @@ -13,12 +13,24 @@ import { stubUpdateLowcalSessionData, } from "../tests/mocks/saveAndReturnMocks"; import type { Node, Flow, Breadcrumb } from "../types"; +import { userContext } from "../modules/auth/middleware"; +import { getJWT } from "../tests/mockJWT"; const validateSessionPath = "/validate-session"; +const getStoreMock = jest.spyOn(userContext, "getStore"); describe("Validate Session endpoint", () => { const reconciledData = omit(mockLowcalSession.data, "passport"); + beforeEach(() => { + getStoreMock.mockReturnValue({ + user: { + sub: "123", + jwt: getJWT({ role: "teamEditor" }), + }, + }); + }); + afterEach(() => { queryMock.reset(); }); diff --git a/api.planx.uk/saveAndReturn/validateSession.ts b/api.planx.uk/saveAndReturn/validateSession.ts index 858fdb6beb..374ad2d7e7 100644 --- a/api.planx.uk/saveAndReturn/validateSession.ts +++ b/api.planx.uk/saveAndReturn/validateSession.ts @@ -1,7 +1,6 @@ import { gql } from "graphql-request"; import omit from "lodash.omit"; import { NextFunction, Request, Response } from "express"; -import { adminGraphQLClient as adminClient } from "../hasura"; import { getMostRecentPublishedFlow } from "../helpers"; import { sortBreadcrumbs } from "@opensystemslab/planx-core"; import { ComponentType } from "@opensystemslab/planx-core/types"; @@ -16,6 +15,7 @@ import type { PublishedFlow, Node, } from "../types"; +import { $api } from "../client"; export interface ValidationResponse { message: string; @@ -210,7 +210,7 @@ async function diffLatestPublishedFlow({ }): Promise { const response: { diff_latest_published_flow: { data: PublishedFlow["data"] | null }; - } = await adminClient.request( + } = await $api.client.request( gql` query GetFlowDiff($flowId: uuid!, $since: timestamptz!) { diff_latest_published_flow( @@ -225,6 +225,10 @@ async function diffLatestPublishedFlow({ return response.diff_latest_published_flow.data; } +interface FindSession { + sessions: Partial[]; +} + async function findSession({ sessionId, email, @@ -232,38 +236,35 @@ async function findSession({ sessionId: string; email: string; }): Promise | undefined> { - const response: { lowcal_sessions: Partial[] } = - await adminClient.request( - gql` - query FindSession($sessionId: uuid!, $email: String!) { - lowcal_sessions( - where: { id: { _eq: $sessionId }, email: { _eq: $email } } - limit: 1 - ) { - flow_id - data - updated_at - lockedAt: locked_at - paymentRequests: payment_requests { - id - payeeName: payee_name - payeeEmail: payee_email - } + const response = await $api.client.request( + gql` + query FindSession($sessionId: uuid!, $email: String!) { + sessions: lowcal_sessions( + where: { id: { _eq: $sessionId }, email: { _eq: $email } } + limit: 1 + ) { + flow_id + data + updated_at + lockedAt: locked_at + paymentRequests: payment_requests { + id + payeeName: payee_name + payeeEmail: payee_email } } - `, - { sessionId, email }, - ); - return response.lowcal_sessions.length - ? response.lowcal_sessions[0] - : undefined; + } + `, + { sessionId, email }, + ); + return response.sessions.length ? response.sessions[0] : undefined; } async function createAuditEntry( sessionId: string, data: ValidationResponse, ): Promise { - await adminClient.request( + await $api.client.request( gql` mutation InsertReconciliationRequests( $session_id: String = "" diff --git a/api.planx.uk/send/bops.test.ts b/api.planx.uk/send/bops.test.ts index dbed9228d4..65d2030b47 100644 --- a/api.planx.uk/send/bops.test.ts +++ b/api.planx.uk/send/bops.test.ts @@ -9,16 +9,21 @@ jest.mock("../saveAndReturn/utils", () => ({ })); jest.mock("@opensystemslab/planx-core", () => { + const actualCoreDomainClient = jest.requireActual( + "@opensystemslab/planx-core", + ).CoreDomainClient; + return { - CoreDomainClient: jest.fn().mockImplementation(() => ({ - export: { - bopsPayload: () => + CoreDomainClient: class extends actualCoreDomainClient { + constructor() { + super(); + this.export.bopsPayload = () => jest.fn().mockResolvedValue({ exportData: expectedPayload, redactedExportData: expectedPayload, - }), - }, - })), + }); + } + }, }; }); @@ -29,7 +34,7 @@ describe(`sending an application to BOPS`, () => { queryMock.mockQuery({ name: "FindApplication", data: { - bops_applications: [], + bopsApplications: [], }, variables: { session_id: "123" }, }); @@ -38,7 +43,7 @@ describe(`sending an application to BOPS`, () => { name: "CreateBopsApplication", matchOnVariables: false, data: { - insert_bops_applications_one: { id: 22 }, + insertBopsApplication: { id: 22 }, }, }); }); @@ -93,7 +98,7 @@ describe(`sending an application to BOPS`, () => { queryMock.mockQuery({ name: "FindApplication", data: { - bops_applications: [ + bopsApplications: [ { response: { message: "Application created", id: "bops_app_id" } }, ], }, diff --git a/api.planx.uk/send/bops.ts b/api.planx.uk/send/bops.ts index 80d1edebb1..6d6d4a4956 100644 --- a/api.planx.uk/send/bops.ts +++ b/api.planx.uk/send/bops.ts @@ -1,5 +1,4 @@ import axios, { AxiosResponse } from "axios"; -import { adminGraphQLClient as adminClient } from "../hasura"; import { markSessionAsSubmitted } from "../saveAndReturn/utils"; import { NextFunction, Request, Response } from "express"; import { gql } from "graphql-request"; @@ -12,6 +11,13 @@ interface SendToBOPSRequest { }; } +interface CreateBopsApplication { + insertBopsApplication: { + id: string; + bopsId: string; + }; +} + /** * @swagger * /bops/{localAuthority}: @@ -87,7 +93,7 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => { // Mark session as submitted so that reminder and expiry emails are not triggered markSessionAsSubmitted(payload?.sessionId); - const applicationId = await adminClient.request( + const applicationId = await $api.client.request( gql` mutation CreateBopsApplication( $bops_id: String = "" @@ -98,7 +104,7 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => { $response_headers: jsonb = {} $session_id: String! ) { - insert_bops_applications_one( + insertBopsApplication: insert_bops_applications_one( object: { bops_id: $bops_id destination_url: $destination_url @@ -110,7 +116,7 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => { } ) { id - bops_id + bopsId: bops_id } } `, @@ -126,7 +132,7 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => { return { application: { - ...applicationId.insert_bops_applications_one, + ...applicationId.insertBopsApplication, bopsResponse: res.data, }, }; @@ -159,16 +165,22 @@ const sendToBOPS = async (req: Request, res: Response, next: NextFunction) => { } }; +interface FindApplication { + bopsApplications: { + response: Record; + }[]; +} + /** * Query the BOPS audit table to see if we already have an application for this session */ async function checkBOPSAuditTable( sessionId: string, ): Promise> { - const application = await adminClient.request( + const application = await $api.client.request( gql` query FindApplication($session_id: String = "") { - bops_applications( + bopsApplications: bops_applications( where: { session_id: { _eq: $session_id } } order_by: { created_at: desc } ) { @@ -181,7 +193,7 @@ async function checkBOPSAuditTable( }, ); - return application?.bops_applications[0]?.response; + return application?.bopsApplications[0]?.response; } export { sendToBOPS }; diff --git a/api.planx.uk/send/email.test.ts b/api.planx.uk/send/email.test.ts index 398f6d8889..985213588e 100644 --- a/api.planx.uk/send/email.test.ts +++ b/api.planx.uk/send/email.test.ts @@ -9,17 +9,23 @@ const mockGenerateCSVData = jest.fn().mockResolvedValue([ metadata: {}, }, ]); + jest.mock("@opensystemslab/planx-core", () => { + const actualCoreDomainClient = jest.requireActual( + "@opensystemslab/planx-core", + ).CoreDomainClient; + return { Passport: jest.fn().mockImplementation(() => ({ files: jest.fn().mockImplementation(() => []), })), - CoreDomainClient: jest.fn().mockImplementation(() => ({ - getDocumentTemplateNamesForSession: jest.fn(), - export: { - csvData: () => mockGenerateCSVData(), - }, - })), + CoreDomainClient: class extends actualCoreDomainClient { + constructor() { + super(); + this.getDocumentTemplateNamesForSession = jest.fn(); + this.export.csvData = () => mockGenerateCSVData(); + } + }, }; }); @@ -55,7 +61,7 @@ describe(`sending an application by email to a planning office`, () => { name: "GetSessionData", matchOnVariables: false, data: { - lowcal_sessions_by_pk: { data: {} }, + session: { data: {} }, }, variables: { id: "123" }, }); @@ -64,7 +70,7 @@ describe(`sending an application by email to a planning office`, () => { name: "GetSessionEmailDetails", matchOnVariables: false, data: { - lowcal_sessions_by_pk: { + session: { email: "applicant@test.com", flow: { slug: "test-flow" }, }, @@ -76,7 +82,7 @@ describe(`sending an application by email to a planning office`, () => { name: "MarkSessionAsSubmitted", matchOnVariables: false, data: { - update_lowcal_sessions_by_pk: { id: "123" }, + session: { id: "123" }, }, variables: { sessionId: "123" }, }); @@ -85,7 +91,7 @@ describe(`sending an application by email to a planning office`, () => { name: "CreateEmailApplication", matchOnVariables: false, data: { - insert_email_applications_one: { id: 1 }, + application: { id: 1 }, }, variables: { sessionId: "123", @@ -181,7 +187,7 @@ describe(`downloading application data received by email`, () => { name: "GetSessionData", matchOnVariables: false, data: { - lowcal_sessions_by_pk: { data: { passport: { test: "dummy data" } } }, + session: { data: { passport: { test: "dummy data" } } }, }, variables: { id: "123" }, }); @@ -218,7 +224,7 @@ describe(`downloading application data received by email`, () => { "/download-application-files/123?email=planners@southwark.gov.uk&localAuthority=southwark", ) .expect(200) - .then(() => { + .then((_res) => { expect(mockBuildSubmissionExportZip).toHaveBeenCalledWith({ sessionId: "123", }); diff --git a/api.planx.uk/send/email.ts b/api.planx.uk/send/email.ts index 40ee5a360b..dadc5801b4 100644 --- a/api.planx.uk/send/email.ts +++ b/api.planx.uk/send/email.ts @@ -1,11 +1,13 @@ import type { NextFunction, Request, Response } from "express"; import { gql } from "graphql-request"; import capitalize from "lodash/capitalize"; -import { adminGraphQLClient as adminClient } from "../hasura"; import { markSessionAsSubmitted } from "../saveAndReturn/utils"; import { sendEmail } from "../notify"; import { EmailSubmissionNotifyConfig } from "../types"; import { buildSubmissionExportZip } from "./exportZip"; +import { $api, $public } from "../client"; +import { NotifyPersonalisation } from "@opensystemslab/planx-core/dist/types/team"; +import { Session } from "@opensystemslab/planx-core/types"; /** * @swagger @@ -158,8 +160,15 @@ export async function downloadApplicationFiles( } } +interface GetTeamEmailSettings { + teams: { + sendToEmail: string; + notifyPersonalisation: NotifyPersonalisation; + }[]; +} + async function getTeamEmailSettings(localAuthority: string) { - const response = await adminClient.request( + const response = await $api.client.request( gql` query GetTeamEmailSettings($slug: String) { teams(where: { slug: { _eq: $slug } }) { @@ -176,11 +185,20 @@ async function getTeamEmailSettings(localAuthority: string) { return response?.teams[0]; } +interface GetSessionEmailDetailsById { + session: { + email: string; + flow: { + slug: string; + }; + } | null; +} + async function getSessionEmailDetailsById(sessionId: string) { - const response = await adminClient.request( + const response = await $api.client.request( gql` query GetSessionEmailDetails($id: uuid!) { - lowcal_sessions_by_pk(id: $id) { + session: lowcal_sessions_by_pk(id: $id) { email flow { slug @@ -193,14 +211,23 @@ async function getSessionEmailDetailsById(sessionId: string) { }, ); - return response?.lowcal_sessions_by_pk; + if (!response.session) + throw Error( + `Cannot find session ${sessionId} in GetSessionEmailDetails query`, + ); + + return response.session; +} + +interface GetSessionData { + session: Partial>; } async function getSessionData(sessionId: string) { - const response = await adminClient.request( + const response = await $api.client.request( gql` query GetSessionData($id: uuid!) { - lowcal_sessions_by_pk(id: $id) { + session: lowcal_sessions_by_pk(id: $id) { data } } @@ -210,7 +237,13 @@ async function getSessionData(sessionId: string) { }, ); - return response?.lowcal_sessions_by_pk?.data; + return response?.session?.data; +} + +interface CreateEmailApplication { + application: { + id?: string; + }; } async function insertAuditEntry( @@ -223,7 +256,7 @@ async function insertAuditEntry( expiryDate?: string; }, ) { - const response = await adminClient.request( + const response = await $api.client.request( gql` mutation CreateEmailApplication( $session_id: uuid! @@ -232,7 +265,7 @@ async function insertAuditEntry( $request: jsonb $response: jsonb ) { - insert_email_applications_one( + application: insert_email_applications_one( object: { session_id: $session_id team_slug: $team_slug @@ -254,5 +287,5 @@ async function insertAuditEntry( }, ); - return response?.insert_email_applications_one?.id; + return response?.application?.id; } diff --git a/api.planx.uk/send/helpers.ts b/api.planx.uk/send/helpers.ts index c9360e88c2..eca20463a5 100644 --- a/api.planx.uk/send/helpers.ts +++ b/api.planx.uk/send/helpers.ts @@ -1,6 +1,6 @@ import { gql } from "graphql-request"; import airbrake from "../airbrake"; -import { adminGraphQLClient } from "../hasura"; +import { $api } from "../client"; export async function logPaymentStatus({ sessionId, @@ -78,7 +78,7 @@ async function insertPaymentStatus({ status: string; amount: number; }): Promise { - const _response = await adminGraphQLClient.request( + const _response = await $api.client.request( gql` mutation InsertPaymentStatus( $flowId: uuid! diff --git a/api.planx.uk/send/uniform.ts b/api.planx.uk/send/uniform.ts index 4ec34f1b0c..2e67440fa9 100644 --- a/api.planx.uk/send/uniform.ts +++ b/api.planx.uk/send/uniform.ts @@ -3,7 +3,6 @@ import { NextFunction, Request, Response } from "express"; import { Buffer } from "node:buffer"; import FormData from "form-data"; import fs from "fs"; -import { adminGraphQLClient as adminClient } from "../hasura"; import { markSessionAsSubmitted } from "../saveAndReturn/utils"; import { gql } from "graphql-request"; import { $api } from "../client"; @@ -174,7 +173,7 @@ async function checkUniformAuditTable( sessionId: string, ): Promise { const application: Record<"uniform_applications", UniformApplication[]> = - await adminClient.request( + await $api.client.request( gql` query FindApplication($submission_reference: String = "") { uniform_applications( @@ -390,7 +389,7 @@ const createUniformApplicationAuditRecord = async ({ const application: Record< "insert_uniform_applications_one", UniformApplication - > = await adminClient.request( + > = await $api.client.request( gql` mutation CreateUniformApplication( $idox_submission_id: String = "" diff --git a/api.planx.uk/tests/mocks/saveAndReturnMocks.ts b/api.planx.uk/tests/mocks/saveAndReturnMocks.ts index 02393437d2..4e3828ca70 100644 --- a/api.planx.uk/tests/mocks/saveAndReturnMocks.ts +++ b/api.planx.uk/tests/mocks/saveAndReturnMocks.ts @@ -73,7 +73,7 @@ export const mockLowcalSession: LowCalSession = { export const mockFindSession = (breadcrumbs = {}) => ({ name: "FindSession", data: { - lowcal_sessions: [ + sessions: [ { ...mockLowcalSession, data: { @@ -92,7 +92,7 @@ export const mockFindSession = (breadcrumbs = {}) => ({ export const mockNotFoundSession = { name: "FindSession", data: { - lowcal_sessions: [], + sessions: [], }, variables: { sessionId: "not-found-id", @@ -103,8 +103,8 @@ export const mockNotFoundSession = { export const mockGetMostRecentPublishedFlow = (data: Flow["data"]) => ({ name: "GetMostRecentPublishedFlow", data: { - flows_by_pk: { - published_flows: [ + flow: { + publishedFlows: [ { data, }, @@ -154,7 +154,7 @@ export const mockValidateSingleSessionRequest = { export const mockSoftDeleteLowcalSession = { name: "SoftDeleteLowcalSession", data: { - update_lowcal_sessions_by_pk: { + session: { id: "123", }, }, @@ -166,9 +166,9 @@ export const mockSoftDeleteLowcalSession = { export const mockSetupEmailNotifications = { name: "SetupEmailNotifications", data: { - update_lowcal_sessions_by_pk: { + session: { id: "123", - has_user_saved: true, + hasUserSaved: true, }, }, variables: { diff --git a/hasura.planx.uk/metadata/tables.yaml b/hasura.planx.uk/metadata/tables.yaml index e3fb973ab0..c458b88e8a 100644 --- a/hasura.planx.uk/metadata/tables.yaml +++ b/hasura.planx.uk/metadata/tables.yaml @@ -1116,6 +1116,16 @@ - message - session_id - created_at + select_permissions: + - role: api + permission: + columns: + - id + - response + - message + - session_id + - created_at + filter: {} delete_permissions: - role: api permission: diff --git a/hasura.planx.uk/tests/reconciliation_requests.test.js b/hasura.planx.uk/tests/reconciliation_requests.test.js index 482f98723d..9e8f5812d6 100644 --- a/hasura.planx.uk/tests/reconciliation_requests.test.js +++ b/hasura.planx.uk/tests/reconciliation_requests.test.js @@ -69,8 +69,8 @@ describe("reconciliation_requests", () => { i = await introspectAs("api"); }); - test("cannot query reconciliation_requests", () => { - expect(i.queries).not.toContain("reconciliation_requests"); + test("can query reconciliation_requests", () => { + expect(i.queries).toContain("reconciliation_requests"); }); test("cannot update reconciliation_requests", () => { @@ -83,6 +83,7 @@ describe("reconciliation_requests", () => { test("can insert reconciliation requests", () => { expect(i.mutations).toContain("insert_reconciliation_requests"); + expect(i.mutations).toContain("insert_reconciliation_requests_one"); }); }); });