From 8fb8e0699e0c046fc4ada1f0aa04127edfc2c394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Mon, 2 Oct 2023 16:13:08 +0100 Subject: [PATCH 1/8] feat: Add payment exemption status to Slack notifications (#2251) * refactor: Move webhook routes from server to module * refactor: Update sendSlackNotification to modular structure * test: Add email-submission tests * refactor: Rename and restructure folders, simplify service * feat: Add exemption status to notification * docs: Add Swagger docs * fix: Don't use fee to calculate exemption statuses --- .../_old}/lowcalSessionEvents.test.ts | 6 +- .../webhooks/_old}/lowcalSessionEvents.ts | 4 +- .../_old}/paymentRequestEvents.test.ts | 6 +- .../webhooks/_old}/paymentRequestEvents.ts | 4 +- .../sanitiseApplicationData/index.test.ts | 2 +- .../_old}/sanitiseApplicationData/index.ts | 2 +- .../sanitiseApplicationData/mocks/queries.ts | 0 .../operations.test.ts | 8 +- .../sanitiseApplicationData/operations.ts | 8 +- .../_old}/sanitiseApplicationData/types.d.ts | 0 api.planx.uk/modules/webhooks/controller.ts | 31 ++ api.planx.uk/modules/webhooks/docs.yaml | 135 +++++++ api.planx.uk/modules/webhooks/routes.ts | 40 ++ .../webhooks/sendNotification/index.test.ts | 345 ++++++++++++++++++ .../webhooks/sendNotification/schema.ts | 68 ++++ .../webhooks/sendNotification/service.ts | 64 ++++ .../webhooks/sendNotification/types.ts | 33 ++ api.planx.uk/server.ts | 36 +- api.planx.uk/shared/middleware/validate.ts | 4 +- .../webhooks/sendNotification.test.ts | 186 ---------- api.planx.uk/webhooks/sendNotifications.ts | 78 ---- 21 files changed, 740 insertions(+), 320 deletions(-) rename api.planx.uk/{webhooks => modules/webhooks/_old}/lowcalSessionEvents.test.ts (97%) rename api.planx.uk/{webhooks => modules/webhooks/_old}/lowcalSessionEvents.ts (94%) rename api.planx.uk/{webhooks => modules/webhooks/_old}/paymentRequestEvents.test.ts (98%) rename api.planx.uk/{webhooks => modules/webhooks/_old}/paymentRequestEvents.ts (97%) rename api.planx.uk/{webhooks => modules/webhooks/_old}/sanitiseApplicationData/index.test.ts (99%) rename api.planx.uk/{webhooks => modules/webhooks/_old}/sanitiseApplicationData/index.ts (96%) rename api.planx.uk/{webhooks => modules/webhooks/_old}/sanitiseApplicationData/mocks/queries.ts (100%) rename api.planx.uk/{webhooks => modules/webhooks/_old}/sanitiseApplicationData/operations.test.ts (96%) rename api.planx.uk/{webhooks => modules/webhooks/_old}/sanitiseApplicationData/operations.ts (96%) rename api.planx.uk/{webhooks => modules/webhooks/_old}/sanitiseApplicationData/types.d.ts (100%) create mode 100644 api.planx.uk/modules/webhooks/controller.ts create mode 100644 api.planx.uk/modules/webhooks/docs.yaml create mode 100644 api.planx.uk/modules/webhooks/routes.ts create mode 100644 api.planx.uk/modules/webhooks/sendNotification/index.test.ts create mode 100644 api.planx.uk/modules/webhooks/sendNotification/schema.ts create mode 100644 api.planx.uk/modules/webhooks/sendNotification/service.ts create mode 100644 api.planx.uk/modules/webhooks/sendNotification/types.ts delete mode 100644 api.planx.uk/webhooks/sendNotification.test.ts delete mode 100644 api.planx.uk/webhooks/sendNotifications.ts diff --git a/api.planx.uk/webhooks/lowcalSessionEvents.test.ts b/api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.test.ts similarity index 97% rename from api.planx.uk/webhooks/lowcalSessionEvents.test.ts rename to api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.test.ts index 0158b7f888..0cddfe272b 100644 --- a/api.planx.uk/webhooks/lowcalSessionEvents.test.ts +++ b/api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.test.ts @@ -1,10 +1,10 @@ import supertest from "supertest"; -import app from "../server"; -import { createScheduledEvent } from "../hasura/metadata"; +import app from "../../../server"; +import { createScheduledEvent } from "../../../hasura/metadata"; const { post } = supertest(app); -jest.mock("../hasura/metadata"); +jest.mock("../../../hasura/metadata"); const mockedCreateScheduledEvent = createScheduledEvent as jest.MockedFunction< typeof createScheduledEvent >; diff --git a/api.planx.uk/webhooks/lowcalSessionEvents.ts b/api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.ts similarity index 94% rename from api.planx.uk/webhooks/lowcalSessionEvents.ts rename to api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.ts index 407f951d82..1d50affdc3 100644 --- a/api.planx.uk/webhooks/lowcalSessionEvents.ts +++ b/api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.ts @@ -1,11 +1,11 @@ import { addDays } from "date-fns"; import { Request, Response, NextFunction } from "express"; -import { createScheduledEvent } from "../hasura/metadata"; +import { createScheduledEvent } from "../../../hasura/metadata"; import { DAYS_UNTIL_EXPIRY, REMINDER_DAYS_FROM_EXPIRY, -} from "../saveAndReturn/utils"; +} from "../../../saveAndReturn/utils"; /** * Create "reminder" events for a lowcal_session record diff --git a/api.planx.uk/webhooks/paymentRequestEvents.test.ts b/api.planx.uk/modules/webhooks/_old/paymentRequestEvents.test.ts similarity index 98% rename from api.planx.uk/webhooks/paymentRequestEvents.test.ts rename to api.planx.uk/modules/webhooks/_old/paymentRequestEvents.test.ts index eb0edb9e34..282ca011a5 100644 --- a/api.planx.uk/webhooks/paymentRequestEvents.test.ts +++ b/api.planx.uk/modules/webhooks/_old/paymentRequestEvents.test.ts @@ -1,10 +1,10 @@ import supertest from "supertest"; -import app from "../server"; -import { createScheduledEvent } from "../hasura/metadata"; +import app from "../../../server"; +import { createScheduledEvent } from "../../../hasura/metadata"; const { post } = supertest(app); -jest.mock("../hasura/metadata"); +jest.mock("../../../hasura/metadata"); const mockedCreateScheduledEvent = createScheduledEvent as jest.MockedFunction< typeof createScheduledEvent >; diff --git a/api.planx.uk/webhooks/paymentRequestEvents.ts b/api.planx.uk/modules/webhooks/_old/paymentRequestEvents.ts similarity index 97% rename from api.planx.uk/webhooks/paymentRequestEvents.ts rename to api.planx.uk/modules/webhooks/_old/paymentRequestEvents.ts index e9d2dda42e..30a387b228 100644 --- a/api.planx.uk/webhooks/paymentRequestEvents.ts +++ b/api.planx.uk/modules/webhooks/_old/paymentRequestEvents.ts @@ -1,11 +1,11 @@ import { addDays } from "date-fns"; import { Request, Response, NextFunction } from "express"; -import { createScheduledEvent } from "../hasura/metadata"; +import { createScheduledEvent } from "../../../hasura/metadata"; import { DAYS_UNTIL_EXPIRY, REMINDER_DAYS_FROM_EXPIRY, -} from "../saveAndReturn/utils"; +} from "../../../saveAndReturn/utils"; /** * Create two "invitation" events for a payments_request record: one for the nominee and one for the agent diff --git a/api.planx.uk/webhooks/sanitiseApplicationData/index.test.ts b/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/index.test.ts similarity index 99% rename from api.planx.uk/webhooks/sanitiseApplicationData/index.test.ts rename to api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/index.test.ts index 1eaf5b572e..96da6ca4b3 100644 --- a/api.planx.uk/webhooks/sanitiseApplicationData/index.test.ts +++ b/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/index.test.ts @@ -1,5 +1,5 @@ import supertest from "supertest"; -import app from "../../server"; +import app from "../../../../server"; import * as operations from "./operations"; const mockSend = jest.fn(); diff --git a/api.planx.uk/webhooks/sanitiseApplicationData/index.ts b/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/index.ts similarity index 96% rename from api.planx.uk/webhooks/sanitiseApplicationData/index.ts rename to api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/index.ts index 4716d520aa..9454104edf 100644 --- a/api.planx.uk/webhooks/sanitiseApplicationData/index.ts +++ b/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/index.ts @@ -3,7 +3,7 @@ import { Request, Response, NextFunction } from "express"; import { getOperations, operationHandler } from "./operations"; import { OperationResult } from "./types"; -import { getFormattedEnvironment } from "../../helpers"; +import { getFormattedEnvironment } from "../../../../helpers"; /** * Called by Hasura cron job `sanitise_application_data` on a nightly basis diff --git a/api.planx.uk/webhooks/sanitiseApplicationData/mocks/queries.ts b/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/mocks/queries.ts similarity index 100% rename from api.planx.uk/webhooks/sanitiseApplicationData/mocks/queries.ts rename to api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/mocks/queries.ts diff --git a/api.planx.uk/webhooks/sanitiseApplicationData/operations.test.ts b/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/operations.test.ts similarity index 96% rename from api.planx.uk/webhooks/sanitiseApplicationData/operations.test.ts rename to api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/operations.test.ts index 79e4e3a8a6..9e178f682e 100644 --- a/api.planx.uk/webhooks/sanitiseApplicationData/operations.test.ts +++ b/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/operations.test.ts @@ -1,5 +1,5 @@ -import { runSQL } from "../../hasura/schema"; -import { queryMock } from "../../tests/graphqlQueryMock"; +import { runSQL } from "../../../../hasura/schema"; +import { queryMock } from "../../../../tests/graphqlQueryMock"; import { mockIds, mockSanitiseBOPSApplicationsMutation, @@ -25,11 +25,11 @@ import { deleteHasuraScheduledEventsForSubmittedSessions, } from "./operations"; -jest.mock("../../hasura/schema"); +jest.mock("../../../../hasura/schema"); const mockRunSQL = runSQL as jest.MockedFunction; const mockFindSession = jest.fn(); -jest.mock("../../client", () => { +jest.mock("../../../../client", () => { return { $admin: { session: { diff --git a/api.planx.uk/webhooks/sanitiseApplicationData/operations.ts b/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/operations.ts similarity index 96% rename from api.planx.uk/webhooks/sanitiseApplicationData/operations.ts rename to api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/operations.ts index 2ab3a08d46..7e27194681 100644 --- a/api.planx.uk/webhooks/sanitiseApplicationData/operations.ts +++ b/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/operations.ts @@ -2,10 +2,10 @@ import { gql } from "graphql-request"; import { subMonths } from "date-fns"; import { Operation, OperationResult } from "./types"; -import { adminGraphQLClient } from "../../hasura"; -import { runSQL } from "../../hasura/schema"; -import { getFilesForSession } from "../../session/files"; -import { deleteFilesByURL } from "../../s3/deleteFile"; +import { adminGraphQLClient } from "../../../../hasura"; +import { runSQL } from "../../../../hasura/schema"; +import { getFilesForSession } from "../../../../session/files"; +import { deleteFilesByURL } from "../../../../s3/deleteFile"; const RETENTION_PERIOD_MONTHS = 6; export const getRetentionPeriod = () => diff --git a/api.planx.uk/webhooks/sanitiseApplicationData/types.d.ts b/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/types.d.ts similarity index 100% rename from api.planx.uk/webhooks/sanitiseApplicationData/types.d.ts rename to api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/types.d.ts diff --git a/api.planx.uk/modules/webhooks/controller.ts b/api.planx.uk/modules/webhooks/controller.ts new file mode 100644 index 0000000000..8b98358f89 --- /dev/null +++ b/api.planx.uk/modules/webhooks/controller.ts @@ -0,0 +1,31 @@ +import { ServerError } from "../../errors"; +import { sendSlackNotification } from "./sendNotification/service"; +import { SendSlackNotification } from "./sendNotification/types"; + +export const sendSlackNotificationController: SendSlackNotification = async ( + req, + res, + next, +) => { + const isProduction = process.env.APP_ENVIRONMENT === "production"; + if (!isProduction) { + return res.status(200).send({ + message: `Staging application submitted, skipping Slack notification`, + }); + } + + const eventData = req.body.event.data.new; + const eventType = req.query.type; + + try { + const data = await sendSlackNotification(eventData, eventType); + return res.status(200).send({ message: "Posted to Slack", data }); + } catch (error) { + return next( + new ServerError({ + message: `Failed to send ${eventType} Slack notification`, + cause: error, + }), + ); + } +}; diff --git a/api.planx.uk/modules/webhooks/docs.yaml b/api.planx.uk/modules/webhooks/docs.yaml new file mode 100644 index 0000000000..837e64a8ea --- /dev/null +++ b/api.planx.uk/modules/webhooks/docs.yaml @@ -0,0 +1,135 @@ +openapi: 3.1.0 +info: + title: Plan✕ API + version: 0.1.0 +tags: + - name: webhooks + description: Webhooks for event management +components: + schemas: + Payload: + type: object + properties: + sessionId: + type: string + required: + - sessionId + BopsSubmissionSchema: + type: object + properties: + body: + type: object + properties: + event: + type: object + properties: + data: + type: object + properties: + new: + type: object + properties: + payload: + $ref: "#/components/schemas/Payload" + bops_id: + type: string + destination_url: + type: string + required: + - body + UniformSubmissionSchema: + type: object + properties: + body: + type: object + properties: + event: + type: object + properties: + data: + type: object + properties: + new: + type: object + properties: + payload: + $ref: "#/components/schemas/Payload" + submission_reference: + type: string + response: + type: object + properties: + organisation: + type: string + required: + - body + EmailSubmissionSchema: + type: object + properties: + body: + type: object + properties: + event: + type: object + properties: + data: + type: object + properties: + new: + type: object + properties: + session_id: + type: string + team_slug: + type: string + request: + type: object + properties: + personalisation: + type: object + properties: + serviceName: + type: string + required: + - body + SendSlackNotificationSchema: + oneOf: + - $ref: "#/components/schemas/BopsSubmissionSchema" + - $ref: "#/components/schemas/UniformSubmissionSchema" + - $ref: "#/components/schemas/EmailSubmissionSchema" + responses: + SlackNotificationSuccessMessage: + content: + application/json: + schema: + type: object + properties: + data: + type: string + required: false + description: The generated Slack message + message: + type: string + required: true +paths: + /webhooks/hasura/sendSlackNotification: + post: + tags: ["webhooks"] + summary: Send Slack notification + description: Endpoint to trigger a Slack notification following a submission event + parameters: + - in: query + name: type + type: string + enum: ["bops-submission", "uniform-submission", "email-submission"] + required: true + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SendSlackNotificationSchema" + responses: + "200": + $ref: "#/components/responses/SlackNotificationSuccessMessage" + "500": + $ref: "#/components/responses/ErrorMessage" diff --git a/api.planx.uk/modules/webhooks/routes.ts b/api.planx.uk/modules/webhooks/routes.ts new file mode 100644 index 0000000000..c98421b7e3 --- /dev/null +++ b/api.planx.uk/modules/webhooks/routes.ts @@ -0,0 +1,40 @@ +import { Router } from "express"; +import { useHasuraAuth } from "../auth/middleware"; +import { createPaymentSendEvents } from "../../inviteToPay/createPaymentSendEvents"; +import { sanitiseApplicationData } from "./_old/sanitiseApplicationData"; +import { + createExpiryEvent, + createReminderEvent, +} from "./_old/lowcalSessionEvents"; +import { + createPaymentExpiryEvents, + createPaymentInvitationEvents, + createPaymentReminderEvents, +} from "./_old/paymentRequestEvents"; +import { validate } from "../../shared/middleware/validate"; +import { sendSlackNotificationController } from "./controller"; +import { sendSlackNotificationSchema } from "./sendNotification/schema"; + +const router = Router(); + +router.use("/hasura", useHasuraAuth); +router.post("/hasura/create-reminder-event", createReminderEvent); +router.post("/hasura/create-expiry-event", createExpiryEvent); +router.post( + "/hasura/create-payment-invitation-events", + createPaymentInvitationEvents, +); +router.post( + "/hasura/create-payment-reminder-events", + createPaymentReminderEvents, +); +router.post("/hasura/create-payment-expiry-events", createPaymentExpiryEvents); +router.post("/hasura/create-payment-send-events", createPaymentSendEvents); +router.post( + "/hasura/send-slack-notification", + validate(sendSlackNotificationSchema), + sendSlackNotificationController, +); +router.post("/hasura/sanitise-application-data", sanitiseApplicationData); + +export default router; diff --git a/api.planx.uk/modules/webhooks/sendNotification/index.test.ts b/api.planx.uk/modules/webhooks/sendNotification/index.test.ts new file mode 100644 index 0000000000..e73c115371 --- /dev/null +++ b/api.planx.uk/modules/webhooks/sendNotification/index.test.ts @@ -0,0 +1,345 @@ +import supertest from "supertest"; +import app from "../../../server"; +import SlackNotify from "slack-notify"; +import { BOPSBody, EmailBody, UniformBody } from "./types"; +import { $admin } from "../../../client"; +import { CoreDomainClient } from "@opensystemslab/planx-core"; + +const mockSessionWithFee = { + data: { + passport: { + data: { + "application.fee.payable": "123", + }, + }, + }, +}; + +const mockSessionWithDisabilityExemption = { + data: { + passport: { + data: { + "application.fee.exemption.disability": ["true"], + }, + }, + }, +}; + +const mockSessionWithResubmissionExemption = { + data: { + passport: { + data: { + "application.fee.exemption.resubmission": ["true"], + }, + }, + }, +}; + +jest.mock("../../../client"); +const mockAdmin = jest.mocked($admin); + +const mockSend = jest.fn(); +jest.mock("slack-notify", () => + jest.fn().mockImplementation(() => { + return { send: mockSend }; + }), +); + +const { post } = supertest(app); + +describe("Send Slack notifications endpoint", () => { + const ENDPOINT = "/webhooks/hasura/send-slack-notification"; + const ORIGINAL_ENV = process.env; + + beforeEach(() => { + process.env = { ...ORIGINAL_ENV }; + mockAdmin.session.find = jest.fn().mockResolvedValue(mockSessionWithFee); + mockSend.mockResolvedValue("Success!"); + }); + + afterEach(jest.clearAllMocks); + + afterAll(() => (process.env = ORIGINAL_ENV)); + + describe("authentication and validation", () => { + it("fails without correct authentication", async () => { + await post(ENDPOINT) + .expect(401) + .then((response) => { + expect(response.body).toEqual({ + error: "Unauthorised", + }); + }); + }); + + it("returns a 400 if 'type' is missing", async () => { + const body = { event: {} }; + await post(ENDPOINT) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(400) + .then((response) => { + expect(response.body).toHaveProperty("issues"); + expect(response.body).toHaveProperty("name", "ZodError"); + }); + }); + + it("returns a 400 if 'type' is incorrect", async () => { + const body = { event: {} }; + await post(ENDPOINT + "?type=test-submission") + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(400) + .then((response) => { + expect(response.body).toHaveProperty("issues"); + expect(response.body).toHaveProperty("name", "ZodError"); + }); + }); + + it("returns a 400 if 'event' is missing", async () => { + await post(ENDPOINT + "?type=bops-submission") + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .expect(400) + .then((response) => { + expect(response.body).toHaveProperty("issues"); + expect(response.body).toHaveProperty("name", "ZodError"); + }); + }); + }); + + describe("BOPS notifications", () => { + const body: BOPSBody = { + event: { + data: { + new: { + payload: { sessionId: "xyz123" }, + bops_id: "abc123", + destination_url: "https://www.bops-production.com", + }, + }, + }, + }; + + it("skips the staging environment", async () => { + process.env.APP_ENVIRONMENT = "staging"; + await post(ENDPOINT) + .query({ type: "bops-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(200) + .then((response) => { + expect(response.body.message).toMatch(/skipping Slack notification/); + }); + }); + + it("posts to Slack on success", async () => { + process.env.APP_ENVIRONMENT = "production"; + + await post(ENDPOINT) + .query({ type: "bops-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(200) + .then((response) => { + expect(SlackNotify).toHaveBeenCalledWith( + process.env.SLACK_WEBHOOK_URL, + ); + expect(mockSend).toHaveBeenCalledTimes(1); + expect(response.body.message).toBe("Posted to Slack"); + expect(response.body.data).toMatch(/abc123/); + expect(response.body.data).toMatch(/www.bops-production.com/); + }); + }); + + it("returns error when Slack fails", async () => { + process.env.APP_ENVIRONMENT = "production"; + mockSend.mockRejectedValue("Fail!"); + + await post(ENDPOINT) + .query({ type: "bops-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(500) + .then((response) => { + expect(mockSend).toHaveBeenCalledTimes(1); + expect(response.body.error).toMatch(/Failed to send/); + }); + }); + }); + + describe("Uniform notifications", () => { + const body: UniformBody = { + event: { + data: { + new: { + payload: { sessionId: "xyz123" }, + submission_reference: "abc123", + response: { + organisation: "test-council", + }, + }, + }, + }, + }; + + it("skips the staging environment", async () => { + process.env.APP_ENVIRONMENT = "staging"; + await post(ENDPOINT) + .query({ type: "uniform-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(200) + .then((response) => { + expect(response.body.message).toMatch(/skipping Slack notification/); + }); + }); + + it("posts to Slack on success", async () => { + process.env.APP_ENVIRONMENT = "production"; + + await post(ENDPOINT) + .query({ type: "uniform-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(200) + .then((response) => { + expect(SlackNotify).toHaveBeenCalledWith( + process.env.SLACK_WEBHOOK_URL, + ); + expect(mockSend).toHaveBeenCalledTimes(1); + expect(mockAdmin.session.find).toHaveBeenCalledTimes(1); + + expect(response.body.message).toBe("Posted to Slack"); + expect(response.body.data).toMatch(/abc123/); + expect(response.body.data).toMatch(/test-council/); + }); + }); + + it("adds a status to the Slack message for a disability exemption", async () => { + process.env.APP_ENVIRONMENT = "production"; + mockAdmin.session.find = jest + .fn() + .mockResolvedValue(mockSessionWithDisabilityExemption); + + await post(ENDPOINT) + .query({ type: "uniform-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(200) + .then((response) => { + expect(response.body.data).toMatch(/[Exempt]/); + }); + }); + + it("adds a status to the Slack message for a resubmission exemption", async () => { + process.env.APP_ENVIRONMENT = "production"; + mockAdmin.session.find = jest + .fn() + .mockResolvedValue(mockSessionWithResubmissionExemption); + + await post(ENDPOINT) + .query({ type: "uniform-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(200) + .then((response) => { + expect(response.body.data).toMatch(/[Resubmission]/); + }); + }); + + it("handles missing sessions", async () => { + process.env.APP_ENVIRONMENT = "production"; + mockAdmin.session.find = jest.fn().mockResolvedValueOnce(null); + + await post(ENDPOINT) + .query({ type: "uniform-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(500) + .then((response) => { + expect(mockAdmin.session.find).toHaveBeenCalledTimes(1); + expect(response.body.error).toMatch(/Failed to send/); + }); + }); + + it("returns error when Slack fails", async () => { + process.env.APP_ENVIRONMENT = "production"; + mockSend.mockRejectedValue("Fail!"); + + await post(ENDPOINT) + .query({ type: "uniform-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(500) + .then((response) => { + expect(mockSend).toHaveBeenCalledTimes(1); + expect(response.body.error).toMatch(/Failed to send/); + }); + }); + }); + + describe("Email notifications", () => { + const body: EmailBody = { + event: { + data: { + new: { + session_id: "abc123", + team_slug: "testTeam", + request: { + personalisation: { + serviceName: "testServiceName", + }, + }, + }, + }, + }, + }; + + it("skips the staging environment", async () => { + process.env.APP_ENVIRONMENT = "staging"; + await post(ENDPOINT) + .query({ type: "email-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(200) + .then((response) => { + expect(response.body.message).toMatch(/skipping Slack notification/); + }); + }); + + it("posts to Slack on success", async () => { + process.env.APP_ENVIRONMENT = "production"; + + await post(ENDPOINT) + .query({ type: "email-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(200) + .then((response) => { + expect(SlackNotify).toHaveBeenCalledWith( + process.env.SLACK_WEBHOOK_URL, + ); + expect(mockSend).toHaveBeenCalledTimes(1); + expect(response.body.message).toBe("Posted to Slack"); + expect(response.body.data).toMatch(/abc123/); + expect(response.body.data).toMatch(/testTeam/); + expect(response.body.data).toMatch(/testServiceName/); + }); + }); + + it("returns error when Slack fails", async () => { + process.env.APP_ENVIRONMENT = "production"; + mockSend.mockRejectedValue("Fail!"); + + await post(ENDPOINT) + .query({ type: "email-submission" }) + .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) + .send(body) + .expect(500) + .then((response) => { + expect(mockSend).toHaveBeenCalledTimes(1); + expect(response.body.error).toMatch(/Failed to send/); + }); + }); + }); +}); diff --git a/api.planx.uk/modules/webhooks/sendNotification/schema.ts b/api.planx.uk/modules/webhooks/sendNotification/schema.ts new file mode 100644 index 0000000000..e8cb1c4f74 --- /dev/null +++ b/api.planx.uk/modules/webhooks/sendNotification/schema.ts @@ -0,0 +1,68 @@ +import { z } from "zod"; + +const payload = z.object({ + sessionId: z.string(), +}); + +export const bopsSubmissionSchema = z.object({ + body: z.object({ + event: z.object({ + data: z.object({ + new: z.object({ + payload, + bops_id: z.string(), + destination_url: z.string(), + }), + }), + }), + }), + query: z.object({ + type: z.literal("bops-submission"), + }), +}); + +export const uniformSubmissionSchema = z.object({ + body: z.object({ + event: z.object({ + data: z.object({ + new: z.object({ + payload, + submission_reference: z.string(), + response: z.object({ + organisation: z.string(), + }), + }), + }), + }), + }), + query: z.object({ + type: z.literal("uniform-submission"), + }), +}); + +export const emailSubmissionSchema = z.object({ + body: z.object({ + event: z.object({ + data: z.object({ + new: z.object({ + session_id: z.string(), + team_slug: z.string(), + request: z.object({ + personalisation: z.object({ + serviceName: z.string(), + }), + }), + }), + }), + }), + }), + query: z.object({ + type: z.literal("email-submission"), + }), +}); + +export const sendSlackNotificationSchema = z.union([ + bopsSubmissionSchema, + uniformSubmissionSchema, + emailSubmissionSchema, +]); diff --git a/api.planx.uk/modules/webhooks/sendNotification/service.ts b/api.planx.uk/modules/webhooks/sendNotification/service.ts new file mode 100644 index 0000000000..be8678a0e9 --- /dev/null +++ b/api.planx.uk/modules/webhooks/sendNotification/service.ts @@ -0,0 +1,64 @@ +import { Passport } from "@opensystemslab/planx-core"; +import SlackNotify from "slack-notify"; +import { + BOPSEventData, + EmailEventData, + EventData, + EventType, + UniformEventData, +} from "./types"; +import { $admin } from "../../../client"; + +export const sendSlackNotification = async ( + data: EventData, + type: EventType, +) => { + const slack = SlackNotify(process.env.SLACK_WEBHOOK_URL!); + let message = getMessageForEventType(data, type); + + const sessionId = getSessionIdFromEvent(data, type); + const { disability, resubmission } = + await getExemptionStatusesForSession(sessionId); + if (disability) message += " [Exempt]"; + if (resubmission) message += " [Resubmission]"; + + await slack.send(":incoming_envelope: " + message); + return message; +}; + +const getMessageForEventType = (data: EventData, type: EventType) => { + if (type === "bops-submission") { + const { bops_id, destination_url } = data as BOPSEventData; + return `New BOPS submission *${bops_id}* [${destination_url}]`; + } + + if (type === "uniform-submission") { + const { submission_reference, response } = data as UniformEventData; + return `New Uniform submission *${submission_reference}* [${response.organisation}]`; + } + + if (type === "email-submission") { + const { request, session_id, team_slug } = data as EmailEventData; + return `New email submission "${request.personalisation.serviceName}" *${session_id}* [${team_slug}]`; + } +}; + +const getSessionIdFromEvent = (data: EventData, type: EventType) => + ({ + "bops-submission": (data as BOPSEventData).payload?.sessionId, + "uniform-submission": (data as UniformEventData).payload?.sessionId, + "email-submission": (data as EmailEventData).session_id, + })[type]; + +const getExemptionStatusesForSession = async (sessionId: string) => { + const session = await $admin.session.find(sessionId); + if (!session) throw Error(`Unable to find session with ID ${sessionId}`); + + const passport = new Passport(session.data.passport); + const disability = passport.boolean(["application.fee.exemption.disability"]); + const resubmission = passport.boolean([ + "application.fee.exemption.resubmission", + ]); + + return { disability, resubmission }; +}; diff --git a/api.planx.uk/modules/webhooks/sendNotification/types.ts b/api.planx.uk/modules/webhooks/sendNotification/types.ts new file mode 100644 index 0000000000..cd1e293bf9 --- /dev/null +++ b/api.planx.uk/modules/webhooks/sendNotification/types.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; +import { ValidatedRequestHandler } from "../../../shared/middleware/validate"; +import { + bopsSubmissionSchema, + emailSubmissionSchema, + sendSlackNotificationSchema, + uniformSubmissionSchema, +} from "./schema"; + +interface SendSlackNotificationResponse { + message: string; + data?: string; +} + +export type EventType = z.infer< + typeof sendSlackNotificationSchema +>["query"]["type"]; + +export type BOPSBody = z.infer["body"]; +export type BOPSEventData = BOPSBody["event"]["data"]["new"]; + +export type UniformBody = z.infer["body"]; +export type UniformEventData = UniformBody["event"]["data"]["new"]; + +export type EmailBody = z.infer["body"]; +export type EmailEventData = EmailBody["event"]["data"]["new"]; + +export type EventData = BOPSEventData | UniformEventData | EmailEventData; + +export type SendSlackNotification = ValidatedRequestHandler< + typeof sendSlackNotificationSchema, + SendSlackNotificationResponse +>; diff --git a/api.planx.uk/server.ts b/api.planx.uk/server.ts index 6db24eb716..bb8e293c08 100644 --- a/api.planx.uk/server.ts +++ b/api.planx.uk/server.ts @@ -40,10 +40,6 @@ import { } from "./modules/auth/middleware"; import airbrake from "./airbrake"; -import { - createReminderEvent, - createExpiryEvent, -} from "./webhooks/lowcalSessionEvents"; import { adminGraphQLClient as adminClient } from "./hasura"; import { sendEmailLimiter, apiLimiter } from "./rateLimit"; import { @@ -56,31 +52,24 @@ import { sendToBOPS } from "./send/bops"; import { createSendEvents } from "./send/createSendEvents"; import { downloadApplicationFiles, sendToEmail } from "./send/email"; import { sendToUniform } from "./send/uniform"; -import { sendSlackNotification } from "./webhooks/sendNotifications"; import { copyFlow } from "./editor/copyFlow"; import { moveFlow } from "./editor/moveFlow"; import { useOrdnanceSurveyProxy } from "./proxy/ordnanceSurvey"; import { downloadFeedbackCSV } from "./admin/feedback/downloadFeedbackCSV"; -import { sanitiseApplicationData } from "./webhooks/sanitiseApplicationData"; import { getOneAppXML } from "./admin/session/oneAppXML"; import { gql } from "graphql-request"; -import { - createPaymentExpiryEvents, - createPaymentInvitationEvents, - createPaymentReminderEvents, -} from "./webhooks/paymentRequestEvents"; import { classifiedRoadsSearch } from "./gis/classifiedRoads"; import { getBOPSPayload } from "./admin/session/bops"; import { getCSVData, getRedactedCSVData } from "./admin/session/csv"; import { getHTMLExport, getRedactedHTMLExport } from "./admin/session/html"; import { generateZip } from "./admin/session/zip"; -import { createPaymentSendEvents } from "./inviteToPay/createPaymentSendEvents"; import { getSessionSummary } from "./admin/session/summary"; import { googleStrategy } from "./modules/auth/strategy/google"; import authRoutes from "./modules/auth/routes"; import teamRoutes from "./modules/team/routes"; import miscRoutes from "./modules/misc/routes"; import userRoutes from "./modules/user/routes"; +import webhookRoutes from "./modules/webhooks/routes"; import { useSwaggerDocs } from "./docs"; import { Role } from "@opensystemslab/planx-core/types"; @@ -197,6 +186,7 @@ app.use(authRoutes); app.use(miscRoutes); app.use("/user", userRoutes); app.use("/team", teamRoutes); +app.use("/webhooks", webhookRoutes); app.use("/gis", router); @@ -456,28 +446,6 @@ app.post("/validate-session", validateSession); app.post("/invite-to-pay/:sessionId", inviteToPay); -app.use("/webhooks/hasura", useHasuraAuth); -app.post("/webhooks/hasura/create-reminder-event", createReminderEvent); -app.post("/webhooks/hasura/create-expiry-event", createExpiryEvent); -app.post( - "/webhooks/hasura/create-payment-invitation-events", - createPaymentInvitationEvents, -); -app.post( - "/webhooks/hasura/create-payment-reminder-events", - createPaymentReminderEvents, -); -app.post( - "/webhooks/hasura/create-payment-expiry-events", - createPaymentExpiryEvents, -); -app.post( - "/webhooks/hasura/create-payment-send-events", - createPaymentSendEvents, -); -app.post("/webhooks/hasura/send-slack-notification", sendSlackNotification); -app.post("/webhooks/hasura/sanitise-application-data", sanitiseApplicationData); - app.use("/proxy/ordnance-survey", useOrdnanceSurveyProxy); app.get("/error", async (res, req, next) => { diff --git a/api.planx.uk/shared/middleware/validate.ts b/api.planx.uk/shared/middleware/validate.ts index a5ed42650f..ce647f2f16 100644 --- a/api.planx.uk/shared/middleware/validate.ts +++ b/api.planx.uk/shared/middleware/validate.ts @@ -1,12 +1,12 @@ import { Request, RequestHandler, Response, NextFunction } from "express"; -import { AnyZodObject, ZodSchema, z } from "zod"; +import { AnyZodObject, ZodSchema, ZodTypeAny, ZodUnion, z } from "zod"; /** * Middleware to validate incoming requests to the API * Takes a ZodSchema and returns a validated, and typed, Request object */ export const validate = - (schema: AnyZodObject) => + (schema: AnyZodObject | ZodUnion) => async ( req: Request>, res: Response, diff --git a/api.planx.uk/webhooks/sendNotification.test.ts b/api.planx.uk/webhooks/sendNotification.test.ts deleted file mode 100644 index 5d8eda61f6..0000000000 --- a/api.planx.uk/webhooks/sendNotification.test.ts +++ /dev/null @@ -1,186 +0,0 @@ -import supertest from "supertest"; -import app from "../server"; -import SlackNotify from "slack-notify"; - -const ENDPOINT = "/webhooks/hasura/send-slack-notification"; - -const mockSend = jest.fn(); -jest.mock("slack-notify", () => - jest.fn().mockImplementation(() => { - return { send: mockSend }; - }), -); - -const { post } = supertest(app); - -describe("Send Slack notifications endpoint", () => { - const ORIGINAL_ENV = process.env; - - beforeEach(() => { - jest.resetModules(); - process.env = { ...ORIGINAL_ENV }; - }); - - afterAll(() => { - process.env = ORIGINAL_ENV; - }); - - describe("authentication and validation", () => { - it("fails without correct authentication", async () => { - await post(ENDPOINT) - .expect(401) - .then((response) => { - expect(response.body).toEqual({ - error: "Unauthorised", - }); - }); - }); - - it("returns a 404 if 'type' is missing", async () => { - const body = { event: {} }; - await post(ENDPOINT) - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .send(body) - .expect(404) - .then((response) => { - expect(response.body).toEqual({ - message: "Missing info required to send a Slack notification", - }); - }); - }); - - it("returns a 404 if 'type' is incorrect", async () => { - const body = { event: {} }; - await post(ENDPOINT + "?type=test-submission") - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .send(body) - .expect(404) - .then((response) => { - expect(response.body).toEqual({ - message: "Missing info required to send a Slack notification", - }); - }); - }); - - it("returns a 404 if 'event' is missing", async () => { - await post(ENDPOINT + "?type=bops-submission") - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .expect(404) - .then((response) => { - expect(response.body).toEqual({ - message: "Missing info required to send a Slack notification", - }); - }); - }); - }); - - const destinations = [ - { - name: "BOPS", - type: "bops-submission", - stagingBody: { - event: { - data: { - new: { - destination_url: "https://www.bops-staging.com", - }, - }, - }, - }, - prodBody: { - event: { - data: { - new: { - destination_url: "https://www.bops-production.com", - }, - }, - }, - }, - }, - { - name: "Uniform", - type: "uniform-submission", - stagingBody: { - event: { - data: { - new: { - response: { - _links: { - self: { - href: "https://www.uniform-staging.com", - }, - }, - }, - }, - }, - }, - }, - prodBody: { - event: { - data: { - new: { - response: { - _links: { - self: { - href: "https://www.uniform-production.com", - }, - }, - }, - }, - }, - }, - }, - }, - ]; - - for (const destination of destinations) { - describe(`${destination.name} notifications`, () => { - afterEach(() => jest.clearAllMocks()); - - it("skips the staging environment", async () => { - process.env.APP_ENVIRONMENT = "staging"; - await post(ENDPOINT + `?type=${destination.type}`) - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .send(destination.stagingBody) - .expect(200) - .then((response) => { - expect(response.body.message).toMatch( - /skipping Slack notification/, - ); - }); - }); - - it("posts to Slack on success", async () => { - process.env.APP_ENVIRONMENT = "production"; - mockSend.mockResolvedValue("Success!"); - - await post(ENDPOINT + `?type=${destination.type}`) - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .send(destination.prodBody) - .expect(200) - .then((response) => { - expect(SlackNotify).toHaveBeenCalledWith( - process.env.SLACK_WEBHOOK_URL, - ); - expect(mockSend).toHaveBeenCalledTimes(1); - expect(response.body.message).toBe("Posted to Slack"); - }); - }); - - it("returns error when Slack fails", async () => { - process.env.APP_ENVIRONMENT = "production"; - mockSend.mockRejectedValue("Fail!"); - - await post(ENDPOINT + `?type=${destination.type}`) - .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) - .send(destination.prodBody) - .expect(500) - .then((response) => { - expect(mockSend).toHaveBeenCalledTimes(1); - expect(response.body.error).toMatch(/Failed to send/); - expect(response.body.error).toMatch(/Fail!/); - }); - }); - }); - } -}); diff --git a/api.planx.uk/webhooks/sendNotifications.ts b/api.planx.uk/webhooks/sendNotifications.ts deleted file mode 100644 index c05061f3b1..0000000000 --- a/api.planx.uk/webhooks/sendNotifications.ts +++ /dev/null @@ -1,78 +0,0 @@ -import SlackNotify from "slack-notify"; - -import { Request, Response, NextFunction } from "express"; - -const sendSlackNotification = async ( - req: Request, - res: Response, - next: NextFunction, -): Promise => { - const isProduction = process.env.APP_ENVIRONMENT === "production"; - const supportedTypes = [ - "bops-submission", - "uniform-submission", - "email-submission", - ]; - if ( - !req.body?.event || - !req.query?.type || - !supportedTypes.includes(req.query.type as string) - ) { - return res.status(404).send({ - message: "Missing info required to send a Slack notification", - }); - } - - try { - // hook into the #planx-notifications channel - const slack = SlackNotify(process.env.SLACK_WEBHOOK_URL!); - - const data = req.body?.event?.data?.new; - - if (req.query.type === "bops-submission") { - if (isProduction) { - const bopsMessage = `:incoming_envelope: New BOPS submission *${data?.bops_id}* [${data?.destination_url}]`; - await slack.send(bopsMessage); - return res - .status(200) - .send({ message: "Posted to Slack", data: bopsMessage }); - } - return res.status(200).send({ - message: `Staging application submitted, skipping Slack notification`, - }); - } - - if (req.query.type === "uniform-submission") { - if (isProduction) { - const uniformMessage = `:incoming_envelope: New Uniform submission *${data?.submission_reference}* [${data?.response?.organisation}]`; - await slack.send(uniformMessage); - return res - .status(200) - .send({ message: "Posted to Slack", data: uniformMessage }); - } - return res.status(200).send({ - message: `Staging application submitted, skipping Slack notification`, - }); - } - - if (req.query.type === "email-submission") { - if (isProduction) { - const emailMessage = `:incoming_envelope: New email submission "${data?.request?.personalisation?.serviceName}" *${data?.session_id}* [${data?.team_slug}]`; - await slack.send(emailMessage); - return res - .status(200) - .send({ message: "Posted to Slack", data: emailMessage }); - } - return res.status(200).send({ - message: `Staging application submitted, skipping Slack notification`, - }); - } - } catch (error) { - return next({ - error, - message: `Failed to send ${req.query.type} Slack notification. Error: ${error}`, - }); - } -}; - -export { sendSlackNotification }; From 7f5e2e8f0d8ff3230650112be24ab84d83309989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Wed, 4 Oct 2023 09:00:18 +0100 Subject: [PATCH 2/8] chore: Bump tinycolor to v4 (#2275) * chore: Bump tinycolor to v4 * chore: Linting * test: Use full hex to match test cases --- editor.planx.uk/package.json | 2 +- editor.planx.uk/pnpm-lock.yaml | 10 +++--- .../src/components/Header.test.tsx | 6 ++-- editor.planx.uk/src/lib/dataMergedHotfix.ts | 6 ++-- .../lib/__tests__/externalPortals.test.ts | 2 +- .../mocks/multipleExternalPortals.json | 33 +++++-------------- .../__tests__/mocks/singleExternalPortal.json | 24 ++++---------- .../src/pages/FlowEditor/lib/store/preview.ts | 3 +- .../src/pages/FlowEditor/lib/store/team.ts | 4 +-- .../src/pages/FlowEditor/lib/store/user.ts | 4 +-- editor.planx.uk/src/routes/authenticated.tsx | 2 +- editor.planx.uk/src/routes/team.tsx | 2 +- editor.planx.uk/src/routes/views/team.tsx | 13 ++++---- 13 files changed, 42 insertions(+), 69 deletions(-) diff --git a/editor.planx.uk/package.json b/editor.planx.uk/package.json index d5195c1e33..1fc3c696d6 100644 --- a/editor.planx.uk/package.json +++ b/editor.planx.uk/package.json @@ -5,7 +5,7 @@ "dependencies": { "@airbrake/browser": "^2.1.8", "@apollo/client": "^3.7.16", - "@ctrl/tinycolor": "^3.6.0", + "@ctrl/tinycolor": "^4.0.2", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@feedback-fish/react": "^1.2.2", diff --git a/editor.planx.uk/pnpm-lock.yaml b/editor.planx.uk/pnpm-lock.yaml index ab8619d7d4..cb8d6007b1 100644 --- a/editor.planx.uk/pnpm-lock.yaml +++ b/editor.planx.uk/pnpm-lock.yaml @@ -19,8 +19,8 @@ dependencies: specifier: ^3.7.16 version: 3.7.16(graphql@16.8.1)(react-dom@18.2.0)(react@18.2.0) '@ctrl/tinycolor': - specifier: ^3.6.0 - version: 3.6.0 + specifier: ^4.0.2 + version: 4.0.2 '@emotion/react': specifier: ^11.11.1 version: 11.11.1(@types/react@18.2.20)(react@18.2.0) @@ -3804,9 +3804,9 @@ packages: dependencies: postcss-selector-parser: 6.0.13 - /@ctrl/tinycolor@3.6.0: - resolution: {integrity: sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ==} - engines: {node: '>=10'} + /@ctrl/tinycolor@4.0.2: + resolution: {integrity: sha512-fKQinXE9pJ83J1n+C3rDl2xNLJwfoYNvXLRy5cYZA9hBJJw2q+sbb/AOSNKmLxnTWyNTmy4994dueSwP4opi5g==} + engines: {node: '>=14'} dev: false /@discoveryjs/json-ext@0.5.7: diff --git a/editor.planx.uk/src/components/Header.test.tsx b/editor.planx.uk/src/components/Header.test.tsx index 483361cb05..ac64dcebc0 100644 --- a/editor.planx.uk/src/components/Header.test.tsx +++ b/editor.planx.uk/src/components/Header.test.tsx @@ -27,9 +27,9 @@ const mockTeam2: Team = { slug: "closedsystemslab", }; -jest.spyOn(ReactNavi, "useNavigation").mockReturnValue(({ - navigate: jest.fn() -}) as any); +jest.spyOn(ReactNavi, "useNavigation").mockReturnValue({ + navigate: jest.fn(), +} as any); describe("Header Component - Editor Route", () => { beforeAll(() => { diff --git a/editor.planx.uk/src/lib/dataMergedHotfix.ts b/editor.planx.uk/src/lib/dataMergedHotfix.ts index 2e1d40c309..48228f173c 100644 --- a/editor.planx.uk/src/lib/dataMergedHotfix.ts +++ b/editor.planx.uk/src/lib/dataMergedHotfix.ts @@ -25,11 +25,13 @@ const getFlowData = async (id: string) => { // in order to load frontend /preview routes for flows that are not published export const dataMerged = async (id: string, ob: Record = {}) => { // get the primary flow data - const { slug, data }: { slug: string; data: Record } = await getFlowData(id); + const { slug, data }: { slug: string; data: Record } = + await getFlowData(id); // recursively get and flatten internal portals & external portals for (const [nodeId, node] of Object.entries(data)) { - const isExternalPortalRoot = nodeId === "_root" && Object.keys(ob).length > 0; + const isExternalPortalRoot = + nodeId === "_root" && Object.keys(ob).length > 0; const isExternalPortal = node.type === TYPES.ExternalPortal; const isMerged = ob[node.data?.flowId]; diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/externalPortals.test.ts b/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/externalPortals.test.ts index 564469d48e..6646cafadd 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/externalPortals.test.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/externalPortals.test.ts @@ -60,4 +60,4 @@ describe("A flow with repeated external portals can be navigated as expected", ( record("withinExternalPortal", { answers: [] }); expect(upcomingCardIds()[0]).toEqual("finalNode"); }); -}); \ No newline at end of file +}); diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/mocks/multipleExternalPortals.json b/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/mocks/multipleExternalPortals.json index b2693fa5e4..4ea6daea92 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/mocks/multipleExternalPortals.json +++ b/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/mocks/multipleExternalPortals.json @@ -1,15 +1,10 @@ { "_root": { - "edges": [ - "firstNode", - "finalNode" - ] + "edges": ["firstNode", "finalNode"] }, "externalPortal1": { "type": 300, - "edges": [ - "externalFlowId" - ] + "edges": ["externalFlowId"] }, "option3": { "data": { @@ -19,29 +14,21 @@ }, "externalPortal2": { "type": 300, - "edges": [ - "externalFlowId" - ] + "edges": ["externalFlowId"] }, "firstNode": { "data": { "text": "This is a question" }, "type": 100, - "edges": [ - "option1", - "option2", - "option3" - ] + "edges": ["option1", "option2", "option3"] }, "option2": { "data": { "text": "Option 2" }, "type": 200, - "edges": [ - "externalPortal2" - ] + "edges": ["externalPortal2"] }, "finalNode": { "data": { @@ -65,17 +52,13 @@ "text": "Option 1" }, "type": 200, - "edges": [ - "externalPortal1" - ] + "edges": ["externalPortal1"] }, "externalFlowId": { "data": { "text": "daf-external-portal-test" }, "type": 300, - "edges": [ - "withinExternalPortal" - ] + "edges": ["withinExternalPortal"] } -} \ No newline at end of file +} diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/mocks/singleExternalPortal.json b/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/mocks/singleExternalPortal.json index dea585a6dc..7b408035a9 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/mocks/singleExternalPortal.json +++ b/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/mocks/singleExternalPortal.json @@ -1,9 +1,6 @@ { "_root": { - "edges": [ - "firstNode", - "finalNode" - ] + "edges": ["firstNode", "finalNode"] }, "option2": { "data": { @@ -21,28 +18,21 @@ }, "externalPortal": { "type": 300, - "edges": [ - "externalFlowId" - ] + "edges": ["externalFlowId"] }, "firstNode": { "data": { "text": "This is a question with many options" }, "type": 100, - "edges": [ - "option1", - "option2" - ] + "edges": ["option1", "option2"] }, "option1": { "data": { "text": "Option 1" }, "type": 200, - "edges": [ - "externalPortal" - ] + "edges": ["externalPortal"] }, "withinExternalPortal": { "data": { @@ -58,8 +48,6 @@ "text": "test-external-portal" }, "type": 300, - "edges": [ - "withinExternalPortal" - ] + "edges": ["withinExternalPortal"] } -} \ No newline at end of file +} diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/store/preview.ts b/editor.planx.uk/src/pages/FlowEditor/lib/store/preview.ts index e5be16d5fc..629982166e 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/store/preview.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/store/preview.ts @@ -1,4 +1,3 @@ -import tinycolor from "@ctrl/tinycolor"; import type { Flag, FlagSet, @@ -742,7 +741,7 @@ export const getResultData = ( text: "No result", category: category as FlagSet, bgColor: "#EEEEEE", - color: tinycolor("black").toHexString(), + color: "#000000", description: "", }; diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/store/team.ts b/editor.planx.uk/src/pages/FlowEditor/lib/store/team.ts index d120327e6d..8eeaed720d 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/store/team.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/store/team.ts @@ -7,7 +7,7 @@ import { Team } from "types"; import type { StateCreator } from "zustand"; export interface TeamStore { - teamId: number, + teamId: number; teamTheme?: TeamTheme; teamName: string; teamSettings?: TeamSettings; @@ -88,7 +88,7 @@ export const teamStore: StateCreator = ( get().setTeam(team); }, - clearTeamStore: () => + clearTeamStore: () => set({ teamId: 0, teamTheme: undefined, diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/store/user.ts b/editor.planx.uk/src/pages/FlowEditor/lib/store/user.ts index a102b1f6f5..0f8cec17fd 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/store/user.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/store/user.ts @@ -35,8 +35,8 @@ export const userStore: StateCreator = ( }, async initUserStore(jwt: string) { - const { getUser, setUser } = get(); - + const { getUser, setUser } = get(); + if (getUser()) return; const id = (jwtDecode(jwt) as any)["sub"]; diff --git a/editor.planx.uk/src/routes/authenticated.tsx b/editor.planx.uk/src/routes/authenticated.tsx index eb6a45ac85..d8a7966536 100644 --- a/editor.planx.uk/src/routes/authenticated.tsx +++ b/editor.planx.uk/src/routes/authenticated.tsx @@ -27,7 +27,7 @@ const editorRoutes = compose( }); useStore.getState().clearTeamStore(); - + return { title: makeTitle("Teams"), view: , diff --git a/editor.planx.uk/src/routes/team.tsx b/editor.planx.uk/src/routes/team.tsx index 3c70c7a521..12e3f355b2 100644 --- a/editor.planx.uk/src/routes/team.tsx +++ b/editor.planx.uk/src/routes/team.tsx @@ -19,7 +19,7 @@ const routes = compose( mount({ "/": route(() => ({ title: makeTitle(useStore.getState().teamName), - view: , + view: , })), "/:flow": lazy(async (req) => { diff --git a/editor.planx.uk/src/routes/views/team.tsx b/editor.planx.uk/src/routes/views/team.tsx index bbf1898d6e..3a2a02d296 100644 --- a/editor.planx.uk/src/routes/views/team.tsx +++ b/editor.planx.uk/src/routes/views/team.tsx @@ -1,7 +1,7 @@ -import { NaviRequest, NotFoundError } from "navi" +import { NaviRequest, NotFoundError } from "navi"; import { useStore } from "pages/FlowEditor/lib/store"; import React from "react"; -import { View } from "react-navi" +import { View } from "react-navi"; import { getTeamFromDomain } from "routes/utils"; /** @@ -10,7 +10,8 @@ import { getTeamFromDomain } from "routes/utils"; */ export const teamView = async (req: NaviRequest) => { const { initTeamStore, teamSlug: currentSlug } = useStore.getState(); - const routeSlug = req.params.team || await getTeamFromDomain(window.location.hostname) + const routeSlug = + req.params.team || (await getTeamFromDomain(window.location.hostname)); if (currentSlug !== routeSlug) { try { @@ -19,6 +20,6 @@ export const teamView = async (req: NaviRequest) => { throw new NotFoundError(`Team not found: ${error}`); } } - - return -} \ No newline at end of file + + return ; +}; From 8984f399acf3459ca8a0be37b59f9030b962cdce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Wed, 4 Oct 2023 09:00:38 +0100 Subject: [PATCH 3/8] chore: Remove Hasura seeds (#2276) --- hasura.planx.uk/seeds/1595528762802_teams_and_users.sql | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 hasura.planx.uk/seeds/1595528762802_teams_and_users.sql diff --git a/hasura.planx.uk/seeds/1595528762802_teams_and_users.sql b/hasura.planx.uk/seeds/1595528762802_teams_and_users.sql deleted file mode 100644 index 1728178b6d..0000000000 --- a/hasura.planx.uk/seeds/1595528762802_teams_and_users.sql +++ /dev/null @@ -1,6 +0,0 @@ -INSERT INTO public.users (id, first_name, last_name, email, is_platform_admin) VALUES (2, 'Alastair', 'Parvin', 'alastair@opensystemslab.io', true) ON CONFLICT (id) DO NOTHING; -INSERT INTO public.users (id, first_name, last_name, email, is_platform_admin) VALUES (20, 'Jessica', 'McInchak', 'jessica@opensystemslab.io', true) ON CONFLICT (id) DO NOTHING; -INSERT INTO public.users (id, first_name, last_name, email, is_platform_admin) VALUES (33, 'Dafydd', 'Pearson', 'dafydd@opensystemslab.io', true) ON CONFLICT (id) DO NOTHING; -INSERT INTO public.users (id, first_name, last_name, email, is_platform_admin) VALUES (65, 'Ian', 'Jones', 'ian@opensystemslab.io', true) ON CONFLICT (id) DO NOTHING; -SELECT setval('users_id_seq', max(id)) FROM users; -SELECT setval('teams_id_seq', max(id)) FROM teams; From fadbae7a16fe54c34a36dd00cf1747e703f28ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Wed, 4 Oct 2023 09:01:02 +0100 Subject: [PATCH 4/8] refactor(api): Update remaining webhook routes to modular structure (#2252) --- api.planx.uk/hasura/metadata/index.ts | 7 +- .../createPaymentSendEvents.test.ts | 17 +- .../inviteToPay/createPaymentSendEvents.ts | 11 +- .../webhooks/_old/lowcalSessionEvents.ts | 76 -------- .../webhooks/_old/paymentRequestEvents.ts | 136 -------------- api.planx.uk/modules/webhooks/controller.ts | 108 ++++++++++- api.planx.uk/modules/webhooks/docs.yaml | 177 +++++++++++++++++- api.planx.uk/modules/webhooks/routes.ts | 57 ++++-- .../lowcalSessionEvents/index.test.ts} | 55 ++++-- .../service/lowcalSessionEvents/index.ts | 44 +++++ .../service/lowcalSessionEvents/schema.ts | 21 +++ .../paymentRequestEvents/index.test.ts} | 83 +++++--- .../service/paymentRequestEvents/index.ts | 86 +++++++++ .../service/paymentRequestEvents/schema.ts | 21 +++ .../sanitiseApplicationData/index.test.ts | 6 +- .../sanitiseApplicationData/index.ts | 32 +--- .../sanitiseApplicationData/mocks/queries.ts | 0 .../operations.test.ts | 0 .../sanitiseApplicationData/operations.ts | 0 .../sanitiseApplicationData/types.ts} | 8 + .../sendNotification/index.test.ts | 8 +- .../sendNotification/index.ts} | 2 +- .../{ => service}/sendNotification/schema.ts | 0 .../{ => service}/sendNotification/types.ts | 2 +- api.planx.uk/send/createSendEvents.ts | 11 +- api.planx.uk/shared/middleware/validate.ts | 1 + 26 files changed, 637 insertions(+), 332 deletions(-) delete mode 100644 api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.ts delete mode 100644 api.planx.uk/modules/webhooks/_old/paymentRequestEvents.ts rename api.planx.uk/modules/webhooks/{_old/lowcalSessionEvents.test.ts => service/lowcalSessionEvents/index.test.ts} (76%) create mode 100644 api.planx.uk/modules/webhooks/service/lowcalSessionEvents/index.ts create mode 100644 api.planx.uk/modules/webhooks/service/lowcalSessionEvents/schema.ts rename api.planx.uk/modules/webhooks/{_old/paymentRequestEvents.test.ts => service/paymentRequestEvents/index.test.ts} (80%) create mode 100644 api.planx.uk/modules/webhooks/service/paymentRequestEvents/index.ts create mode 100644 api.planx.uk/modules/webhooks/service/paymentRequestEvents/schema.ts rename api.planx.uk/modules/webhooks/{_old => service}/sanitiseApplicationData/index.test.ts (96%) rename api.planx.uk/modules/webhooks/{_old => service}/sanitiseApplicationData/index.ts (61%) rename api.planx.uk/modules/webhooks/{_old => service}/sanitiseApplicationData/mocks/queries.ts (100%) rename api.planx.uk/modules/webhooks/{_old => service}/sanitiseApplicationData/operations.test.ts (100%) rename api.planx.uk/modules/webhooks/{_old => service}/sanitiseApplicationData/operations.ts (100%) rename api.planx.uk/modules/webhooks/{_old/sanitiseApplicationData/types.d.ts => service/sanitiseApplicationData/types.ts} (53%) rename api.planx.uk/modules/webhooks/{ => service}/sendNotification/index.test.ts (98%) rename api.planx.uk/modules/webhooks/{sendNotification/service.ts => service/sendNotification/index.ts} (97%) rename api.planx.uk/modules/webhooks/{ => service}/sendNotification/schema.ts (100%) rename api.planx.uk/modules/webhooks/{ => service}/sendNotification/types.ts (92%) diff --git a/api.planx.uk/hasura/metadata/index.ts b/api.planx.uk/hasura/metadata/index.ts index c9f7e79364..14829616b7 100644 --- a/api.planx.uk/hasura/metadata/index.ts +++ b/api.planx.uk/hasura/metadata/index.ts @@ -25,13 +25,18 @@ type RequiredScheduledEventArgs = Pick< "webhook" | "schedule_at" | "comment" | "payload" >; +export interface ScheduledEventResponse { + message: "success"; + event_id: string; +} + /** * POST a request to the Hasura Metadata API * https://hasura.io/docs/latest/graphql/core/api-reference/metadata-api/index/ */ const postToMetadataAPI = async ( body: ScheduledEvent, -): Promise> => { +): Promise> => { try { return await Axios.post( process.env.HASURA_METADATA_URL!, diff --git a/api.planx.uk/inviteToPay/createPaymentSendEvents.test.ts b/api.planx.uk/inviteToPay/createPaymentSendEvents.test.ts index d9eac45774..cb75bf84b1 100644 --- a/api.planx.uk/inviteToPay/createPaymentSendEvents.test.ts +++ b/api.planx.uk/inviteToPay/createPaymentSendEvents.test.ts @@ -9,6 +9,11 @@ const mockedCreateScheduledEvent = createScheduledEvent as jest.MockedFunction< typeof createScheduledEvent >; +const mockScheduledEventResponse = { + message: "success", + event_id: "abc123", +} as const; + describe("Create payment send events webhook", () => { const ENDPOINT = "/webhooks/hasura/create-payment-send-events"; @@ -83,7 +88,7 @@ describe("Create payment send events webhook", () => { }); it("returns a 200 on successful event setup", async () => { - mockedCreateScheduledEvent.mockResolvedValue("test-event-id"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await supertest(app) .post(ENDPOINT) @@ -91,13 +96,15 @@ describe("Create payment send events webhook", () => { .send({ payload: { sessionId: "123" } }) .expect(200) .then((response) => { - expect(response.body).toMatchObject({ email: "test-event-id" }); + expect(response.body).toMatchObject({ + email: mockScheduledEventResponse, + }); }); }); it("passes the correct arguments along to createScheduledEvent", async () => { const body = { createdAt: new Date(), payload: { sessionId: "123" } }; - mockedCreateScheduledEvent.mockResolvedValue("test-event-id"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await supertest(app) .post(ENDPOINT) @@ -105,7 +112,9 @@ describe("Create payment send events webhook", () => { .send(body) .expect(200) .then((response) => { - expect(response.body).toMatchObject({ email: "test-event-id" }); + expect(response.body).toMatchObject({ + email: mockScheduledEventResponse, + }); }); const mockArgs = mockedCreateScheduledEvent.mock.calls[0][0]; diff --git a/api.planx.uk/inviteToPay/createPaymentSendEvents.ts b/api.planx.uk/inviteToPay/createPaymentSendEvents.ts index 3b34947ec1..7e2b7e05ef 100644 --- a/api.planx.uk/inviteToPay/createPaymentSendEvents.ts +++ b/api.planx.uk/inviteToPay/createPaymentSendEvents.ts @@ -3,7 +3,10 @@ import { NextFunction, Request, Response } from "express"; import { gql } from "graphql-request"; import { $admin } from "../client"; import { adminGraphQLClient as adminClient } from "../hasura"; -import { createScheduledEvent } from "../hasura/metadata"; +import { + ScheduledEventResponse, + createScheduledEvent, +} from "../hasura/metadata"; import { getMostRecentPublishedFlow } from "../helpers"; import { Flow, Node, Team } from "../types"; @@ -14,9 +17,9 @@ enum Destination { } interface CombinedResponse { - bops?: Record; - uniform?: Record; - email?: Record; + bops?: ScheduledEventResponse; + uniform?: ScheduledEventResponse; + email?: ScheduledEventResponse; } // Create "One-off Scheduled Events" in Hasura when a payment request is paid diff --git a/api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.ts b/api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.ts deleted file mode 100644 index 1d50affdc3..0000000000 --- a/api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { addDays } from "date-fns"; -import { Request, Response, NextFunction } from "express"; - -import { createScheduledEvent } from "../../../hasura/metadata"; -import { - DAYS_UNTIL_EXPIRY, - REMINDER_DAYS_FROM_EXPIRY, -} from "../../../saveAndReturn/utils"; - -/** - * Create "reminder" events for a lowcal_session record - */ -const createReminderEvent = async ( - req: Request, - res: Response, - next: NextFunction, -): Promise => { - try { - const { createdAt, payload } = req.body; - if (!createdAt || !payload) - return next({ - status: 400, - message: "Required value missing", - }); - const response = await Promise.all( - REMINDER_DAYS_FROM_EXPIRY.map((day: number) => - createScheduledEvent({ - webhook: "{{HASURA_PLANX_API_URL}}/send-email/reminder", - schedule_at: addDays(Date.parse(createdAt), DAYS_UNTIL_EXPIRY - day), - payload: payload, - comment: `reminder_${payload.sessionId}_${day}day`, - }), - ), - ); - res.json(response); - } catch (error) { - return next({ - error, - message: `Failed to create reminder event. Error: ${error}`, - }); - } -}; - -/** - * Create an "expiry" event for a lowcal_session record - */ -const createExpiryEvent = async ( - req: Request, - res: Response, - next: NextFunction, -): Promise => { - try { - const { createdAt, payload } = req.body; - if (!createdAt || !payload) - return next({ - status: 400, - message: "Required value missing", - }); - const response = await createScheduledEvent({ - webhook: "{{HASURA_PLANX_API_URL}}/send-email/expiry", - schedule_at: addDays(Date.parse(createdAt), DAYS_UNTIL_EXPIRY), - payload: payload, - comment: `expiry_${payload.sessionId}`, - }); - res.json(response); - } catch (error) { - return next({ - error, - message: `Failed to create expiry event. Error: ${ - (error as Error).message - }`, - }); - } -}; - -export { createReminderEvent, createExpiryEvent }; diff --git a/api.planx.uk/modules/webhooks/_old/paymentRequestEvents.ts b/api.planx.uk/modules/webhooks/_old/paymentRequestEvents.ts deleted file mode 100644 index 30a387b228..0000000000 --- a/api.planx.uk/modules/webhooks/_old/paymentRequestEvents.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { addDays } from "date-fns"; -import { Request, Response, NextFunction } from "express"; - -import { createScheduledEvent } from "../../../hasura/metadata"; -import { - DAYS_UNTIL_EXPIRY, - REMINDER_DAYS_FROM_EXPIRY, -} from "../../../saveAndReturn/utils"; - -/** - * Create two "invitation" events for a payments_request record: one for the nominee and one for the agent - */ -const createPaymentInvitationEvents = async ( - req: Request, - res: Response, - next: NextFunction, -): Promise => { - try { - const { createdAt, payload } = req.body; - if (!createdAt || !payload) - return next({ - status: 400, - message: "Required value missing", - }); - const response = await Promise.all([ - createScheduledEvent({ - webhook: "{{HASURA_PLANX_API_URL}}/send-email/invite-to-pay", - schedule_at: createdAt, - payload: payload, - comment: `payment_invitation_${payload.paymentRequestId}`, - }), - createScheduledEvent({ - webhook: "{{HASURA_PLANX_API_URL}}/send-email/invite-to-pay-agent", - schedule_at: createdAt, - payload: payload, - comment: `payment_invitation_agent_${payload.paymentRequestId}`, - }), - ]); - res.json(response); - } catch (error) { - return next({ - error, - message: `Failed to create payment invitation events. Error: ${error}`, - }); - } -}; - -/** - * Create "reminder" events for a payment_requests record: one for the nominee and one for the agent - */ -const createPaymentReminderEvents = async ( - req: Request, - res: Response, - next: NextFunction, -): Promise => { - try { - const { createdAt, payload } = req.body; - if (!createdAt || !payload) - return next({ - status: 400, - message: "Required value missing", - }); - const applicantResponse = await Promise.all( - REMINDER_DAYS_FROM_EXPIRY.map((day: number) => - createScheduledEvent({ - webhook: "{{HASURA_PLANX_API_URL}}/send-email/payment-reminder", - schedule_at: addDays(Date.parse(createdAt), DAYS_UNTIL_EXPIRY - day), - payload: payload, - comment: `payment_reminder_${payload.paymentRequestId}_${day}day`, - }), - ), - ); - const agentResponse = await Promise.all( - REMINDER_DAYS_FROM_EXPIRY.map((day: number) => - createScheduledEvent({ - webhook: "{{HASURA_PLANX_API_URL}}/send-email/payment-reminder-agent", - schedule_at: addDays(Date.parse(createdAt), DAYS_UNTIL_EXPIRY - day), - payload: payload, - comment: `payment_reminder_agent_${payload.paymentRequestId}_${day}day`, - }), - ), - ); - res.json([...applicantResponse, ...agentResponse]); - } catch (error) { - return next({ - error, - message: `Failed to create payment reminder events. Error: ${error}`, - }); - } -}; - -/** - * Create two "expiry" events for a payment_requests record: one for the nominee and one for the agent - */ -const createPaymentExpiryEvents = async ( - req: Request, - res: Response, - next: NextFunction, -): Promise => { - try { - const { createdAt, payload } = req.body; - if (!createdAt || !payload) - return next({ - status: 400, - message: "Required value missing", - }); - const response = await Promise.all([ - createScheduledEvent({ - webhook: "{{HASURA_PLANX_API_URL}}/send-email/payment-expiry", - schedule_at: addDays(Date.parse(createdAt), DAYS_UNTIL_EXPIRY), - payload: payload, - comment: `payment_expiry_${payload.paymentRequestId}`, - }), - createScheduledEvent({ - webhook: "{{HASURA_PLANX_API_URL}}/send-email/payment-expiry-agent", - schedule_at: addDays(Date.parse(createdAt), DAYS_UNTIL_EXPIRY), - payload: payload, - comment: `payment_expiry_agent_${payload.paymentRequestId}`, - }), - ]); - res.json(response); - } catch (error) { - return next({ - error, - message: `Failed to create payment expiry events. Error: ${ - (error as Error).message - }`, - }); - } -}; - -export { - createPaymentInvitationEvents, - createPaymentReminderEvents, - createPaymentExpiryEvents, -}; diff --git a/api.planx.uk/modules/webhooks/controller.ts b/api.planx.uk/modules/webhooks/controller.ts index 8b98358f89..1621159778 100644 --- a/api.planx.uk/modules/webhooks/controller.ts +++ b/api.planx.uk/modules/webhooks/controller.ts @@ -1,6 +1,19 @@ import { ServerError } from "../../errors"; -import { sendSlackNotification } from "./sendNotification/service"; -import { SendSlackNotification } from "./sendNotification/types"; +import { CreateSessionEventController } from "./service/lowcalSessionEvents/schema"; +import { + createSessionExpiryEvent, + createSessionReminderEvent, +} from "./service/lowcalSessionEvents"; +import { SendSlackNotification } from "./service/sendNotification/types"; +import { sendSlackNotification } from "./service/sendNotification"; +import { CreatePaymentEventController } from "./service/paymentRequestEvents/schema"; +import { + createPaymentExpiryEvents, + createPaymentInvitationEvents, + createPaymentReminderEvents, +} from "./service/paymentRequestEvents"; +import { SanitiseApplicationData } from "./service/sanitiseApplicationData/types"; +import { sanitiseApplicationData } from "./service/sanitiseApplicationData"; export const sendSlackNotificationController: SendSlackNotification = async ( req, @@ -29,3 +42,94 @@ export const sendSlackNotificationController: SendSlackNotification = async ( ); } }; + +export const createPaymentInvitationEventsController: CreatePaymentEventController = + async (req, res, next) => { + try { + const response = await createPaymentInvitationEvents(req.body); + res.json(response); + } catch (error) { + return next( + new ServerError({ + message: `Failed to create payment invitation events`, + cause: error, + }), + ); + } + }; + +export const createPaymentReminderEventsController: CreatePaymentEventController = + async (req, res, next) => { + try { + const response = await createPaymentReminderEvents(req.body); + res.json(response); + } catch (error) { + return next( + new ServerError({ + message: "Failed to create payment reminder events", + cause: error, + }), + ); + } + }; + +export const createPaymentExpiryEventsController: CreatePaymentEventController = + async (req, res, next) => { + try { + const response = await createPaymentExpiryEvents(req.body); + res.json(response); + } catch (error) { + return next( + new ServerError({ + message: "Failed to create payment expiry events", + cause: error, + }), + ); + } + }; + +export const createSessionReminderEventController: CreateSessionEventController = + async (req, res, next) => { + try { + const response = await createSessionReminderEvent(req.body); + res.json(response); + } catch (error) { + return next( + new ServerError({ + message: "Failed to create session reminder event", + cause: error, + }), + ); + } + }; + +export const createSessionExpiryEventController: CreateSessionEventController = + async (req, res, next) => { + try { + const response = await createSessionExpiryEvent(req.body); + res.json(response); + } catch (error) { + return next( + new ServerError({ + message: "Failed to create session expiry event", + cause: error, + }), + ); + } + }; + +export const sanitiseApplicationDataController: SanitiseApplicationData = + async (_req, res, next) => { + try { + const { operationFailed, results } = await sanitiseApplicationData(); + if (operationFailed) res.status(500); + return res.json(results); + } catch (error) { + return next( + new ServerError({ + message: "Failed to sanitise application data", + cause: error, + }), + ); + } + }; diff --git a/api.planx.uk/modules/webhooks/docs.yaml b/api.planx.uk/modules/webhooks/docs.yaml index 837e64a8ea..b200378244 100644 --- a/api.planx.uk/modules/webhooks/docs.yaml +++ b/api.planx.uk/modules/webhooks/docs.yaml @@ -14,7 +14,7 @@ components: type: string required: - sessionId - BopsSubmissionSchema: + BopsSubmission: type: object properties: body: @@ -37,7 +37,7 @@ components: type: string required: - body - UniformSubmissionSchema: + UniformSubmission: type: object properties: body: @@ -63,7 +63,7 @@ components: type: string required: - body - EmailSubmissionSchema: + EmailSubmission: type: object properties: body: @@ -92,11 +92,37 @@ components: type: string required: - body - SendSlackNotificationSchema: + SendSlackNotification: oneOf: - - $ref: "#/components/schemas/BopsSubmissionSchema" - - $ref: "#/components/schemas/UniformSubmissionSchema" - - $ref: "#/components/schemas/EmailSubmissionSchema" + - $ref: "#/components/schemas/BopsSubmission" + - $ref: "#/components/schemas/UniformSubmission" + - $ref: "#/components/schemas/EmailSubmission" + CreatePaymentEvent: + type: object + properties: + createdAt: + required: true + type: string + format: date-time + payload: + required: true + type: object + properties: + paymentRequestId: + type: string + CreateSessionEvent: + type: object + properties: + createdAt: + required: true + type: string + format: date-time + payload: + required: true + type: object + properties: + sessionId: + type: string responses: SlackNotificationSuccessMessage: content: @@ -111,6 +137,56 @@ components: message: type: string required: true + ScheduledEvent: + content: + application/json: + schema: + type: array + items: + type: object + properties: + message: + type: string + enum: ["success"] + event_id: + type: string + description: Internal Hasura ID of the generated event + OperationResultSuccess: + content: + application/json: + schema: + type: array + items: + type: object + properties: + operationName: + type: string + status: + type: string + enum: + - success + count: + type: integer + OperationResultFailure: + content: + application/json: + schema: + type: array + items: + type: object + properties: + operationName: + type: string + status: + type: string + enum: + - success + - failure + count: + type: integer + errorMessage: + type: string + required: false paths: /webhooks/hasura/sendSlackNotification: post: @@ -127,9 +203,94 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/SendSlackNotificationSchema" + $ref: "#/components/schemas/SendSlackNotification" responses: "200": $ref: "#/components/responses/SlackNotificationSuccessMessage" "500": $ref: "#/components/responses/ErrorMessage" + /webhooks/hasura/create-payment-invitation-events: + post: + tags: ["webhooks"] + summary: Create payment invitation events + description: Setup events which will trigger payment invitation emails to be generated + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreatePaymentEvent" + responses: + "200": + $ref: "#/components/responses/ScheduledEvent" + "500": + $ref: "#/components/responses/ErrorMessage" + /webhooks/hasura/create-payment-reminder-events: + post: + tags: ["webhooks"] + summary: Create payment reminder events + description: Setup events which will trigger payment reminder emails to be generated + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreatePaymentEvent" + responses: + "200": + $ref: "#/components/responses/ScheduledEvent" + "500": + $ref: "#/components/responses/ErrorMessage" + /webhooks/hasura/create-payment-expiry-events: + post: + tags: ["webhooks"] + summary: Create payment expiry events + description: Setup events which will trigger payment expiry emails to be generated + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreatePaymentEvent" + responses: + "200": + $ref: "#/components/responses/ScheduledEvent" + "500": + $ref: "#/components/responses/ErrorMessage" + /webhooks/hasura/create-reminder-event: + post: + tags: ["webhooks"] + summary: Create session reminder event + description: Setup events which will trigger session reminder emails to be generated + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreateSessionEvent" + responses: + "200": + $ref: "#/components/responses/ScheduledEvent" + "500": + $ref: "#/components/responses/ErrorMessage" + /webhooks/hasura/create-expiry-event: + post: + tags: ["webhooks"] + summary: Create session expiry event + description: Setup events which will trigger session expiry email to be generated + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CreateSessionEvent" + responses: + "200": + $ref: "#/components/responses/ScheduledEvent" + "500": + $ref: "#/components/responses/ErrorMessage" + /webhooks/hasura/sanitise-application-data: + post: + tags: ["webhooks"] + summary: Sanitise application data + description: Called by Hasura cron job "sanitise_application_data" on a nightly basis + responses: + "200": + $ref: "#/components/responses/OperationResultSuccess" + "500": + $ref: "#/components/responses/OperationResultFailure" diff --git a/api.planx.uk/modules/webhooks/routes.ts b/api.planx.uk/modules/webhooks/routes.ts index c98421b7e3..dc7c1bbbcd 100644 --- a/api.planx.uk/modules/webhooks/routes.ts +++ b/api.planx.uk/modules/webhooks/routes.ts @@ -1,40 +1,59 @@ import { Router } from "express"; import { useHasuraAuth } from "../auth/middleware"; import { createPaymentSendEvents } from "../../inviteToPay/createPaymentSendEvents"; -import { sanitiseApplicationData } from "./_old/sanitiseApplicationData"; -import { - createExpiryEvent, - createReminderEvent, -} from "./_old/lowcalSessionEvents"; -import { - createPaymentExpiryEvents, - createPaymentInvitationEvents, - createPaymentReminderEvents, -} from "./_old/paymentRequestEvents"; import { validate } from "../../shared/middleware/validate"; -import { sendSlackNotificationController } from "./controller"; -import { sendSlackNotificationSchema } from "./sendNotification/schema"; +import { + createPaymentExpiryEventsController, + createPaymentInvitationEventsController, + createPaymentReminderEventsController, + createSessionExpiryEventController, + createSessionReminderEventController, + sanitiseApplicationDataController, + sendSlackNotificationController, +} from "./controller"; +import { sendSlackNotificationSchema } from "./service/sendNotification/schema"; +import { createPaymentEventSchema } from "./service/paymentRequestEvents/schema"; +import { createSessionEventSchema } from "./service/lowcalSessionEvents/schema"; const router = Router(); router.use("/hasura", useHasuraAuth); -router.post("/hasura/create-reminder-event", createReminderEvent); -router.post("/hasura/create-expiry-event", createExpiryEvent); router.post( "/hasura/create-payment-invitation-events", - createPaymentInvitationEvents, + validate(createPaymentEventSchema), + createPaymentInvitationEventsController, ); router.post( "/hasura/create-payment-reminder-events", - createPaymentReminderEvents, + validate(createPaymentEventSchema), + createPaymentReminderEventsController, +); +router.post( + "/hasura/create-payment-expiry-events", + validate(createPaymentEventSchema), + createPaymentExpiryEventsController, ); -router.post("/hasura/create-payment-expiry-events", createPaymentExpiryEvents); -router.post("/hasura/create-payment-send-events", createPaymentSendEvents); router.post( "/hasura/send-slack-notification", validate(sendSlackNotificationSchema), sendSlackNotificationController, ); -router.post("/hasura/sanitise-application-data", sanitiseApplicationData); +router.post( + "/hasura/create-reminder-event", + validate(createSessionEventSchema), + createSessionReminderEventController, +); +router.post( + "/hasura/create-expiry-event", + validate(createSessionEventSchema), + createSessionExpiryEventController, +); +router.post( + "/hasura/sanitise-application-data", + sanitiseApplicationDataController, +); + +// TODO: Convert to the new API module structure +router.post("/hasura/create-payment-send-events", createPaymentSendEvents); export default router; diff --git a/api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.test.ts b/api.planx.uk/modules/webhooks/service/lowcalSessionEvents/index.test.ts similarity index 76% rename from api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.test.ts rename to api.planx.uk/modules/webhooks/service/lowcalSessionEvents/index.test.ts index 0cddfe272b..8f557a4686 100644 --- a/api.planx.uk/modules/webhooks/_old/lowcalSessionEvents.test.ts +++ b/api.planx.uk/modules/webhooks/service/lowcalSessionEvents/index.test.ts @@ -1,14 +1,19 @@ import supertest from "supertest"; -import app from "../../../server"; -import { createScheduledEvent } from "../../../hasura/metadata"; +import app from "../../../../server"; +import { createScheduledEvent } from "../../../../hasura/metadata"; const { post } = supertest(app); -jest.mock("../../../hasura/metadata"); +jest.mock("../../../../hasura/metadata"); const mockedCreateScheduledEvent = createScheduledEvent as jest.MockedFunction< typeof createScheduledEvent >; +const mockScheduledEventResponse = { + message: "success", + event_id: "abc123", +} as const; + describe("Create reminder event webhook", () => { const ENDPOINT = "/webhooks/hasura/create-reminder-event"; @@ -33,15 +38,16 @@ describe("Create reminder event webhook", () => { .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) .send(body) .expect(400) - .then((response) => - expect(response.body.error).toEqual("Required value missing"), - ); + .then((response) => { + expect(response.body).toHaveProperty("issues"); + expect(response.body).toHaveProperty("name", "ZodError"); + }); } }); it("returns a 200 on successful event setup", async () => { const body = { createdAt: new Date(), payload: { sessionId: "123" } }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) @@ -50,20 +56,26 @@ describe("Create reminder event webhook", () => { .then((response) => { // it's queued up x2 reminders for 7 days and 1 day from expiry expect(response.body).toHaveLength(2); - expect(response.body).toStrictEqual(["test", "test"]); + expect(response.body).toStrictEqual([ + mockScheduledEventResponse, + mockScheduledEventResponse, + ]); }); }); it("passes the correct arguments along to createScheduledEvent", async () => { const body = { createdAt: new Date(), payload: { sessionId: "123" } }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) .send(body) .expect(200) .then((response) => { - expect(response.body).toStrictEqual(["test", "test"]); + expect(response.body).toStrictEqual([ + mockScheduledEventResponse, + mockScheduledEventResponse, + ]); }); const mockArgs = mockedCreateScheduledEvent.mock.calls[0][0]; @@ -92,7 +104,9 @@ describe("Create reminder event webhook", () => { .send(body) .expect(500) .then((response) => { - expect(response.body.error).toMatch(/Failed to create reminder event/); + expect(response.body.error).toMatch( + /Failed to create session reminder event/, + ); }); }); }); @@ -121,35 +135,36 @@ describe("Create expiry event webhook", () => { .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) .send(body) .expect(400) - .then((response) => - expect(response.body.error).toEqual("Required value missing"), - ); + .then((response) => { + expect(response.body).toHaveProperty("issues"); + expect(response.body).toHaveProperty("name", "ZodError"); + }); } }); it("returns a 200 on successful event setup", async () => { const body = { createdAt: new Date(), payload: { sessionId: "123" } }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) .send(body) .expect(200) .then((response) => { - expect(response.body).toBe("test"); + expect(response.body).toStrictEqual([mockScheduledEventResponse]); }); }); it("passes the correct arguments along to createScheduledEvent", async () => { const body = { createdAt: new Date(), payload: { sessionId: "123" } }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) .send(body) .expect(200) .then((response) => { - expect(response.body).toBe("test"); + expect(response.body).toStrictEqual([mockScheduledEventResponse]); }); const mockArgs = mockedCreateScheduledEvent.mock.calls[0][0]; expect(mockArgs.webhook).toBe("{{HASURA_PLANX_API_URL}}/send-email/expiry"); @@ -166,7 +181,9 @@ describe("Create expiry event webhook", () => { .send(body) .expect(500) .then((response) => { - expect(response.body.error).toMatch(/Failed to create expiry event/); + expect(response.body.error).toMatch( + /Failed to create session expiry event/, + ); }); }); }); diff --git a/api.planx.uk/modules/webhooks/service/lowcalSessionEvents/index.ts b/api.planx.uk/modules/webhooks/service/lowcalSessionEvents/index.ts new file mode 100644 index 0000000000..b8ec4cf072 --- /dev/null +++ b/api.planx.uk/modules/webhooks/service/lowcalSessionEvents/index.ts @@ -0,0 +1,44 @@ +import { addDays } from "date-fns"; + +import { createScheduledEvent } from "../../../../hasura/metadata"; +import { + DAYS_UNTIL_EXPIRY, + REMINDER_DAYS_FROM_EXPIRY, +} from "../../../../saveAndReturn/utils"; +import { CreateSessionEvent } from "./schema"; + +/** + * Create "reminder" events for a lowcal_session record + */ +export const createSessionReminderEvent = async ({ + createdAt, + payload, +}: CreateSessionEvent) => { + const response = await Promise.all( + REMINDER_DAYS_FROM_EXPIRY.map((day: number) => + createScheduledEvent({ + webhook: "{{HASURA_PLANX_API_URL}}/send-email/reminder", + schedule_at: addDays(createdAt, DAYS_UNTIL_EXPIRY - day), + payload: payload, + comment: `reminder_${payload.sessionId}_${day}day`, + }), + ), + ); + return response; +}; + +/** + * Create an "expiry" event for a lowcal_session record + */ +export const createSessionExpiryEvent = async ({ + createdAt, + payload, +}: CreateSessionEvent) => { + const response = await createScheduledEvent({ + webhook: "{{HASURA_PLANX_API_URL}}/send-email/expiry", + schedule_at: addDays(createdAt, DAYS_UNTIL_EXPIRY), + payload: payload, + comment: `expiry_${payload.sessionId}`, + }); + return [response]; +}; diff --git a/api.planx.uk/modules/webhooks/service/lowcalSessionEvents/schema.ts b/api.planx.uk/modules/webhooks/service/lowcalSessionEvents/schema.ts new file mode 100644 index 0000000000..53e73f0b06 --- /dev/null +++ b/api.planx.uk/modules/webhooks/service/lowcalSessionEvents/schema.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; +import { ValidatedRequestHandler } from "../../../../shared/middleware/validate"; +import { ScheduledEventResponse } from "../../../../hasura/metadata"; + +export const createSessionEventSchema = z.object({ + body: z.object({ + createdAt: z.string().transform((val) => new Date(val)), + payload: z.object({ + sessionId: z.string(), + }), + }), +}); + +export type CreateSessionEvent = z.infer< + typeof createSessionEventSchema +>["body"]; + +export type CreateSessionEventController = ValidatedRequestHandler< + typeof createSessionEventSchema, + ScheduledEventResponse[] +>; diff --git a/api.planx.uk/modules/webhooks/_old/paymentRequestEvents.test.ts b/api.planx.uk/modules/webhooks/service/paymentRequestEvents/index.test.ts similarity index 80% rename from api.planx.uk/modules/webhooks/_old/paymentRequestEvents.test.ts rename to api.planx.uk/modules/webhooks/service/paymentRequestEvents/index.test.ts index 282ca011a5..20beeefd98 100644 --- a/api.planx.uk/modules/webhooks/_old/paymentRequestEvents.test.ts +++ b/api.planx.uk/modules/webhooks/service/paymentRequestEvents/index.test.ts @@ -1,14 +1,20 @@ import supertest from "supertest"; -import app from "../../../server"; -import { createScheduledEvent } from "../../../hasura/metadata"; +import app from "../../../../server"; +import { createScheduledEvent } from "../../../../hasura/metadata"; +import { CreatePaymentEvent } from "./schema"; const { post } = supertest(app); -jest.mock("../../../hasura/metadata"); +jest.mock("../../../../hasura/metadata"); const mockedCreateScheduledEvent = createScheduledEvent as jest.MockedFunction< typeof createScheduledEvent >; +const mockScheduledEventResponse = { + message: "success", + event_id: "abc123", +} as const; + describe("Create payment invitation events webhook", () => { const ENDPOINT = "/webhooks/hasura/create-payment-invitation-events"; @@ -33,18 +39,19 @@ describe("Create payment invitation events webhook", () => { .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) .send(body) .expect(400) - .then((response) => - expect(response.body.error).toEqual("Required value missing"), - ); + .then((response) => { + expect(response.body).toHaveProperty("issues"); + expect(response.body).toHaveProperty("name", "ZodError"); + }); } }); it("returns a 200 on successful event setup", async () => { - const body = { + const body: CreatePaymentEvent = { createdAt: new Date(), payload: { paymentRequestId: "123" }, }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) @@ -53,7 +60,10 @@ describe("Create payment invitation events webhook", () => { .then((response) => { // it's queued up x2 invitations: one for the payee and one for the agent expect(response.body).toHaveLength(2); - expect(response.body).toStrictEqual(["test", "test"]); + expect(response.body).toStrictEqual([ + mockScheduledEventResponse, + mockScheduledEventResponse, + ]); }); }); @@ -62,7 +72,7 @@ describe("Create payment invitation events webhook", () => { createdAt: new Date(), payload: { paymentRequestId: "123" }, }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) @@ -70,7 +80,10 @@ describe("Create payment invitation events webhook", () => { .expect(200) .then((response) => { expect(response.body).toHaveLength(2); - expect(response.body).toStrictEqual(["test", "test"]); + expect(response.body).toStrictEqual([ + mockScheduledEventResponse, + mockScheduledEventResponse, + ]); }); const mockArgs = mockedCreateScheduledEvent.mock.calls[0][0]; @@ -135,9 +148,10 @@ describe("Create payment reminder events webhook", () => { .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) .send(body) .expect(400) - .then((response) => - expect(response.body.error).toEqual("Required value missing"), - ); + .then((response) => { + expect(response.body).toHaveProperty("issues"); + expect(response.body).toHaveProperty("name", "ZodError"); + }); } }); @@ -146,7 +160,7 @@ describe("Create payment reminder events webhook", () => { createdAt: new Date(), payload: { paymentRequestId: "123" }, }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) @@ -155,7 +169,12 @@ describe("Create payment reminder events webhook", () => { .then((response) => { // it's queued up x4 reminders: 2 for the payee and 2 for the agent at 7 days and 1 day from expiry expect(response.body).toHaveLength(4); - expect(response.body).toStrictEqual(["test", "test", "test", "test"]); + expect(response.body).toStrictEqual([ + mockScheduledEventResponse, + mockScheduledEventResponse, + mockScheduledEventResponse, + mockScheduledEventResponse, + ]); }); }); @@ -164,7 +183,7 @@ describe("Create payment reminder events webhook", () => { createdAt: new Date(), payload: { paymentRequestId: "123" }, }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) @@ -172,7 +191,12 @@ describe("Create payment reminder events webhook", () => { .expect(200) .then((response) => { expect(response.body).toHaveLength(4); - expect(response.body).toStrictEqual(["test", "test", "test", "test"]); + expect(response.body).toStrictEqual([ + mockScheduledEventResponse, + mockScheduledEventResponse, + mockScheduledEventResponse, + mockScheduledEventResponse, + ]); }); const mockArgs = mockedCreateScheduledEvent.mock.calls[0][0]; @@ -255,9 +279,10 @@ describe("Create payment expiry events webhook", () => { .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) .send(body) .expect(400) - .then((response) => - expect(response.body.error).toEqual("Required value missing"), - ); + .then((response) => { + expect(response.body).toHaveProperty("issues"); + expect(response.body).toHaveProperty("name", "ZodError"); + }); } }); @@ -266,16 +291,19 @@ describe("Create payment expiry events webhook", () => { createdAt: new Date(), payload: { paymentRequestId: "123" }, }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) .send(body) .expect(200) .then((response) => { - // it's queued up x2 expirys: one for the payee and one for the agent + // it's queued up x2 expiries: one for the payee and one for the agent expect(response.body).toHaveLength(2); - expect(response.body).toStrictEqual(["test", "test"]); + expect(response.body).toStrictEqual([ + mockScheduledEventResponse, + mockScheduledEventResponse, + ]); }); }); @@ -284,7 +312,7 @@ describe("Create payment expiry events webhook", () => { createdAt: new Date(), payload: { paymentRequestId: "123" }, }; - mockedCreateScheduledEvent.mockResolvedValue("test"); + mockedCreateScheduledEvent.mockResolvedValue(mockScheduledEventResponse); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) @@ -292,7 +320,10 @@ describe("Create payment expiry events webhook", () => { .expect(200) .then((response) => { expect(response.body).toHaveLength(2); - expect(response.body).toStrictEqual(["test", "test"]); + expect(response.body).toStrictEqual([ + mockScheduledEventResponse, + mockScheduledEventResponse, + ]); }); const mockArgs = mockedCreateScheduledEvent.mock.calls[0][0]; diff --git a/api.planx.uk/modules/webhooks/service/paymentRequestEvents/index.ts b/api.planx.uk/modules/webhooks/service/paymentRequestEvents/index.ts new file mode 100644 index 0000000000..9679c7931e --- /dev/null +++ b/api.planx.uk/modules/webhooks/service/paymentRequestEvents/index.ts @@ -0,0 +1,86 @@ +import { addDays } from "date-fns"; + +import { createScheduledEvent } from "../../../../hasura/metadata"; +import { + DAYS_UNTIL_EXPIRY, + REMINDER_DAYS_FROM_EXPIRY, +} from "../../../../saveAndReturn/utils"; +import { CreatePaymentEvent } from "./schema"; + +/** + * Create two "invitation" events for a payments_request record: one for the nominee and one for the agent + */ +export const createPaymentInvitationEvents = async ({ + createdAt, + payload, +}: CreatePaymentEvent) => { + const response = await Promise.all([ + createScheduledEvent({ + webhook: "{{HASURA_PLANX_API_URL}}/send-email/invite-to-pay", + schedule_at: createdAt, + payload: payload, + comment: `payment_invitation_${payload.paymentRequestId}`, + }), + createScheduledEvent({ + webhook: "{{HASURA_PLANX_API_URL}}/send-email/invite-to-pay-agent", + schedule_at: createdAt, + payload: payload, + comment: `payment_invitation_agent_${payload.paymentRequestId}`, + }), + ]); + return response; +}; + +/** + * Create "reminder" events for a payment_requests record: one for the nominee and one for the agent + */ +export const createPaymentReminderEvents = async ({ + createdAt, + payload, +}: CreatePaymentEvent) => { + const applicantResponse = await Promise.all( + REMINDER_DAYS_FROM_EXPIRY.map((day: number) => + createScheduledEvent({ + webhook: "{{HASURA_PLANX_API_URL}}/send-email/payment-reminder", + schedule_at: addDays(createdAt, DAYS_UNTIL_EXPIRY - day), + payload: payload, + comment: `payment_reminder_${payload.paymentRequestId}_${day}day`, + }), + ), + ); + const agentResponse = await Promise.all( + REMINDER_DAYS_FROM_EXPIRY.map((day: number) => + createScheduledEvent({ + webhook: "{{HASURA_PLANX_API_URL}}/send-email/payment-reminder-agent", + schedule_at: addDays(createdAt, DAYS_UNTIL_EXPIRY - day), + payload: payload, + comment: `payment_reminder_agent_${payload.paymentRequestId}_${day}day`, + }), + ), + ); + return [...applicantResponse, ...agentResponse]; +}; + +/** + * Create two "expiry" events for a payment_requests record: one for the nominee and one for the agent + */ +export const createPaymentExpiryEvents = async ({ + createdAt, + payload, +}: CreatePaymentEvent) => { + const response = await Promise.all([ + createScheduledEvent({ + webhook: "{{HASURA_PLANX_API_URL}}/send-email/payment-expiry", + schedule_at: addDays(createdAt, DAYS_UNTIL_EXPIRY), + payload: payload, + comment: `payment_expiry_${payload.paymentRequestId}`, + }), + createScheduledEvent({ + webhook: "{{HASURA_PLANX_API_URL}}/send-email/payment-expiry-agent", + schedule_at: addDays(createdAt, DAYS_UNTIL_EXPIRY), + payload: payload, + comment: `payment_expiry_agent_${payload.paymentRequestId}`, + }), + ]); + return response; +}; diff --git a/api.planx.uk/modules/webhooks/service/paymentRequestEvents/schema.ts b/api.planx.uk/modules/webhooks/service/paymentRequestEvents/schema.ts new file mode 100644 index 0000000000..5b7a48404c --- /dev/null +++ b/api.planx.uk/modules/webhooks/service/paymentRequestEvents/schema.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; +import { ValidatedRequestHandler } from "../../../../shared/middleware/validate"; +import { ScheduledEventResponse } from "../../../../hasura/metadata"; + +export const createPaymentEventSchema = z.object({ + body: z.object({ + createdAt: z.string().transform((val) => new Date(val)), + payload: z.object({ + paymentRequestId: z.string(), + }), + }), +}); + +export type CreatePaymentEvent = z.infer< + typeof createPaymentEventSchema +>["body"]; + +export type CreatePaymentEventController = ValidatedRequestHandler< + typeof createPaymentEventSchema, + ScheduledEventResponse[] +>; diff --git a/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/index.test.ts b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/index.test.ts similarity index 96% rename from api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/index.test.ts rename to api.planx.uk/modules/webhooks/service/sanitiseApplicationData/index.test.ts index 96da6ca4b3..a4930c9178 100644 --- a/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/index.test.ts +++ b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/index.test.ts @@ -30,13 +30,15 @@ describe("Sanitise application data webhook", () => { it("returns a 500 if an unhandled error is thrown whilst running operations", async () => { const mockOperationHandler = jest.spyOn(operations, "operationHandler"); - mockOperationHandler.mockRejectedValueOnce(new Error("Unhandled error!")); + mockOperationHandler.mockRejectedValueOnce("Unhandled error!"); await post(ENDPOINT) .set({ Authorization: process.env.HASURA_PLANX_API_KEY }) .expect(500) .then((response) => - expect(response.body.error).toMatch(/Unhandled error!/), + expect(response.body.error).toMatch( + /Failed to sanitise application data/, + ), ); }); diff --git a/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/index.ts b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/index.ts similarity index 61% rename from api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/index.ts rename to api.planx.uk/modules/webhooks/service/sanitiseApplicationData/index.ts index 9454104edf..af9cecf7c3 100644 --- a/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/index.ts +++ b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/index.ts @@ -9,35 +9,19 @@ import { getFormattedEnvironment } from "../../../../helpers"; * Called by Hasura cron job `sanitise_application_data` on a nightly basis * See hasura.planx.uk/metadata/cron_triggers.yaml */ -export const sanitiseApplicationData = async ( - _req: Request, - res: Response, - next: NextFunction, -): Promise => { +export const sanitiseApplicationData = async () => { const operations = getOperations(); const results: OperationResult[] = []; - try { - for (const operation of operations) { - const result = await operationHandler(operation); - results.push(result); - } - } catch (error) { - // Unhandled error, flag with Airbrake - return next({ - error, - message: `Failed to sanitise application data. ${ - (error as Error).message - }`, - }); - } - const operationFailed = results.find((result) => result.status === "failure"); - if (operationFailed) { - await postToSlack(results); - res.status(500); + for (const operation of operations) { + const result = await operationHandler(operation); + results.push(result); } - return res.json(results); + const operationFailed = results.some((result) => result.status === "failure"); + if (operationFailed) await postToSlack(results); + + return { operationFailed, results }; }; export const postToSlack = async (results: OperationResult[]) => { diff --git a/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/mocks/queries.ts b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/mocks/queries.ts similarity index 100% rename from api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/mocks/queries.ts rename to api.planx.uk/modules/webhooks/service/sanitiseApplicationData/mocks/queries.ts diff --git a/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/operations.test.ts b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.test.ts similarity index 100% rename from api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/operations.test.ts rename to api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.test.ts diff --git a/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/operations.ts b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.ts similarity index 100% rename from api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/operations.ts rename to api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.ts diff --git a/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/types.d.ts b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/types.ts similarity index 53% rename from api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/types.d.ts rename to api.planx.uk/modules/webhooks/service/sanitiseApplicationData/types.ts index 55bd0d8bc5..88b9df2804 100644 --- a/api.planx.uk/modules/webhooks/_old/sanitiseApplicationData/types.d.ts +++ b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/types.ts @@ -1,3 +1,6 @@ +import { z } from "zod"; +import { ValidatedRequestHandler } from "../../../../shared/middleware/validate"; + export interface OperationResult { operationName: string; status: "processing" | "success" | "failure"; @@ -8,3 +11,8 @@ export interface OperationResult { export type QueryResult = string[]; export type Operation = () => Promise; + +export type SanitiseApplicationData = ValidatedRequestHandler< + z.ZodUndefined, + OperationResult[] +>; diff --git a/api.planx.uk/modules/webhooks/sendNotification/index.test.ts b/api.planx.uk/modules/webhooks/service/sendNotification/index.test.ts similarity index 98% rename from api.planx.uk/modules/webhooks/sendNotification/index.test.ts rename to api.planx.uk/modules/webhooks/service/sendNotification/index.test.ts index e73c115371..d3a4d25a54 100644 --- a/api.planx.uk/modules/webhooks/sendNotification/index.test.ts +++ b/api.planx.uk/modules/webhooks/service/sendNotification/index.test.ts @@ -1,8 +1,8 @@ import supertest from "supertest"; -import app from "../../../server"; +import app from "../../../../server"; import SlackNotify from "slack-notify"; import { BOPSBody, EmailBody, UniformBody } from "./types"; -import { $admin } from "../../../client"; +import { $admin } from "../../../../client"; import { CoreDomainClient } from "@opensystemslab/planx-core"; const mockSessionWithFee = { @@ -35,7 +35,7 @@ const mockSessionWithResubmissionExemption = { }, }; -jest.mock("../../../client"); +jest.mock("../../../../client"); const mockAdmin = jest.mocked($admin); const mockSend = jest.fn(); @@ -208,10 +208,8 @@ describe("Send Slack notifications endpoint", () => { ); expect(mockSend).toHaveBeenCalledTimes(1); expect(mockAdmin.session.find).toHaveBeenCalledTimes(1); - expect(response.body.message).toBe("Posted to Slack"); expect(response.body.data).toMatch(/abc123/); - expect(response.body.data).toMatch(/test-council/); }); }); diff --git a/api.planx.uk/modules/webhooks/sendNotification/service.ts b/api.planx.uk/modules/webhooks/service/sendNotification/index.ts similarity index 97% rename from api.planx.uk/modules/webhooks/sendNotification/service.ts rename to api.planx.uk/modules/webhooks/service/sendNotification/index.ts index be8678a0e9..e0dfd36947 100644 --- a/api.planx.uk/modules/webhooks/sendNotification/service.ts +++ b/api.planx.uk/modules/webhooks/service/sendNotification/index.ts @@ -7,7 +7,7 @@ import { EventType, UniformEventData, } from "./types"; -import { $admin } from "../../../client"; +import { $admin } from "../../../../client"; export const sendSlackNotification = async ( data: EventData, diff --git a/api.planx.uk/modules/webhooks/sendNotification/schema.ts b/api.planx.uk/modules/webhooks/service/sendNotification/schema.ts similarity index 100% rename from api.planx.uk/modules/webhooks/sendNotification/schema.ts rename to api.planx.uk/modules/webhooks/service/sendNotification/schema.ts diff --git a/api.planx.uk/modules/webhooks/sendNotification/types.ts b/api.planx.uk/modules/webhooks/service/sendNotification/types.ts similarity index 92% rename from api.planx.uk/modules/webhooks/sendNotification/types.ts rename to api.planx.uk/modules/webhooks/service/sendNotification/types.ts index cd1e293bf9..1575f45429 100644 --- a/api.planx.uk/modules/webhooks/sendNotification/types.ts +++ b/api.planx.uk/modules/webhooks/service/sendNotification/types.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { ValidatedRequestHandler } from "../../../shared/middleware/validate"; +import { ValidatedRequestHandler } from "../../../../shared/middleware/validate"; import { bopsSubmissionSchema, emailSubmissionSchema, diff --git a/api.planx.uk/send/createSendEvents.ts b/api.planx.uk/send/createSendEvents.ts index ed611c121f..8f5c12af4e 100644 --- a/api.planx.uk/send/createSendEvents.ts +++ b/api.planx.uk/send/createSendEvents.ts @@ -1,10 +1,13 @@ import { NextFunction, Request, Response } from "express"; -import { createScheduledEvent } from "../hasura/metadata"; +import { + ScheduledEventResponse, + createScheduledEvent, +} from "../hasura/metadata"; interface CombinedResponse { - bops?: Record; - uniform?: Record; - email?: Record; + bops?: ScheduledEventResponse; + uniform?: ScheduledEventResponse; + email?: ScheduledEventResponse; } // Create "One-off Scheduled Events" in Hasura from Send component for selected destinations diff --git a/api.planx.uk/shared/middleware/validate.ts b/api.planx.uk/shared/middleware/validate.ts index ce647f2f16..854a37938e 100644 --- a/api.planx.uk/shared/middleware/validate.ts +++ b/api.planx.uk/shared/middleware/validate.ts @@ -20,6 +20,7 @@ export const validate = }); return next(); } catch (error) { + console.error(error); return res.status(400).json(error); } }; From eca3963b22531c8bcb96c79437f756a2795a9a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Wed, 4 Oct 2023 09:48:13 +0100 Subject: [PATCH 5/8] chore: Update Zod (CVE-2023-4316) (#2277) * chore: Update Zod (CVE-2023-4316) * chore: Bump planx-core --- api.planx.uk/package.json | 4 +- api.planx.uk/pnpm-lock.yaml | 78 ++++++++--------- e2e/tests/api-driven/package.json | 2 +- e2e/tests/api-driven/pnpm-lock.yaml | 74 ++++++++--------- e2e/tests/ui-driven/package.json | 2 +- e2e/tests/ui-driven/pnpm-lock.yaml | 74 ++++++++--------- editor.planx.uk/package.json | 4 +- editor.planx.uk/pnpm-lock.yaml | 124 +++++++++++++++++++++------- 8 files changed, 212 insertions(+), 150 deletions(-) diff --git a/api.planx.uk/package.json b/api.planx.uk/package.json index eb86c57d6c..a8d81c4717 100644 --- a/api.planx.uk/package.json +++ b/api.planx.uk/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "@airbrake/node": "^2.1.8", - "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#3d395fa", + "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#d92224b", "@types/isomorphic-fetch": "^0.0.36", "adm-zip": "^0.5.10", "aws-sdk": "^2.1467.0", @@ -42,7 +42,7 @@ "string-to-stream": "^3.0.1", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0", - "zod": "^3.22.2" + "zod": "^3.22.3" }, "scripts": { "dev": "ts-node-dev --files index.ts", diff --git a/api.planx.uk/pnpm-lock.yaml b/api.planx.uk/pnpm-lock.yaml index e5dad92bca..7948276e87 100644 --- a/api.planx.uk/pnpm-lock.yaml +++ b/api.planx.uk/pnpm-lock.yaml @@ -9,8 +9,8 @@ dependencies: specifier: ^2.1.8 version: 2.1.8 '@opensystemslab/planx-core': - specifier: git+https://github.com/theopensystemslab/planx-core#3d395fa - version: github.com/theopensystemslab/planx-core/3d395fa + specifier: git+https://github.com/theopensystemslab/planx-core#d92224b + version: github.com/theopensystemslab/planx-core/d92224b '@types/isomorphic-fetch': specifier: ^0.0.36 version: 0.0.36 @@ -123,8 +123,8 @@ dependencies: specifier: ^5.0.0 version: 5.0.0(express@4.18.2) zod: - specifier: ^3.22.2 - version: 3.22.2 + specifier: ^3.22.3 + version: 3.22.3 devDependencies: '@babel/core': @@ -1624,8 +1624,8 @@ packages: resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} dev: false - /@mui/base@5.0.0-beta.16(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-OYxhC81c9bO0wobGcM8rrY5bRwpCXAI21BL0P2wz/2vTv4ek7ALz9+U5M8wgdmtRNUhmCmAB4L2WRwFRf5Cd8Q==} + /@mui/base@5.0.0-beta.17(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-xNbk7iOXrglNdIxFBN0k3ySsPIFLWCnFxqsAYl7CIcDkD9low4kJ7IUuy6ctwx/HAy2fenrT3KXHr1sGjAMgpQ==} engines: {node: '>=12.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 @@ -1642,7 +1642,7 @@ packages: '@babel/runtime': 7.22.15 '@floating-ui/react-dom': 2.0.2(react-dom@18.2.0)(react@18.2.0) '@mui/types': 7.2.4 - '@mui/utils': 5.14.10(react@18.2.0) + '@mui/utils': 5.14.11(react@18.2.0) '@popperjs/core': 2.11.8 clsx: 2.0.0 prop-types: 15.8.1 @@ -1650,12 +1650,12 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /@mui/core-downloads-tracker@5.14.10: - resolution: {integrity: sha512-kPHu/NhZq1k+vSZR5wq3AyUfD4bnfWAeuKpps0+8PS7ZHQ2Lyv1cXJh+PlFdCIOa0PK98rk3JPwMzS8BMhdHwQ==} + /@mui/core-downloads-tracker@5.14.11: + resolution: {integrity: sha512-uY8FLQURhXe3f3O4dS5OSGML9KDm9+IE226cBu78jarVIzdQGPlXwGIlSI9VJR8MvZDA6C0+6XfWDhWCHruC5Q==} dev: false - /@mui/material@5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-ejFMppnO+lzBXpzju+N4SSz0Mhmi5sihXUGcr5FxpgB6bfUP0Lpe32O0Sw/3s8xlmLEvG1fqVT0rRyAVMlCA+A==} + /@mui/material@5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-DnSdJzcR7lwG12JA5L2t8JF+RDzMygu5rCNW+logWb/KW2/TRzwLyVWO+CorHTBjBRd38DBxnwOCDiYkDd+N3A==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -1678,11 +1678,11 @@ packages: '@babel/runtime': 7.22.15 '@emotion/react': 11.11.1(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(react@18.2.0) - '@mui/base': 5.0.0-beta.16(react-dom@18.2.0)(react@18.2.0) - '@mui/core-downloads-tracker': 5.14.10 - '@mui/system': 5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) + '@mui/base': 5.0.0-beta.17(react-dom@18.2.0)(react@18.2.0) + '@mui/core-downloads-tracker': 5.14.11 + '@mui/system': 5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) '@mui/types': 7.2.4 - '@mui/utils': 5.14.10(react@18.2.0) + '@mui/utils': 5.14.11(react@18.2.0) '@types/react-transition-group': 4.4.6 clsx: 2.0.0 csstype: 3.1.2 @@ -1693,8 +1693,8 @@ packages: react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) dev: false - /@mui/private-theming@5.14.10(react@18.2.0): - resolution: {integrity: sha512-f67xOj3H06wWDT9xBg7hVL/HSKNF+HG1Kx0Pm23skkbEqD2Ef2Lif64e5nPdmWVv+7cISCYtSuE2aeuzrZe78w==} + /@mui/private-theming@5.14.11(react@18.2.0): + resolution: {integrity: sha512-MSnNNzTu9pfKLCKs1ZAKwOTgE4bz+fQA0fNr8Jm7NDmuWmw0CaN9Vq2/MHsatE7+S0A25IAKby46Uv1u53rKVQ==} engines: {node: '>=12.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 @@ -1706,13 +1706,13 @@ packages: optional: true dependencies: '@babel/runtime': 7.22.15 - '@mui/utils': 5.14.10(react@18.2.0) + '@mui/utils': 5.14.11(react@18.2.0) prop-types: 15.8.1 react: 18.2.0 dev: false - /@mui/styled-engine@5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0): - resolution: {integrity: sha512-EJckxmQHrsBvDbFu1trJkvjNw/1R7jfNarnqPSnL+jEQawCkQIqVELWLrlOa611TFtxSJGkdUfCFXeJC203HVg==} + /@mui/styled-engine@5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0): + resolution: {integrity: sha512-jdUlqRgTYQ8RMtPX4MbRZqar6W2OiIb6J5KEFbIu4FqvPrk44Each4ppg/LAqp1qNlBYq5i+7Q10MYLMpDxX9A==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.4.1 @@ -1735,8 +1735,8 @@ packages: react: 18.2.0 dev: false - /@mui/system@5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0): - resolution: {integrity: sha512-QQmtTG/R4gjmLiL5ECQ7kRxLKDm8aKKD7seGZfbINtRVJDyFhKChA1a+K2bfqIAaBo1EMDv+6FWNT1Q5cRKjFA==} + /@mui/system@5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0): + resolution: {integrity: sha512-yl8xV+y0k7j6dzBsHabKwoShmjqLa8kTxrhUI3JpqLG358VRVMJRW/ES0HhvfcCi4IVXde+Tc2P3K1akGL8zoA==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -1756,10 +1756,10 @@ packages: '@babel/runtime': 7.22.15 '@emotion/react': 11.11.1(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(react@18.2.0) - '@mui/private-theming': 5.14.10(react@18.2.0) - '@mui/styled-engine': 5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) + '@mui/private-theming': 5.14.11(react@18.2.0) + '@mui/styled-engine': 5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) '@mui/types': 7.2.4 - '@mui/utils': 5.14.10(react@18.2.0) + '@mui/utils': 5.14.11(react@18.2.0) clsx: 2.0.0 csstype: 3.1.2 prop-types: 15.8.1 @@ -1775,8 +1775,8 @@ packages: optional: true dev: false - /@mui/utils@5.14.10(react@18.2.0): - resolution: {integrity: sha512-Rn+vYQX7FxkcW0riDX/clNUwKuOJFH45HiULxwmpgnzQoQr3A0lb+QYwaZ+FAkZrR7qLoHKmLQlcItu6LT0y/Q==} + /@mui/utils@5.14.11(react@18.2.0): + resolution: {integrity: sha512-fmkIiCPKyDssYrJ5qk+dime1nlO3dmWfCtaPY/uVBqCRMBZ11JhddB9m8sjI2mgqQQwRJG5bq3biaosNdU/s4Q==} engines: {node: '>=12.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 @@ -3475,8 +3475,8 @@ packages: dependencies: esutils: 2.0.3 - /docx@8.2.2: - resolution: {integrity: sha512-dWI5WfD/fDCLdjIA7CcDzV/1uyBD+mmr7jDLTUN997hcbPz56E701Kf1EEWdibdH9kk+0tFSmE+C0jTlkRZ7kQ==} + /docx@8.2.3: + resolution: {integrity: sha512-Rlr/wPTDh+xQpFew3m4zYQ5OWEZO36HItyPCUbGdicB+ar4zIgseeJdqfcZIY0uQtMSrhGRpSFOTjii7h9cpHw==} engines: {node: '>=10'} dependencies: '@types/node': 20.4.1 @@ -7741,8 +7741,8 @@ packages: engines: {node: '>=10'} dev: true - /type-fest@4.3.1: - resolution: {integrity: sha512-pphNW/msgOUSkJbH58x8sqpq8uQj6b0ZKGxEsLKMUnGorRcDjrUaLS+39+/ub41JNTwrrMyJcUB8+YZs3mbwqw==} + /type-fest@4.3.3: + resolution: {integrity: sha512-bxhiFii6BBv6UiSDq7uKTMyADT9unXEl3ydGefndVLxFeB44LRbT4K7OJGDYSyDrKnklCC1Pre68qT2wbUl2Aw==} engines: {node: '>=16'} dev: false @@ -8099,12 +8099,12 @@ packages: commander: 9.5.0 dev: false - /zod@3.22.2: - resolution: {integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==} + /zod@3.22.3: + resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} dev: false - github.com/theopensystemslab/planx-core/3d395fa: - resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/3d395fa} + github.com/theopensystemslab/planx-core/d92224b: + resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/d92224b} name: '@opensystemslab/planx-core' version: 1.0.0 prepare: true @@ -8112,12 +8112,12 @@ packages: dependencies: '@emotion/react': 11.11.1(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(react@18.2.0) - '@mui/material': 5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0) '@types/geojson': 7946.0.11 ajv: 8.12.0 ajv-formats: 2.1.1(ajv@8.12.0) copyfiles: 2.4.1 - docx: 8.2.2 + docx: 8.2.3 eslint: 8.50.0 fast-xml-parser: 4.3.1 graphql: 16.8.1 @@ -8138,9 +8138,9 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) striptags: 3.2.0 - type-fest: 4.3.1 + type-fest: 4.3.3 uuid: 9.0.1 - zod: 3.22.2 + zod: 3.22.3 transitivePeerDependencies: - '@types/react' - encoding diff --git a/e2e/tests/api-driven/package.json b/e2e/tests/api-driven/package.json index 0f7618398f..5d607b6297 100644 --- a/e2e/tests/api-driven/package.json +++ b/e2e/tests/api-driven/package.json @@ -6,7 +6,7 @@ }, "dependencies": { "@cucumber/cucumber": "^9.3.0", - "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#44e9ebe", + "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#d92224b", "axios": "^1.4.0", "dotenv": "^16.3.1", "dotenv-expand": "^10.0.0", diff --git a/e2e/tests/api-driven/pnpm-lock.yaml b/e2e/tests/api-driven/pnpm-lock.yaml index 34035b4a87..88e4c5bfa6 100644 --- a/e2e/tests/api-driven/pnpm-lock.yaml +++ b/e2e/tests/api-driven/pnpm-lock.yaml @@ -9,8 +9,8 @@ dependencies: specifier: ^9.3.0 version: 9.3.0 '@opensystemslab/planx-core': - specifier: git+https://github.com/theopensystemslab/planx-core#44e9ebe - version: github.com/theopensystemslab/planx-core/44e9ebe + specifier: git+https://github.com/theopensystemslab/planx-core#d92224b + version: github.com/theopensystemslab/planx-core/d92224b axios: specifier: ^1.4.0 version: 1.4.0 @@ -494,8 +494,8 @@ packages: resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} dev: false - /@mui/base@5.0.0-beta.16(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-OYxhC81c9bO0wobGcM8rrY5bRwpCXAI21BL0P2wz/2vTv4ek7ALz9+U5M8wgdmtRNUhmCmAB4L2WRwFRf5Cd8Q==} + /@mui/base@5.0.0-beta.17(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-xNbk7iOXrglNdIxFBN0k3ySsPIFLWCnFxqsAYl7CIcDkD9low4kJ7IUuy6ctwx/HAy2fenrT3KXHr1sGjAMgpQ==} engines: {node: '>=12.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 @@ -508,7 +508,7 @@ packages: '@babel/runtime': 7.22.15 '@floating-ui/react-dom': 2.0.2(react-dom@18.2.0)(react@18.2.0) '@mui/types': 7.2.4 - '@mui/utils': 5.14.10(react@18.2.0) + '@mui/utils': 5.14.11(react@18.2.0) '@popperjs/core': 2.11.8 clsx: 2.0.0 prop-types: 15.8.1 @@ -516,12 +516,12 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /@mui/core-downloads-tracker@5.14.10: - resolution: {integrity: sha512-kPHu/NhZq1k+vSZR5wq3AyUfD4bnfWAeuKpps0+8PS7ZHQ2Lyv1cXJh+PlFdCIOa0PK98rk3JPwMzS8BMhdHwQ==} + /@mui/core-downloads-tracker@5.14.11: + resolution: {integrity: sha512-uY8FLQURhXe3f3O4dS5OSGML9KDm9+IE226cBu78jarVIzdQGPlXwGIlSI9VJR8MvZDA6C0+6XfWDhWCHruC5Q==} dev: false - /@mui/material@5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-ejFMppnO+lzBXpzju+N4SSz0Mhmi5sihXUGcr5FxpgB6bfUP0Lpe32O0Sw/3s8xlmLEvG1fqVT0rRyAVMlCA+A==} + /@mui/material@5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-DnSdJzcR7lwG12JA5L2t8JF+RDzMygu5rCNW+logWb/KW2/TRzwLyVWO+CorHTBjBRd38DBxnwOCDiYkDd+N3A==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -540,11 +540,11 @@ packages: '@babel/runtime': 7.22.15 '@emotion/react': 11.11.1(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(react@18.2.0) - '@mui/base': 5.0.0-beta.16(react-dom@18.2.0)(react@18.2.0) - '@mui/core-downloads-tracker': 5.14.10 - '@mui/system': 5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) + '@mui/base': 5.0.0-beta.17(react-dom@18.2.0)(react@18.2.0) + '@mui/core-downloads-tracker': 5.14.11 + '@mui/system': 5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) '@mui/types': 7.2.4 - '@mui/utils': 5.14.10(react@18.2.0) + '@mui/utils': 5.14.11(react@18.2.0) '@types/react-transition-group': 4.4.6 clsx: 2.0.0 csstype: 3.1.2 @@ -555,8 +555,8 @@ packages: react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) dev: false - /@mui/private-theming@5.14.10(react@18.2.0): - resolution: {integrity: sha512-f67xOj3H06wWDT9xBg7hVL/HSKNF+HG1Kx0Pm23skkbEqD2Ef2Lif64e5nPdmWVv+7cISCYtSuE2aeuzrZe78w==} + /@mui/private-theming@5.14.11(react@18.2.0): + resolution: {integrity: sha512-MSnNNzTu9pfKLCKs1ZAKwOTgE4bz+fQA0fNr8Jm7NDmuWmw0CaN9Vq2/MHsatE7+S0A25IAKby46Uv1u53rKVQ==} engines: {node: '>=12.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 @@ -566,13 +566,13 @@ packages: optional: true dependencies: '@babel/runtime': 7.22.15 - '@mui/utils': 5.14.10(react@18.2.0) + '@mui/utils': 5.14.11(react@18.2.0) prop-types: 15.8.1 react: 18.2.0 dev: false - /@mui/styled-engine@5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0): - resolution: {integrity: sha512-EJckxmQHrsBvDbFu1trJkvjNw/1R7jfNarnqPSnL+jEQawCkQIqVELWLrlOa611TFtxSJGkdUfCFXeJC203HVg==} + /@mui/styled-engine@5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0): + resolution: {integrity: sha512-jdUlqRgTYQ8RMtPX4MbRZqar6W2OiIb6J5KEFbIu4FqvPrk44Each4ppg/LAqp1qNlBYq5i+7Q10MYLMpDxX9A==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.4.1 @@ -593,8 +593,8 @@ packages: react: 18.2.0 dev: false - /@mui/system@5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0): - resolution: {integrity: sha512-QQmtTG/R4gjmLiL5ECQ7kRxLKDm8aKKD7seGZfbINtRVJDyFhKChA1a+K2bfqIAaBo1EMDv+6FWNT1Q5cRKjFA==} + /@mui/system@5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0): + resolution: {integrity: sha512-yl8xV+y0k7j6dzBsHabKwoShmjqLa8kTxrhUI3JpqLG358VRVMJRW/ES0HhvfcCi4IVXde+Tc2P3K1akGL8zoA==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -612,10 +612,10 @@ packages: '@babel/runtime': 7.22.15 '@emotion/react': 11.11.1(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(react@18.2.0) - '@mui/private-theming': 5.14.10(react@18.2.0) - '@mui/styled-engine': 5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) + '@mui/private-theming': 5.14.11(react@18.2.0) + '@mui/styled-engine': 5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) '@mui/types': 7.2.4 - '@mui/utils': 5.14.10(react@18.2.0) + '@mui/utils': 5.14.11(react@18.2.0) clsx: 2.0.0 csstype: 3.1.2 prop-types: 15.8.1 @@ -631,8 +631,8 @@ packages: optional: true dev: false - /@mui/utils@5.14.10(react@18.2.0): - resolution: {integrity: sha512-Rn+vYQX7FxkcW0riDX/clNUwKuOJFH45HiULxwmpgnzQoQr3A0lb+QYwaZ+FAkZrR7qLoHKmLQlcItu6LT0y/Q==} + /@mui/utils@5.14.11(react@18.2.0): + resolution: {integrity: sha512-fmkIiCPKyDssYrJ5qk+dime1nlO3dmWfCtaPY/uVBqCRMBZ11JhddB9m8sjI2mgqQQwRJG5bq3biaosNdU/s4Q==} engines: {node: '>=12.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 @@ -1124,8 +1124,8 @@ packages: esutils: 2.0.3 dev: false - /docx@8.2.2: - resolution: {integrity: sha512-dWI5WfD/fDCLdjIA7CcDzV/1uyBD+mmr7jDLTUN997hcbPz56E701Kf1EEWdibdH9kk+0tFSmE+C0jTlkRZ7kQ==} + /docx@8.2.3: + resolution: {integrity: sha512-Rlr/wPTDh+xQpFew3m4zYQ5OWEZO36HItyPCUbGdicB+ar4zIgseeJdqfcZIY0uQtMSrhGRpSFOTjii7h9cpHw==} engines: {node: '>=10'} dependencies: '@types/node': 20.4.5 @@ -2531,8 +2531,8 @@ packages: engines: {node: '>=10'} dev: false - /type-fest@4.3.1: - resolution: {integrity: sha512-pphNW/msgOUSkJbH58x8sqpq8uQj6b0ZKGxEsLKMUnGorRcDjrUaLS+39+/ub41JNTwrrMyJcUB8+YZs3mbwqw==} + /type-fest@4.3.3: + resolution: {integrity: sha512-bxhiFii6BBv6UiSDq7uKTMyADT9unXEl3ydGefndVLxFeB44LRbT4K7OJGDYSyDrKnklCC1Pre68qT2wbUl2Aw==} engines: {node: '>=16'} dev: false @@ -2711,12 +2711,12 @@ packages: toposort: 2.0.2 dev: false - /zod@3.22.2: - resolution: {integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==} + /zod@3.22.3: + resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} dev: false - github.com/theopensystemslab/planx-core/44e9ebe: - resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/44e9ebe} + github.com/theopensystemslab/planx-core/d92224b: + resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/d92224b} name: '@opensystemslab/planx-core' version: 1.0.0 prepare: true @@ -2724,12 +2724,12 @@ packages: dependencies: '@emotion/react': 11.11.1(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(react@18.2.0) - '@mui/material': 5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0) '@types/geojson': 7946.0.11 ajv: 8.12.0 ajv-formats: 2.1.1(ajv@8.12.0) copyfiles: 2.4.1 - docx: 8.2.2 + docx: 8.2.3 eslint: 8.50.0 fast-xml-parser: 4.3.1 graphql: 16.8.1 @@ -2750,9 +2750,9 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) striptags: 3.2.0 - type-fest: 4.3.1 + type-fest: 4.3.3 uuid: 9.0.1 - zod: 3.22.2 + zod: 3.22.3 transitivePeerDependencies: - '@types/react' - encoding diff --git a/e2e/tests/ui-driven/package.json b/e2e/tests/ui-driven/package.json index c834ab6182..65fcf76221 100644 --- a/e2e/tests/ui-driven/package.json +++ b/e2e/tests/ui-driven/package.json @@ -8,7 +8,7 @@ "postinstall": "./install-dependencies.sh" }, "dependencies": { - "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#44e9ebe", + "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#d92224b", "axios": "^1.4.0", "dotenv": "^16.3.1", "eslint": "^8.44.0", diff --git a/e2e/tests/ui-driven/pnpm-lock.yaml b/e2e/tests/ui-driven/pnpm-lock.yaml index 6dd5769323..cf65f35302 100644 --- a/e2e/tests/ui-driven/pnpm-lock.yaml +++ b/e2e/tests/ui-driven/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: '@opensystemslab/planx-core': - specifier: git+https://github.com/theopensystemslab/planx-core#44e9ebe - version: github.com/theopensystemslab/planx-core/44e9ebe + specifier: git+https://github.com/theopensystemslab/planx-core#d92224b + version: github.com/theopensystemslab/planx-core/d92224b axios: specifier: ^1.4.0 version: 1.4.0 @@ -343,8 +343,8 @@ packages: resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} dev: false - /@mui/base@5.0.0-beta.16(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-OYxhC81c9bO0wobGcM8rrY5bRwpCXAI21BL0P2wz/2vTv4ek7ALz9+U5M8wgdmtRNUhmCmAB4L2WRwFRf5Cd8Q==} + /@mui/base@5.0.0-beta.17(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-xNbk7iOXrglNdIxFBN0k3ySsPIFLWCnFxqsAYl7CIcDkD9low4kJ7IUuy6ctwx/HAy2fenrT3KXHr1sGjAMgpQ==} engines: {node: '>=12.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 @@ -357,7 +357,7 @@ packages: '@babel/runtime': 7.22.15 '@floating-ui/react-dom': 2.0.2(react-dom@18.2.0)(react@18.2.0) '@mui/types': 7.2.4 - '@mui/utils': 5.14.10(react@18.2.0) + '@mui/utils': 5.14.11(react@18.2.0) '@popperjs/core': 2.11.8 clsx: 2.0.0 prop-types: 15.8.1 @@ -365,12 +365,12 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /@mui/core-downloads-tracker@5.14.10: - resolution: {integrity: sha512-kPHu/NhZq1k+vSZR5wq3AyUfD4bnfWAeuKpps0+8PS7ZHQ2Lyv1cXJh+PlFdCIOa0PK98rk3JPwMzS8BMhdHwQ==} + /@mui/core-downloads-tracker@5.14.11: + resolution: {integrity: sha512-uY8FLQURhXe3f3O4dS5OSGML9KDm9+IE226cBu78jarVIzdQGPlXwGIlSI9VJR8MvZDA6C0+6XfWDhWCHruC5Q==} dev: false - /@mui/material@5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-ejFMppnO+lzBXpzju+N4SSz0Mhmi5sihXUGcr5FxpgB6bfUP0Lpe32O0Sw/3s8xlmLEvG1fqVT0rRyAVMlCA+A==} + /@mui/material@5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-DnSdJzcR7lwG12JA5L2t8JF+RDzMygu5rCNW+logWb/KW2/TRzwLyVWO+CorHTBjBRd38DBxnwOCDiYkDd+N3A==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -389,11 +389,11 @@ packages: '@babel/runtime': 7.22.15 '@emotion/react': 11.11.1(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(react@18.2.0) - '@mui/base': 5.0.0-beta.16(react-dom@18.2.0)(react@18.2.0) - '@mui/core-downloads-tracker': 5.14.10 - '@mui/system': 5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) + '@mui/base': 5.0.0-beta.17(react-dom@18.2.0)(react@18.2.0) + '@mui/core-downloads-tracker': 5.14.11 + '@mui/system': 5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) '@mui/types': 7.2.4 - '@mui/utils': 5.14.10(react@18.2.0) + '@mui/utils': 5.14.11(react@18.2.0) '@types/react-transition-group': 4.4.6 clsx: 2.0.0 csstype: 3.1.2 @@ -404,8 +404,8 @@ packages: react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) dev: false - /@mui/private-theming@5.14.10(react@18.2.0): - resolution: {integrity: sha512-f67xOj3H06wWDT9xBg7hVL/HSKNF+HG1Kx0Pm23skkbEqD2Ef2Lif64e5nPdmWVv+7cISCYtSuE2aeuzrZe78w==} + /@mui/private-theming@5.14.11(react@18.2.0): + resolution: {integrity: sha512-MSnNNzTu9pfKLCKs1ZAKwOTgE4bz+fQA0fNr8Jm7NDmuWmw0CaN9Vq2/MHsatE7+S0A25IAKby46Uv1u53rKVQ==} engines: {node: '>=12.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 @@ -415,13 +415,13 @@ packages: optional: true dependencies: '@babel/runtime': 7.22.15 - '@mui/utils': 5.14.10(react@18.2.0) + '@mui/utils': 5.14.11(react@18.2.0) prop-types: 15.8.1 react: 18.2.0 dev: false - /@mui/styled-engine@5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0): - resolution: {integrity: sha512-EJckxmQHrsBvDbFu1trJkvjNw/1R7jfNarnqPSnL+jEQawCkQIqVELWLrlOa611TFtxSJGkdUfCFXeJC203HVg==} + /@mui/styled-engine@5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0): + resolution: {integrity: sha512-jdUlqRgTYQ8RMtPX4MbRZqar6W2OiIb6J5KEFbIu4FqvPrk44Each4ppg/LAqp1qNlBYq5i+7Q10MYLMpDxX9A==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.4.1 @@ -442,8 +442,8 @@ packages: react: 18.2.0 dev: false - /@mui/system@5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0): - resolution: {integrity: sha512-QQmtTG/R4gjmLiL5ECQ7kRxLKDm8aKKD7seGZfbINtRVJDyFhKChA1a+K2bfqIAaBo1EMDv+6FWNT1Q5cRKjFA==} + /@mui/system@5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0): + resolution: {integrity: sha512-yl8xV+y0k7j6dzBsHabKwoShmjqLa8kTxrhUI3JpqLG358VRVMJRW/ES0HhvfcCi4IVXde+Tc2P3K1akGL8zoA==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -461,10 +461,10 @@ packages: '@babel/runtime': 7.22.15 '@emotion/react': 11.11.1(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(react@18.2.0) - '@mui/private-theming': 5.14.10(react@18.2.0) - '@mui/styled-engine': 5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) + '@mui/private-theming': 5.14.11(react@18.2.0) + '@mui/styled-engine': 5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) '@mui/types': 7.2.4 - '@mui/utils': 5.14.10(react@18.2.0) + '@mui/utils': 5.14.11(react@18.2.0) clsx: 2.0.0 csstype: 3.1.2 prop-types: 15.8.1 @@ -480,8 +480,8 @@ packages: optional: true dev: false - /@mui/utils@5.14.10(react@18.2.0): - resolution: {integrity: sha512-Rn+vYQX7FxkcW0riDX/clNUwKuOJFH45HiULxwmpgnzQoQr3A0lb+QYwaZ+FAkZrR7qLoHKmLQlcItu6LT0y/Q==} + /@mui/utils@5.14.11(react@18.2.0): + resolution: {integrity: sha512-fmkIiCPKyDssYrJ5qk+dime1nlO3dmWfCtaPY/uVBqCRMBZ11JhddB9m8sjI2mgqQQwRJG5bq3biaosNdU/s4Q==} engines: {node: '>=12.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 @@ -989,8 +989,8 @@ packages: dependencies: esutils: 2.0.3 - /docx@8.2.2: - resolution: {integrity: sha512-dWI5WfD/fDCLdjIA7CcDzV/1uyBD+mmr7jDLTUN997hcbPz56E701Kf1EEWdibdH9kk+0tFSmE+C0jTlkRZ7kQ==} + /docx@8.2.3: + resolution: {integrity: sha512-Rlr/wPTDh+xQpFew3m4zYQ5OWEZO36HItyPCUbGdicB+ar4zIgseeJdqfcZIY0uQtMSrhGRpSFOTjii7h9cpHw==} engines: {node: '>=10'} dependencies: '@types/node': 20.5.1 @@ -2336,8 +2336,8 @@ packages: engines: {node: '>=12.20'} dev: false - /type-fest@4.3.1: - resolution: {integrity: sha512-pphNW/msgOUSkJbH58x8sqpq8uQj6b0ZKGxEsLKMUnGorRcDjrUaLS+39+/ub41JNTwrrMyJcUB8+YZs3mbwqw==} + /type-fest@4.3.3: + resolution: {integrity: sha512-bxhiFii6BBv6UiSDq7uKTMyADT9unXEl3ydGefndVLxFeB44LRbT4K7OJGDYSyDrKnklCC1Pre68qT2wbUl2Aw==} engines: {node: '>=16'} dev: false @@ -2487,12 +2487,12 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - /zod@3.22.2: - resolution: {integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==} + /zod@3.22.3: + resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} dev: false - github.com/theopensystemslab/planx-core/44e9ebe: - resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/44e9ebe} + github.com/theopensystemslab/planx-core/d92224b: + resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/d92224b} name: '@opensystemslab/planx-core' version: 1.0.0 prepare: true @@ -2500,12 +2500,12 @@ packages: dependencies: '@emotion/react': 11.11.1(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(react@18.2.0) - '@mui/material': 5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0) '@types/geojson': 7946.0.11 ajv: 8.12.0 ajv-formats: 2.1.1(ajv@8.12.0) copyfiles: 2.4.1 - docx: 8.2.2 + docx: 8.2.3 eslint: 8.50.0 fast-xml-parser: 4.3.1 graphql: 16.8.1 @@ -2526,9 +2526,9 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) striptags: 3.2.0 - type-fest: 4.3.1 + type-fest: 4.3.3 uuid: 9.0.1 - zod: 3.22.2 + zod: 3.22.3 transitivePeerDependencies: - '@types/react' - encoding diff --git a/editor.planx.uk/package.json b/editor.planx.uk/package.json index 1fc3c696d6..b59747f3e4 100644 --- a/editor.planx.uk/package.json +++ b/editor.planx.uk/package.json @@ -14,7 +14,7 @@ "@mui/styles": "^5.14.5", "@mui/utils": "^5.14.5", "@opensystemslab/map": "^0.7.5", - "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#44420b9", + "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#d92224b", "@tiptap/core": "^2.0.3", "@tiptap/extension-bold": "^2.0.3", "@tiptap/extension-bubble-menu": "^2.1.6", @@ -89,7 +89,7 @@ "uuid": "^9.0.0", "wkt": "^0.1.1", "yup": "^0.32.11", - "zod": "^3.22.2", + "zod": "^3.22.3", "zustand": "^4.3.8" }, "devDependencies": { diff --git a/editor.planx.uk/pnpm-lock.yaml b/editor.planx.uk/pnpm-lock.yaml index cb8d6007b1..a89798ab6f 100644 --- a/editor.planx.uk/pnpm-lock.yaml +++ b/editor.planx.uk/pnpm-lock.yaml @@ -46,8 +46,8 @@ dependencies: specifier: ^0.7.5 version: 0.7.5 '@opensystemslab/planx-core': - specifier: git+https://github.com/theopensystemslab/planx-core#44420b9 - version: github.com/theopensystemslab/planx-core/44420b9(@types/react@18.2.20) + specifier: git+https://github.com/theopensystemslab/planx-core#d92224b + version: github.com/theopensystemslab/planx-core/d92224b(@types/react@18.2.20) '@tiptap/core': specifier: ^2.0.3 version: 2.0.3(@tiptap/pm@2.0.3) @@ -271,8 +271,8 @@ dependencies: specifier: ^0.32.11 version: 0.32.11 zod: - specifier: ^3.22.2 - version: 3.22.2 + specifier: ^3.22.3 + version: 3.22.3 zustand: specifier: ^4.3.8 version: 4.3.9(immer@9.0.21)(react@18.2.0) @@ -5225,8 +5225,8 @@ packages: react-is: 18.2.0 dev: false - /@mui/base@5.0.0-beta.16(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-OYxhC81c9bO0wobGcM8rrY5bRwpCXAI21BL0P2wz/2vTv4ek7ALz9+U5M8wgdmtRNUhmCmAB4L2WRwFRf5Cd8Q==} + /@mui/base@5.0.0-beta.17(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-xNbk7iOXrglNdIxFBN0k3ySsPIFLWCnFxqsAYl7CIcDkD9low4kJ7IUuy6ctwx/HAy2fenrT3KXHr1sGjAMgpQ==} engines: {node: '>=12.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 @@ -5239,7 +5239,7 @@ packages: '@babel/runtime': 7.22.15 '@floating-ui/react-dom': 2.0.2(react-dom@18.2.0)(react@18.2.0) '@mui/types': 7.2.4(@types/react@18.2.20) - '@mui/utils': 5.14.10(@types/react@18.2.20)(react@18.2.0) + '@mui/utils': 5.14.11(@types/react@18.2.20)(react@18.2.0) '@popperjs/core': 2.11.8 '@types/react': 18.2.20 clsx: 2.0.0 @@ -5248,8 +5248,8 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /@mui/core-downloads-tracker@5.14.10: - resolution: {integrity: sha512-kPHu/NhZq1k+vSZR5wq3AyUfD4bnfWAeuKpps0+8PS7ZHQ2Lyv1cXJh+PlFdCIOa0PK98rk3JPwMzS8BMhdHwQ==} + /@mui/core-downloads-tracker@5.14.11: + resolution: {integrity: sha512-uY8FLQURhXe3f3O4dS5OSGML9KDm9+IE226cBu78jarVIzdQGPlXwGIlSI9VJR8MvZDA6C0+6XfWDhWCHruC5Q==} dev: false /@mui/core-downloads-tracker@5.14.5: @@ -5273,8 +5273,8 @@ packages: react: 18.2.0 dev: false - /@mui/material@5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-ejFMppnO+lzBXpzju+N4SSz0Mhmi5sihXUGcr5FxpgB6bfUP0Lpe32O0Sw/3s8xlmLEvG1fqVT0rRyAVMlCA+A==} + /@mui/material@5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-DnSdJzcR7lwG12JA5L2t8JF+RDzMygu5rCNW+logWb/KW2/TRzwLyVWO+CorHTBjBRd38DBxnwOCDiYkDd+N3A==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -5293,11 +5293,11 @@ packages: '@babel/runtime': 7.22.15 '@emotion/react': 11.11.1(@types/react@18.2.20)(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.20)(react@18.2.0) - '@mui/base': 5.0.0-beta.16(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) - '@mui/core-downloads-tracker': 5.14.10 - '@mui/system': 5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.20)(react@18.2.0) + '@mui/base': 5.0.0-beta.17(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) + '@mui/core-downloads-tracker': 5.14.11 + '@mui/system': 5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.20)(react@18.2.0) '@mui/types': 7.2.4(@types/react@18.2.20) - '@mui/utils': 5.14.10(@types/react@18.2.20)(react@18.2.0) + '@mui/utils': 5.14.11(@types/react@18.2.20)(react@18.2.0) '@types/react': 18.2.20 '@types/react-transition-group': 4.4.6 clsx: 2.0.0 @@ -5362,6 +5362,23 @@ packages: react: 18.2.0 dev: false + /@mui/private-theming@5.14.11(@types/react@18.2.20)(react@18.2.0): + resolution: {integrity: sha512-MSnNNzTu9pfKLCKs1ZAKwOTgE4bz+fQA0fNr8Jm7NDmuWmw0CaN9Vq2/MHsatE7+S0A25IAKby46Uv1u53rKVQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.22.15 + '@mui/utils': 5.14.11(@types/react@18.2.20)(react@18.2.0) + '@types/react': 18.2.20 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + /@mui/private-theming@5.14.5(@types/react@18.2.20)(react@18.2.0): resolution: {integrity: sha512-cC4C5RrpXpDaaZyH9QwmPhRLgz+f2SYbOty3cPkk4qPSOSfif2ZEcDD9HTENKDDd9deB+xkPKzzZhi8cxIx8Ig==} engines: {node: '>=12.0.0'} @@ -5401,6 +5418,28 @@ packages: react: 18.2.0 dev: false + /@mui/styled-engine@5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0): + resolution: {integrity: sha512-jdUlqRgTYQ8RMtPX4MbRZqar6W2OiIb6J5KEFbIu4FqvPrk44Each4ppg/LAqp1qNlBYq5i+7Q10MYLMpDxX9A==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + dependencies: + '@babel/runtime': 7.22.15 + '@emotion/cache': 11.11.0 + '@emotion/react': 11.11.1(@types/react@18.2.20)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.20)(react@18.2.0) + csstype: 3.1.2 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + /@mui/styles@5.14.5(@types/react@18.2.20)(react@18.2.0): resolution: {integrity: sha512-yss4BRGae6ib4gq6YpVLnPyhHiuSIENwlDPWrTEqgc1UhTMmDBGZ7ZCdZ15YGdwviJuNDDf5Bcp3GK4rE5wZNQ==} engines: {node: '>=12.0.0'} @@ -5432,8 +5471,8 @@ packages: react: 18.2.0 dev: false - /@mui/system@5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.20)(react@18.2.0): - resolution: {integrity: sha512-QQmtTG/R4gjmLiL5ECQ7kRxLKDm8aKKD7seGZfbINtRVJDyFhKChA1a+K2bfqIAaBo1EMDv+6FWNT1Q5cRKjFA==} + /@mui/system@5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.20)(react@18.2.0): + resolution: {integrity: sha512-yl8xV+y0k7j6dzBsHabKwoShmjqLa8kTxrhUI3JpqLG358VRVMJRW/ES0HhvfcCi4IVXde+Tc2P3K1akGL8zoA==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -5451,10 +5490,10 @@ packages: '@babel/runtime': 7.22.15 '@emotion/react': 11.11.1(@types/react@18.2.20)(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.20)(react@18.2.0) - '@mui/private-theming': 5.14.10(@types/react@18.2.20)(react@18.2.0) - '@mui/styled-engine': 5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) + '@mui/private-theming': 5.14.11(@types/react@18.2.20)(react@18.2.0) + '@mui/styled-engine': 5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) '@mui/types': 7.2.4(@types/react@18.2.20) - '@mui/utils': 5.14.10(@types/react@18.2.20)(react@18.2.0) + '@mui/utils': 5.14.11(@types/react@18.2.20)(react@18.2.0) '@types/react': 18.2.20 clsx: 2.0.0 csstype: 3.1.2 @@ -5521,6 +5560,24 @@ packages: react-is: 18.2.0 dev: false + /@mui/utils@5.14.11(@types/react@18.2.20)(react@18.2.0): + resolution: {integrity: sha512-fmkIiCPKyDssYrJ5qk+dime1nlO3dmWfCtaPY/uVBqCRMBZ11JhddB9m8sjI2mgqQQwRJG5bq3biaosNdU/s4Q==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.22.15 + '@types/prop-types': 15.7.5 + '@types/react': 18.2.20 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 18.2.0 + dev: false + /@mui/utils@5.14.5(react@18.2.0): resolution: {integrity: sha512-6Hzw63VR9C5xYv+CbjndoRLU6Gntal8rJ5W+GUzkyHrGWIyYPWZPa6AevnyGioySNETATe1H9oXS8f/7qgIHJA==} engines: {node: '>=12.0.0'} @@ -10765,8 +10822,8 @@ packages: dependencies: esutils: 2.0.3 - /docx@8.2.2: - resolution: {integrity: sha512-dWI5WfD/fDCLdjIA7CcDzV/1uyBD+mmr7jDLTUN997hcbPz56E701Kf1EEWdibdH9kk+0tFSmE+C0jTlkRZ7kQ==} + /docx@8.2.3: + resolution: {integrity: sha512-Rlr/wPTDh+xQpFew3m4zYQ5OWEZO36HItyPCUbGdicB+ar4zIgseeJdqfcZIY0uQtMSrhGRpSFOTjii7h9cpHw==} engines: {node: '>=10'} dependencies: '@types/node': 20.4.2 @@ -19793,6 +19850,11 @@ packages: engines: {node: '>=16'} dev: false + /type-fest@4.3.3: + resolution: {integrity: sha512-bxhiFii6BBv6UiSDq7uKTMyADT9unXEl3ydGefndVLxFeB44LRbT4K7OJGDYSyDrKnklCC1Pre68qT2wbUl2Aw==} + engines: {node: '>=16'} + dev: false + /type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -20843,8 +20905,8 @@ packages: resolution: {integrity: sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==} dev: false - /zod@3.22.2: - resolution: {integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==} + /zod@3.22.3: + resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} dev: false /zustand@4.3.9(immer@9.0.21)(react@18.2.0): @@ -20864,9 +20926,9 @@ packages: use-sync-external-store: 1.2.0(react@18.2.0) dev: false - github.com/theopensystemslab/planx-core/44420b9(@types/react@18.2.20): - resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/44420b9} - id: github.com/theopensystemslab/planx-core/44420b9 + github.com/theopensystemslab/planx-core/d92224b(@types/react@18.2.20): + resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/d92224b} + id: github.com/theopensystemslab/planx-core/d92224b name: '@opensystemslab/planx-core' version: 1.0.0 prepare: true @@ -20874,12 +20936,12 @@ packages: dependencies: '@emotion/react': 11.11.1(@types/react@18.2.20)(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.20)(react@18.2.0) - '@mui/material': 5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.14.11(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) '@types/geojson': 7946.0.11 ajv: 8.12.0 ajv-formats: 2.1.1(ajv@8.12.0) copyfiles: 2.4.1 - docx: 8.2.2 + docx: 8.2.3 eslint: 8.50.0 fast-xml-parser: 4.3.1 graphql: 16.8.1 @@ -20900,9 +20962,9 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) striptags: 3.2.0 - type-fest: 4.3.1 + type-fest: 4.3.3 uuid: 9.0.1 - zod: 3.22.2 + zod: 3.22.3 transitivePeerDependencies: - '@types/react' - encoding From 65f2218dc15d84ae5ffae41b85d55ed26f6f2d80 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 11:36:25 +0100 Subject: [PATCH 6/8] chore(deps-dev): bump postcss from 8.4.25 to 8.4.31 in /editor.planx.uk (#2281) Bumps [postcss](https://github.com/postcss/postcss) from 8.4.25 to 8.4.31. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.25...8.4.31) --- updated-dependencies: - dependency-name: postcss dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- editor.planx.uk/package.json | 2 +- editor.planx.uk/pnpm-lock.yaml | 606 ++++++++++++++++----------------- 2 files changed, 304 insertions(+), 304 deletions(-) diff --git a/editor.planx.uk/package.json b/editor.planx.uk/package.json index b59747f3e4..7424e59526 100644 --- a/editor.planx.uk/package.json +++ b/editor.planx.uk/package.json @@ -149,7 +149,7 @@ "jest-axe": "^6.0.1", "jest-localstorage-mock": "^2.4.26", "lint-staged": "^13.2.3", - "postcss": "^8.4.24", + "postcss": "^8.4.31", "prettier": "^3.0.0", "react-app-rewired": "^2.2.1", "react-refresh": "^0.14.0", diff --git a/editor.planx.uk/pnpm-lock.yaml b/editor.planx.uk/pnpm-lock.yaml index a89798ab6f..d9cb4b60e1 100644 --- a/editor.planx.uk/pnpm-lock.yaml +++ b/editor.planx.uk/pnpm-lock.yaml @@ -47,7 +47,7 @@ dependencies: version: 0.7.5 '@opensystemslab/planx-core': specifier: git+https://github.com/theopensystemslab/planx-core#d92224b - version: github.com/theopensystemslab/planx-core/d92224b(@types/react@18.2.20) + version: git/github.com+theopensystemslab/planx-core/d92224b(@types/react@18.2.20) '@tiptap/core': specifier: ^2.0.3 version: 2.0.3(@tiptap/pm@2.0.3) @@ -400,7 +400,7 @@ devDependencies: version: 5.61.0(eslint@8.44.0)(typescript@4.9.5) autoprefixer: specifier: ^10.4.14 - version: 10.4.14(postcss@8.4.25) + version: 10.4.14(postcss@8.4.31) craco-esbuild: specifier: ^0.5.2 version: 0.5.2(@craco/craco@6.4.5)(esbuild@0.14.54)(react-scripts@5.0.1)(webpack@5.88.1) @@ -447,8 +447,8 @@ devDependencies: specifier: ^13.2.3 version: 13.2.3 postcss: - specifier: ^8.4.24 - version: 8.4.25 + specifier: ^8.4.31 + version: 8.4.31 prettier: specifier: ^3.0.0 version: 3.0.0 @@ -3666,135 +3666,135 @@ packages: /@csstools/normalize.css@12.0.0: resolution: {integrity: sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg==} - /@csstools/postcss-cascade-layers@1.1.1(postcss@8.4.25): + /@csstools/postcss-cascade-layers@1.1.1(postcss@8.4.31): resolution: {integrity: sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.0.13) - postcss: 8.4.25 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 - /@csstools/postcss-color-function@1.1.1(postcss@8.4.25): + /@csstools/postcss-color-function@1.1.1(postcss@8.4.31): resolution: {integrity: sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.25) - postcss: 8.4.25 + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.31) + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /@csstools/postcss-font-format-keywords@1.0.1(postcss@8.4.25): + /@csstools/postcss-font-format-keywords@1.0.1(postcss@8.4.31): resolution: {integrity: sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /@csstools/postcss-hwb-function@1.0.2(postcss@8.4.25): + /@csstools/postcss-hwb-function@1.0.2(postcss@8.4.31): resolution: {integrity: sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /@csstools/postcss-ic-unit@1.0.1(postcss@8.4.25): + /@csstools/postcss-ic-unit@1.0.1(postcss@8.4.31): resolution: {integrity: sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.25) - postcss: 8.4.25 + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.31) + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /@csstools/postcss-is-pseudo-class@2.0.7(postcss@8.4.25): + /@csstools/postcss-is-pseudo-class@2.0.7(postcss@8.4.31): resolution: {integrity: sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.0.13) - postcss: 8.4.25 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 - /@csstools/postcss-nested-calc@1.0.0(postcss@8.4.25): + /@csstools/postcss-nested-calc@1.0.0(postcss@8.4.31): resolution: {integrity: sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /@csstools/postcss-normalize-display-values@1.0.1(postcss@8.4.25): + /@csstools/postcss-normalize-display-values@1.0.1(postcss@8.4.31): resolution: {integrity: sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /@csstools/postcss-oklab-function@1.1.1(postcss@8.4.25): + /@csstools/postcss-oklab-function@1.1.1(postcss@8.4.31): resolution: {integrity: sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.25) - postcss: 8.4.25 + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.31) + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /@csstools/postcss-progressive-custom-properties@1.3.0(postcss@8.4.25): + /@csstools/postcss-progressive-custom-properties@1.3.0(postcss@8.4.31): resolution: {integrity: sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.3 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /@csstools/postcss-stepped-value-functions@1.0.1(postcss@8.4.25): + /@csstools/postcss-stepped-value-functions@1.0.1(postcss@8.4.31): resolution: {integrity: sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /@csstools/postcss-text-decoration-shorthand@1.0.0(postcss@8.4.25): + /@csstools/postcss-text-decoration-shorthand@1.0.0(postcss@8.4.31): resolution: {integrity: sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /@csstools/postcss-trigonometric-functions@1.0.2(postcss@8.4.25): + /@csstools/postcss-trigonometric-functions@1.0.2(postcss@8.4.31): resolution: {integrity: sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==} engines: {node: ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /@csstools/postcss-unset-value@1.0.2(postcss@8.4.25): + /@csstools/postcss-unset-value@1.0.2(postcss@8.4.31): resolution: {integrity: sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 /@csstools/selector-specificity@2.2.0(postcss-selector-parser@6.0.13): resolution: {integrity: sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==} @@ -5356,7 +5356,7 @@ packages: optional: true dependencies: '@babel/runtime': 7.22.15 - '@mui/utils': 5.14.10(@types/react@18.2.20)(react@18.2.0) + '@mui/utils': 5.14.11(@types/react@18.2.20)(react@18.2.0) '@types/react': 18.2.20 prop-types: 15.8.1 react: 18.2.0 @@ -5390,7 +5390,7 @@ packages: optional: true dependencies: '@babel/runtime': 7.22.15 - '@mui/utils': 5.14.10(@types/react@18.2.20)(react@18.2.0) + '@mui/utils': 5.14.11(@types/react@18.2.20)(react@18.2.0) '@types/react': 18.2.20 prop-types: 15.8.1 react: 18.2.0 @@ -8971,7 +8971,7 @@ packages: engines: {node: '>=4'} dev: false - /autoprefixer@10.4.14(postcss@8.4.25): + /autoprefixer@10.4.14(postcss@8.4.31): resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==} engines: {node: ^10 || ^12 || >=14} hasBin: true @@ -8983,7 +8983,7 @@ packages: fraction.js: 4.2.0 normalize-range: 0.1.2 picocolors: 1.0.0 - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 /available-typed-arrays@1.0.5: @@ -10265,14 +10265,14 @@ packages: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} - /css-blank-pseudo@3.0.3(postcss@8.4.25): + /css-blank-pseudo@3.0.3(postcss@8.4.31): resolution: {integrity: sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==} engines: {node: ^12 || ^14 || >=16} hasBin: true peerDependencies: postcss: ^8.4 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 /css-box-model@1.2.1: @@ -10285,22 +10285,22 @@ packages: resolution: {integrity: sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q==} dev: true - /css-declaration-sorter@6.4.0(postcss@8.4.25): + /css-declaration-sorter@6.4.0(postcss@8.4.31): resolution: {integrity: sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==} engines: {node: ^10 || ^12 || >=14} peerDependencies: postcss: ^8.0.9 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 - /css-has-pseudo@3.0.4(postcss@8.4.25): + /css-has-pseudo@3.0.4(postcss@8.4.31): resolution: {integrity: sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==} engines: {node: ^12 || ^14 || >=16} hasBin: true peerDependencies: postcss: ^8.4 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 /css-in-js-utils@3.1.0: @@ -10323,12 +10323,12 @@ packages: peerDependencies: webpack: ^5.0.0 dependencies: - icss-utils: 5.1.0(postcss@8.4.25) - postcss: 8.4.25 - postcss-modules-extract-imports: 3.0.0(postcss@8.4.25) - postcss-modules-local-by-default: 4.0.3(postcss@8.4.25) - postcss-modules-scope: 3.0.0(postcss@8.4.25) - postcss-modules-values: 4.0.0(postcss@8.4.25) + icss-utils: 5.1.0(postcss@8.4.31) + postcss: 8.4.31 + postcss-modules-extract-imports: 3.0.0(postcss@8.4.31) + postcss-modules-local-by-default: 4.0.3(postcss@8.4.31) + postcss-modules-scope: 3.0.0(postcss@8.4.31) + postcss-modules-values: 4.0.0(postcss@8.4.31) postcss-value-parser: 4.2.0 semver: 7.5.3 webpack: 5.88.1(@swc/core@1.3.71)(esbuild@0.14.54) @@ -10352,23 +10352,23 @@ packages: esbuild: optional: true dependencies: - cssnano: 5.1.15(postcss@8.4.25) + cssnano: 5.1.15(postcss@8.4.31) esbuild: 0.14.54 jest-worker: 27.5.1 - postcss: 8.4.25 + postcss: 8.4.31 schema-utils: 4.2.0 serialize-javascript: 6.0.1 source-map: 0.6.1 webpack: 5.88.1(@swc/core@1.3.71)(esbuild@0.14.54) - /css-prefers-color-scheme@6.0.3(postcss@8.4.25): + /css-prefers-color-scheme@6.0.3(postcss@8.4.31): resolution: {integrity: sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==} engines: {node: ^12 || ^14 || >=16} hasBin: true peerDependencies: postcss: ^8.4 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 /css-select-base-adapter@0.1.1: resolution: {integrity: sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==} @@ -10434,60 +10434,60 @@ packages: engines: {node: '>=4'} hasBin: true - /cssnano-preset-default@5.2.14(postcss@8.4.25): + /cssnano-preset-default@5.2.14(postcss@8.4.31): resolution: {integrity: sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - css-declaration-sorter: 6.4.0(postcss@8.4.25) - cssnano-utils: 3.1.0(postcss@8.4.25) - postcss: 8.4.25 - postcss-calc: 8.2.4(postcss@8.4.25) - postcss-colormin: 5.3.1(postcss@8.4.25) - postcss-convert-values: 5.1.3(postcss@8.4.25) - postcss-discard-comments: 5.1.2(postcss@8.4.25) - postcss-discard-duplicates: 5.1.0(postcss@8.4.25) - postcss-discard-empty: 5.1.1(postcss@8.4.25) - postcss-discard-overridden: 5.1.0(postcss@8.4.25) - postcss-merge-longhand: 5.1.7(postcss@8.4.25) - postcss-merge-rules: 5.1.4(postcss@8.4.25) - postcss-minify-font-values: 5.1.0(postcss@8.4.25) - postcss-minify-gradients: 5.1.1(postcss@8.4.25) - postcss-minify-params: 5.1.4(postcss@8.4.25) - postcss-minify-selectors: 5.2.1(postcss@8.4.25) - postcss-normalize-charset: 5.1.0(postcss@8.4.25) - postcss-normalize-display-values: 5.1.0(postcss@8.4.25) - postcss-normalize-positions: 5.1.1(postcss@8.4.25) - postcss-normalize-repeat-style: 5.1.1(postcss@8.4.25) - postcss-normalize-string: 5.1.0(postcss@8.4.25) - postcss-normalize-timing-functions: 5.1.0(postcss@8.4.25) - postcss-normalize-unicode: 5.1.1(postcss@8.4.25) - postcss-normalize-url: 5.1.0(postcss@8.4.25) - postcss-normalize-whitespace: 5.1.1(postcss@8.4.25) - postcss-ordered-values: 5.1.3(postcss@8.4.25) - postcss-reduce-initial: 5.1.2(postcss@8.4.25) - postcss-reduce-transforms: 5.1.0(postcss@8.4.25) - postcss-svgo: 5.1.0(postcss@8.4.25) - postcss-unique-selectors: 5.1.1(postcss@8.4.25) - - /cssnano-utils@3.1.0(postcss@8.4.25): + css-declaration-sorter: 6.4.0(postcss@8.4.31) + cssnano-utils: 3.1.0(postcss@8.4.31) + postcss: 8.4.31 + postcss-calc: 8.2.4(postcss@8.4.31) + postcss-colormin: 5.3.1(postcss@8.4.31) + postcss-convert-values: 5.1.3(postcss@8.4.31) + postcss-discard-comments: 5.1.2(postcss@8.4.31) + postcss-discard-duplicates: 5.1.0(postcss@8.4.31) + postcss-discard-empty: 5.1.1(postcss@8.4.31) + postcss-discard-overridden: 5.1.0(postcss@8.4.31) + postcss-merge-longhand: 5.1.7(postcss@8.4.31) + postcss-merge-rules: 5.1.4(postcss@8.4.31) + postcss-minify-font-values: 5.1.0(postcss@8.4.31) + postcss-minify-gradients: 5.1.1(postcss@8.4.31) + postcss-minify-params: 5.1.4(postcss@8.4.31) + postcss-minify-selectors: 5.2.1(postcss@8.4.31) + postcss-normalize-charset: 5.1.0(postcss@8.4.31) + postcss-normalize-display-values: 5.1.0(postcss@8.4.31) + postcss-normalize-positions: 5.1.1(postcss@8.4.31) + postcss-normalize-repeat-style: 5.1.1(postcss@8.4.31) + postcss-normalize-string: 5.1.0(postcss@8.4.31) + postcss-normalize-timing-functions: 5.1.0(postcss@8.4.31) + postcss-normalize-unicode: 5.1.1(postcss@8.4.31) + postcss-normalize-url: 5.1.0(postcss@8.4.31) + postcss-normalize-whitespace: 5.1.1(postcss@8.4.31) + postcss-ordered-values: 5.1.3(postcss@8.4.31) + postcss-reduce-initial: 5.1.2(postcss@8.4.31) + postcss-reduce-transforms: 5.1.0(postcss@8.4.31) + postcss-svgo: 5.1.0(postcss@8.4.31) + postcss-unique-selectors: 5.1.1(postcss@8.4.31) + + /cssnano-utils@3.1.0(postcss@8.4.31): resolution: {integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 - /cssnano@5.1.15(postcss@8.4.25): + /cssnano@5.1.15(postcss@8.4.31): resolution: {integrity: sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - cssnano-preset-default: 5.2.14(postcss@8.4.25) + cssnano-preset-default: 5.2.14(postcss@8.4.31) lilconfig: 2.1.0 - postcss: 8.4.25 + postcss: 8.4.31 yaml: 1.10.2 /csso@4.2.0: @@ -13147,13 +13147,13 @@ packages: dependencies: safer-buffer: 2.1.2 - /icss-utils@5.1.0(postcss@8.4.25): + /icss-utils@5.1.0(postcss@8.4.31): resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 /idb@7.1.1: resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} @@ -16238,16 +16238,16 @@ packages: engines: {node: '>= 10.0.0'} dev: false - /postcss-attribute-case-insensitive@5.0.2(postcss@8.4.25): + /postcss-attribute-case-insensitive@5.0.2(postcss@8.4.31): resolution: {integrity: sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 - /postcss-browser-comments@4.0.0(browserslist@4.21.9)(postcss@8.4.25): + /postcss-browser-comments@4.0.0(browserslist@4.21.9)(postcss@8.4.31): resolution: {integrity: sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==} engines: {node: '>=8'} peerDependencies: @@ -16255,54 +16255,54 @@ packages: postcss: '>=8' dependencies: browserslist: 4.21.9 - postcss: 8.4.25 + postcss: 8.4.31 - /postcss-calc@8.2.4(postcss@8.4.25): + /postcss-calc@8.2.4(postcss@8.4.31): resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} peerDependencies: postcss: ^8.2.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 postcss-value-parser: 4.2.0 - /postcss-clamp@4.1.0(postcss@8.4.25): + /postcss-clamp@4.1.0(postcss@8.4.31): resolution: {integrity: sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==} engines: {node: '>=7.6.0'} peerDependencies: postcss: ^8.4.6 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-color-functional-notation@4.2.4(postcss@8.4.25): + /postcss-color-functional-notation@4.2.4(postcss@8.4.31): resolution: {integrity: sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-color-hex-alpha@8.0.4(postcss@8.4.25): + /postcss-color-hex-alpha@8.0.4(postcss@8.4.31): resolution: {integrity: sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.4 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-color-rebeccapurple@7.1.1(postcss@8.4.25): + /postcss-color-rebeccapurple@7.1.1(postcss@8.4.31): resolution: {integrity: sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-colormin@5.3.1(postcss@8.4.25): + /postcss-colormin@5.3.1(postcss@8.4.31): resolution: {integrity: sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: @@ -16311,193 +16311,193 @@ packages: browserslist: 4.21.9 caniuse-api: 3.0.0 colord: 2.9.3 - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-convert-values@5.1.3(postcss@8.4.25): + /postcss-convert-values@5.1.3(postcss@8.4.31): resolution: {integrity: sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: browserslist: 4.21.9 - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-custom-media@8.0.2(postcss@8.4.25): + /postcss-custom-media@8.0.2(postcss@8.4.31): resolution: {integrity: sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.3 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-custom-properties@12.1.11(postcss@8.4.25): + /postcss-custom-properties@12.1.11(postcss@8.4.31): resolution: {integrity: sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-custom-selectors@6.0.3(postcss@8.4.25): + /postcss-custom-selectors@6.0.3(postcss@8.4.31): resolution: {integrity: sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.3 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 - /postcss-dir-pseudo-class@6.0.5(postcss@8.4.25): + /postcss-dir-pseudo-class@6.0.5(postcss@8.4.31): resolution: {integrity: sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 - /postcss-discard-comments@5.1.2(postcss@8.4.25): + /postcss-discard-comments@5.1.2(postcss@8.4.31): resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 - /postcss-discard-duplicates@5.1.0(postcss@8.4.25): + /postcss-discard-duplicates@5.1.0(postcss@8.4.31): resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 - /postcss-discard-empty@5.1.1(postcss@8.4.25): + /postcss-discard-empty@5.1.1(postcss@8.4.31): resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 - /postcss-discard-overridden@5.1.0(postcss@8.4.25): + /postcss-discard-overridden@5.1.0(postcss@8.4.31): resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 - /postcss-double-position-gradients@3.1.2(postcss@8.4.25): + /postcss-double-position-gradients@3.1.2(postcss@8.4.31): resolution: {integrity: sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.25) - postcss: 8.4.25 + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.31) + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-env-function@4.0.6(postcss@8.4.25): + /postcss-env-function@4.0.6(postcss@8.4.31): resolution: {integrity: sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.4 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-flexbugs-fixes@5.0.2(postcss@8.4.25): + /postcss-flexbugs-fixes@5.0.2(postcss@8.4.31): resolution: {integrity: sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==} peerDependencies: postcss: ^8.1.4 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 - /postcss-focus-visible@6.0.4(postcss@8.4.25): + /postcss-focus-visible@6.0.4(postcss@8.4.31): resolution: {integrity: sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.4 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 - /postcss-focus-within@5.0.4(postcss@8.4.25): + /postcss-focus-within@5.0.4(postcss@8.4.31): resolution: {integrity: sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.4 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 - /postcss-font-variant@5.0.0(postcss@8.4.25): + /postcss-font-variant@5.0.0(postcss@8.4.31): resolution: {integrity: sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==} peerDependencies: postcss: ^8.1.0 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 - /postcss-gap-properties@3.0.5(postcss@8.4.25): + /postcss-gap-properties@3.0.5(postcss@8.4.31): resolution: {integrity: sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 - /postcss-image-set-function@4.0.7(postcss@8.4.25): + /postcss-image-set-function@4.0.7(postcss@8.4.31): resolution: {integrity: sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-import@15.1.0(postcss@8.4.25): + /postcss-import@15.1.0(postcss@8.4.31): resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} peerDependencies: postcss: ^8.0.0 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.2 - /postcss-initial@4.0.1(postcss@8.4.25): + /postcss-initial@4.0.1(postcss@8.4.31): resolution: {integrity: sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==} peerDependencies: postcss: ^8.0.0 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 - /postcss-js@4.0.1(postcss@8.4.25): + /postcss-js@4.0.1(postcss@8.4.31): resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} engines: {node: ^12 || ^14 || >= 16} peerDependencies: postcss: ^8.4.21 dependencies: camelcase-css: 2.0.1 - postcss: 8.4.25 + postcss: 8.4.31 - /postcss-lab-function@4.2.1(postcss@8.4.25): + /postcss-lab-function@4.2.1(postcss@8.4.31): resolution: {integrity: sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.25) - postcss: 8.4.25 + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.31) + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-load-config@4.0.1(postcss@8.4.25): + /postcss-load-config@4.0.1(postcss@8.4.31): resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} engines: {node: '>= 14'} peerDependencies: @@ -16510,10 +16510,10 @@ packages: optional: true dependencies: lilconfig: 2.1.0 - postcss: 8.4.25 + postcss: 8.4.31 yaml: 2.3.1 - /postcss-loader@6.2.1(postcss@8.4.25)(webpack@5.88.1): + /postcss-loader@6.2.1(postcss@8.4.31)(webpack@5.88.1): resolution: {integrity: sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==} engines: {node: '>= 12.13.0'} peerDependencies: @@ -16522,37 +16522,37 @@ packages: dependencies: cosmiconfig: 7.1.0 klona: 2.0.6 - postcss: 8.4.25 + postcss: 8.4.31 semver: 7.5.3 webpack: 5.88.1(@swc/core@1.3.71)(esbuild@0.14.54) - /postcss-logical@5.0.4(postcss@8.4.25): + /postcss-logical@5.0.4(postcss@8.4.31): resolution: {integrity: sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.4 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 - /postcss-media-minmax@5.0.0(postcss@8.4.25): + /postcss-media-minmax@5.0.0(postcss@8.4.31): resolution: {integrity: sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==} engines: {node: '>=10.0.0'} peerDependencies: postcss: ^8.1.0 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 - /postcss-merge-longhand@5.1.7(postcss@8.4.25): + /postcss-merge-longhand@5.1.7(postcss@8.4.31): resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - stylehacks: 5.1.1(postcss@8.4.25) + stylehacks: 5.1.1(postcss@8.4.31) - /postcss-merge-rules@5.1.4(postcss@8.4.25): + /postcss-merge-rules@5.1.4(postcss@8.4.31): resolution: {integrity: sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: @@ -16560,189 +16560,189 @@ packages: dependencies: browserslist: 4.21.9 caniuse-api: 3.0.0 - cssnano-utils: 3.1.0(postcss@8.4.25) - postcss: 8.4.25 + cssnano-utils: 3.1.0(postcss@8.4.31) + postcss: 8.4.31 postcss-selector-parser: 6.0.13 - /postcss-minify-font-values@5.1.0(postcss@8.4.25): + /postcss-minify-font-values@5.1.0(postcss@8.4.31): resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-minify-gradients@5.1.1(postcss@8.4.25): + /postcss-minify-gradients@5.1.1(postcss@8.4.31): resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: colord: 2.9.3 - cssnano-utils: 3.1.0(postcss@8.4.25) - postcss: 8.4.25 + cssnano-utils: 3.1.0(postcss@8.4.31) + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-minify-params@5.1.4(postcss@8.4.25): + /postcss-minify-params@5.1.4(postcss@8.4.31): resolution: {integrity: sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: browserslist: 4.21.9 - cssnano-utils: 3.1.0(postcss@8.4.25) - postcss: 8.4.25 + cssnano-utils: 3.1.0(postcss@8.4.31) + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-minify-selectors@5.2.1(postcss@8.4.25): + /postcss-minify-selectors@5.2.1(postcss@8.4.31): resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 - /postcss-modules-extract-imports@3.0.0(postcss@8.4.25): + /postcss-modules-extract-imports@3.0.0(postcss@8.4.31): resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 - /postcss-modules-local-by-default@4.0.3(postcss@8.4.25): + /postcss-modules-local-by-default@4.0.3(postcss@8.4.31): resolution: {integrity: sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - icss-utils: 5.1.0(postcss@8.4.25) - postcss: 8.4.25 + icss-utils: 5.1.0(postcss@8.4.31) + postcss: 8.4.31 postcss-selector-parser: 6.0.13 postcss-value-parser: 4.2.0 - /postcss-modules-scope@3.0.0(postcss@8.4.25): + /postcss-modules-scope@3.0.0(postcss@8.4.31): resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 - /postcss-modules-values@4.0.0(postcss@8.4.25): + /postcss-modules-values@4.0.0(postcss@8.4.31): resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - icss-utils: 5.1.0(postcss@8.4.25) - postcss: 8.4.25 + icss-utils: 5.1.0(postcss@8.4.31) + postcss: 8.4.31 - /postcss-nested@6.0.1(postcss@8.4.25): + /postcss-nested@6.0.1(postcss@8.4.31): resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} engines: {node: '>=12.0'} peerDependencies: postcss: ^8.2.14 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 - /postcss-nesting@10.2.0(postcss@8.4.25): + /postcss-nesting@10.2.0(postcss@8.4.31): resolution: {integrity: sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.0.13) - postcss: 8.4.25 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 - /postcss-normalize-charset@5.1.0(postcss@8.4.25): + /postcss-normalize-charset@5.1.0(postcss@8.4.31): resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 - /postcss-normalize-display-values@5.1.0(postcss@8.4.25): + /postcss-normalize-display-values@5.1.0(postcss@8.4.31): resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-normalize-positions@5.1.1(postcss@8.4.25): + /postcss-normalize-positions@5.1.1(postcss@8.4.31): resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-normalize-repeat-style@5.1.1(postcss@8.4.25): + /postcss-normalize-repeat-style@5.1.1(postcss@8.4.31): resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-normalize-string@5.1.0(postcss@8.4.25): + /postcss-normalize-string@5.1.0(postcss@8.4.31): resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-normalize-timing-functions@5.1.0(postcss@8.4.25): + /postcss-normalize-timing-functions@5.1.0(postcss@8.4.31): resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-normalize-unicode@5.1.1(postcss@8.4.25): + /postcss-normalize-unicode@5.1.1(postcss@8.4.31): resolution: {integrity: sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: browserslist: 4.21.9 - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-normalize-url@5.1.0(postcss@8.4.25): + /postcss-normalize-url@5.1.0(postcss@8.4.31): resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: normalize-url: 6.1.0 - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-normalize-whitespace@5.1.1(postcss@8.4.25): + /postcss-normalize-whitespace@5.1.1(postcss@8.4.31): resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-normalize@10.0.1(browserslist@4.21.9)(postcss@8.4.25): + /postcss-normalize@10.0.1(browserslist@4.21.9)(postcss@8.4.31): resolution: {integrity: sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA==} engines: {node: '>= 12'} peerDependencies: @@ -16751,120 +16751,120 @@ packages: dependencies: '@csstools/normalize.css': 12.0.0 browserslist: 4.21.9 - postcss: 8.4.25 - postcss-browser-comments: 4.0.0(browserslist@4.21.9)(postcss@8.4.25) + postcss: 8.4.31 + postcss-browser-comments: 4.0.0(browserslist@4.21.9)(postcss@8.4.31) sanitize.css: 13.0.0 - /postcss-opacity-percentage@1.1.3(postcss@8.4.25): + /postcss-opacity-percentage@1.1.3(postcss@8.4.31): resolution: {integrity: sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 - /postcss-ordered-values@5.1.3(postcss@8.4.25): + /postcss-ordered-values@5.1.3(postcss@8.4.31): resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - cssnano-utils: 3.1.0(postcss@8.4.25) - postcss: 8.4.25 + cssnano-utils: 3.1.0(postcss@8.4.31) + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-overflow-shorthand@3.0.4(postcss@8.4.25): + /postcss-overflow-shorthand@3.0.4(postcss@8.4.31): resolution: {integrity: sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-page-break@3.0.4(postcss@8.4.25): + /postcss-page-break@3.0.4(postcss@8.4.31): resolution: {integrity: sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==} peerDependencies: postcss: ^8 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 - /postcss-place@7.0.5(postcss@8.4.25): + /postcss-place@7.0.5(postcss@8.4.31): resolution: {integrity: sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-preset-env@7.8.3(postcss@8.4.25): + /postcss-preset-env@7.8.3(postcss@8.4.31): resolution: {integrity: sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - '@csstools/postcss-cascade-layers': 1.1.1(postcss@8.4.25) - '@csstools/postcss-color-function': 1.1.1(postcss@8.4.25) - '@csstools/postcss-font-format-keywords': 1.0.1(postcss@8.4.25) - '@csstools/postcss-hwb-function': 1.0.2(postcss@8.4.25) - '@csstools/postcss-ic-unit': 1.0.1(postcss@8.4.25) - '@csstools/postcss-is-pseudo-class': 2.0.7(postcss@8.4.25) - '@csstools/postcss-nested-calc': 1.0.0(postcss@8.4.25) - '@csstools/postcss-normalize-display-values': 1.0.1(postcss@8.4.25) - '@csstools/postcss-oklab-function': 1.1.1(postcss@8.4.25) - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.25) - '@csstools/postcss-stepped-value-functions': 1.0.1(postcss@8.4.25) - '@csstools/postcss-text-decoration-shorthand': 1.0.0(postcss@8.4.25) - '@csstools/postcss-trigonometric-functions': 1.0.2(postcss@8.4.25) - '@csstools/postcss-unset-value': 1.0.2(postcss@8.4.25) - autoprefixer: 10.4.14(postcss@8.4.25) + '@csstools/postcss-cascade-layers': 1.1.1(postcss@8.4.31) + '@csstools/postcss-color-function': 1.1.1(postcss@8.4.31) + '@csstools/postcss-font-format-keywords': 1.0.1(postcss@8.4.31) + '@csstools/postcss-hwb-function': 1.0.2(postcss@8.4.31) + '@csstools/postcss-ic-unit': 1.0.1(postcss@8.4.31) + '@csstools/postcss-is-pseudo-class': 2.0.7(postcss@8.4.31) + '@csstools/postcss-nested-calc': 1.0.0(postcss@8.4.31) + '@csstools/postcss-normalize-display-values': 1.0.1(postcss@8.4.31) + '@csstools/postcss-oklab-function': 1.1.1(postcss@8.4.31) + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.31) + '@csstools/postcss-stepped-value-functions': 1.0.1(postcss@8.4.31) + '@csstools/postcss-text-decoration-shorthand': 1.0.0(postcss@8.4.31) + '@csstools/postcss-trigonometric-functions': 1.0.2(postcss@8.4.31) + '@csstools/postcss-unset-value': 1.0.2(postcss@8.4.31) + autoprefixer: 10.4.14(postcss@8.4.31) browserslist: 4.21.9 - css-blank-pseudo: 3.0.3(postcss@8.4.25) - css-has-pseudo: 3.0.4(postcss@8.4.25) - css-prefers-color-scheme: 6.0.3(postcss@8.4.25) + css-blank-pseudo: 3.0.3(postcss@8.4.31) + css-has-pseudo: 3.0.4(postcss@8.4.31) + css-prefers-color-scheme: 6.0.3(postcss@8.4.31) cssdb: 7.6.0 - postcss: 8.4.25 - postcss-attribute-case-insensitive: 5.0.2(postcss@8.4.25) - postcss-clamp: 4.1.0(postcss@8.4.25) - postcss-color-functional-notation: 4.2.4(postcss@8.4.25) - postcss-color-hex-alpha: 8.0.4(postcss@8.4.25) - postcss-color-rebeccapurple: 7.1.1(postcss@8.4.25) - postcss-custom-media: 8.0.2(postcss@8.4.25) - postcss-custom-properties: 12.1.11(postcss@8.4.25) - postcss-custom-selectors: 6.0.3(postcss@8.4.25) - postcss-dir-pseudo-class: 6.0.5(postcss@8.4.25) - postcss-double-position-gradients: 3.1.2(postcss@8.4.25) - postcss-env-function: 4.0.6(postcss@8.4.25) - postcss-focus-visible: 6.0.4(postcss@8.4.25) - postcss-focus-within: 5.0.4(postcss@8.4.25) - postcss-font-variant: 5.0.0(postcss@8.4.25) - postcss-gap-properties: 3.0.5(postcss@8.4.25) - postcss-image-set-function: 4.0.7(postcss@8.4.25) - postcss-initial: 4.0.1(postcss@8.4.25) - postcss-lab-function: 4.2.1(postcss@8.4.25) - postcss-logical: 5.0.4(postcss@8.4.25) - postcss-media-minmax: 5.0.0(postcss@8.4.25) - postcss-nesting: 10.2.0(postcss@8.4.25) - postcss-opacity-percentage: 1.1.3(postcss@8.4.25) - postcss-overflow-shorthand: 3.0.4(postcss@8.4.25) - postcss-page-break: 3.0.4(postcss@8.4.25) - postcss-place: 7.0.5(postcss@8.4.25) - postcss-pseudo-class-any-link: 7.1.6(postcss@8.4.25) - postcss-replace-overflow-wrap: 4.0.0(postcss@8.4.25) - postcss-selector-not: 6.0.1(postcss@8.4.25) + postcss: 8.4.31 + postcss-attribute-case-insensitive: 5.0.2(postcss@8.4.31) + postcss-clamp: 4.1.0(postcss@8.4.31) + postcss-color-functional-notation: 4.2.4(postcss@8.4.31) + postcss-color-hex-alpha: 8.0.4(postcss@8.4.31) + postcss-color-rebeccapurple: 7.1.1(postcss@8.4.31) + postcss-custom-media: 8.0.2(postcss@8.4.31) + postcss-custom-properties: 12.1.11(postcss@8.4.31) + postcss-custom-selectors: 6.0.3(postcss@8.4.31) + postcss-dir-pseudo-class: 6.0.5(postcss@8.4.31) + postcss-double-position-gradients: 3.1.2(postcss@8.4.31) + postcss-env-function: 4.0.6(postcss@8.4.31) + postcss-focus-visible: 6.0.4(postcss@8.4.31) + postcss-focus-within: 5.0.4(postcss@8.4.31) + postcss-font-variant: 5.0.0(postcss@8.4.31) + postcss-gap-properties: 3.0.5(postcss@8.4.31) + postcss-image-set-function: 4.0.7(postcss@8.4.31) + postcss-initial: 4.0.1(postcss@8.4.31) + postcss-lab-function: 4.2.1(postcss@8.4.31) + postcss-logical: 5.0.4(postcss@8.4.31) + postcss-media-minmax: 5.0.0(postcss@8.4.31) + postcss-nesting: 10.2.0(postcss@8.4.31) + postcss-opacity-percentage: 1.1.3(postcss@8.4.31) + postcss-overflow-shorthand: 3.0.4(postcss@8.4.31) + postcss-page-break: 3.0.4(postcss@8.4.31) + postcss-place: 7.0.5(postcss@8.4.31) + postcss-pseudo-class-any-link: 7.1.6(postcss@8.4.31) + postcss-replace-overflow-wrap: 4.0.0(postcss@8.4.31) + postcss-selector-not: 6.0.1(postcss@8.4.31) postcss-value-parser: 4.2.0 - /postcss-pseudo-class-any-link@7.1.6(postcss@8.4.25): + /postcss-pseudo-class-any-link@7.1.6(postcss@8.4.31): resolution: {integrity: sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 - /postcss-reduce-initial@5.1.2(postcss@8.4.25): + /postcss-reduce-initial@5.1.2(postcss@8.4.31): resolution: {integrity: sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: @@ -16872,31 +16872,31 @@ packages: dependencies: browserslist: 4.21.9 caniuse-api: 3.0.0 - postcss: 8.4.25 + postcss: 8.4.31 - /postcss-reduce-transforms@5.1.0(postcss@8.4.25): + /postcss-reduce-transforms@5.1.0(postcss@8.4.31): resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 - /postcss-replace-overflow-wrap@4.0.0(postcss@8.4.25): + /postcss-replace-overflow-wrap@4.0.0(postcss@8.4.31): resolution: {integrity: sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==} peerDependencies: postcss: ^8.0.3 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 - /postcss-selector-not@6.0.1(postcss@8.4.25): + /postcss-selector-not@6.0.1(postcss@8.4.31): resolution: {integrity: sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 /postcss-selector-parser@6.0.13: @@ -16906,23 +16906,23 @@ packages: cssesc: 3.0.0 util-deprecate: 1.0.2 - /postcss-svgo@5.1.0(postcss@8.4.25): + /postcss-svgo@5.1.0(postcss@8.4.31): resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-value-parser: 4.2.0 svgo: 2.8.0 - /postcss-unique-selectors@5.1.1(postcss@8.4.25): + /postcss-unique-selectors@5.1.1(postcss@8.4.31): resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.25 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 /postcss-value-parser@4.2.0: @@ -16935,8 +16935,8 @@ packages: picocolors: 0.2.1 source-map: 0.6.1 - /postcss@8.4.25: - resolution: {integrity: sha512-7taJ/8t2av0Z+sQEvNzCkpDynl0tX3uJMCODi6nT3PfASC7dYCWV9aQ+uiCf+KBD4SEFcu+GvJdGdwzQ6OSjCw==} + /postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} dependencies: nanoid: 3.3.6 @@ -17791,11 +17791,11 @@ packages: jest-resolve: 27.5.1 jest-watch-typeahead: 1.1.0(jest@27.5.1) mini-css-extract-plugin: 2.7.6(webpack@5.88.1) - postcss: 8.4.25 - postcss-flexbugs-fixes: 5.0.2(postcss@8.4.25) - postcss-loader: 6.2.1(postcss@8.4.25)(webpack@5.88.1) - postcss-normalize: 10.0.1(browserslist@4.21.9)(postcss@8.4.25) - postcss-preset-env: 7.8.3(postcss@8.4.25) + postcss: 8.4.31 + postcss-flexbugs-fixes: 5.0.2(postcss@8.4.31) + postcss-loader: 6.2.1(postcss@8.4.31)(webpack@5.88.1) + postcss-normalize: 10.0.1(browserslist@4.21.9)(postcss@8.4.31) + postcss-preset-env: 7.8.3(postcss@8.4.31) prompts: 2.4.2 react: 18.2.0 react-app-polyfill: 3.0.0 @@ -19238,14 +19238,14 @@ packages: inline-style-parser: 0.1.1 dev: false - /stylehacks@5.1.1(postcss@8.4.25): + /stylehacks@5.1.1(postcss@8.4.31): resolution: {integrity: sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: browserslist: 4.21.9 - postcss: 8.4.25 + postcss: 8.4.31 postcss-selector-parser: 6.0.13 /stylis@4.2.0: @@ -19391,11 +19391,11 @@ packages: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.0.0 - postcss: 8.4.25 - postcss-import: 15.1.0(postcss@8.4.25) - postcss-js: 4.0.1(postcss@8.4.25) - postcss-load-config: 4.0.1(postcss@8.4.25) - postcss-nested: 6.0.1(postcss@8.4.25) + postcss: 8.4.31 + postcss-import: 15.1.0(postcss@8.4.31) + postcss-js: 4.0.1(postcss@8.4.31) + postcss-load-config: 4.0.1(postcss@8.4.31) + postcss-nested: 6.0.1(postcss@8.4.31) postcss-selector-parser: 6.0.13 postcss-value-parser: 4.2.0 resolve: 1.22.2 @@ -20926,9 +20926,9 @@ packages: use-sync-external-store: 1.2.0(react@18.2.0) dev: false - github.com/theopensystemslab/planx-core/d92224b(@types/react@18.2.20): - resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/d92224b} - id: github.com/theopensystemslab/planx-core/d92224b + git/github.com+theopensystemslab/planx-core/d92224b(@types/react@18.2.20): + resolution: {commit: d92224b, repo: git@github.com:theopensystemslab/planx-core.git, type: git} + id: git@github.com+theopensystemslab/planx-core/d92224b name: '@opensystemslab/planx-core' version: 1.0.0 prepare: true From 2ce327edf582b1270ff4a0c4bc5f45d1c935d807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Fri, 6 Oct 2023 15:59:15 +0100 Subject: [PATCH 7/8] dp/fix-date-handling (#2285) --- .../webhooks/service/lowcalSessionEvents/schema.ts | 2 +- .../webhooks/service/paymentRequestEvents/schema.ts | 2 +- api.planx.uk/shared/middleware/validate.ts | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/api.planx.uk/modules/webhooks/service/lowcalSessionEvents/schema.ts b/api.planx.uk/modules/webhooks/service/lowcalSessionEvents/schema.ts index 53e73f0b06..2f35683b0e 100644 --- a/api.planx.uk/modules/webhooks/service/lowcalSessionEvents/schema.ts +++ b/api.planx.uk/modules/webhooks/service/lowcalSessionEvents/schema.ts @@ -4,7 +4,7 @@ import { ScheduledEventResponse } from "../../../../hasura/metadata"; export const createSessionEventSchema = z.object({ body: z.object({ - createdAt: z.string().transform((val) => new Date(val)), + createdAt: z.string().pipe(z.coerce.date()), payload: z.object({ sessionId: z.string(), }), diff --git a/api.planx.uk/modules/webhooks/service/paymentRequestEvents/schema.ts b/api.planx.uk/modules/webhooks/service/paymentRequestEvents/schema.ts index 5b7a48404c..042ea05f71 100644 --- a/api.planx.uk/modules/webhooks/service/paymentRequestEvents/schema.ts +++ b/api.planx.uk/modules/webhooks/service/paymentRequestEvents/schema.ts @@ -4,7 +4,7 @@ import { ScheduledEventResponse } from "../../../../hasura/metadata"; export const createPaymentEventSchema = z.object({ body: z.object({ - createdAt: z.string().transform((val) => new Date(val)), + createdAt: z.string().pipe(z.coerce.date()), payload: z.object({ paymentRequestId: z.string(), }), diff --git a/api.planx.uk/shared/middleware/validate.ts b/api.planx.uk/shared/middleware/validate.ts index 854a37938e..b2b740df00 100644 --- a/api.planx.uk/shared/middleware/validate.ts +++ b/api.planx.uk/shared/middleware/validate.ts @@ -13,11 +13,18 @@ export const validate = next: NextFunction, ) => { try { - schema.parse({ + const parsedReq = schema.parse({ params: req.params, body: req.body, query: req.query, }); + + // Assign parsed values to the request object + // Required for schemas to transform or coerce raw requests + req.params = parsedReq.params; + req.body = parsedReq.body; + req.query = parsedReq.query; + return next(); } catch (error) { console.error(error); From 380f27e35758148a022f7de2bd7f30290a7aadf3 Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Mon, 9 Oct 2023 07:51:33 +0100 Subject: [PATCH 8/8] feat: add option to use "Pay without pay" and show error in cases of undefined fee (#2284) --- .../src/@planx/components/Pay/Editor.tsx | 39 ++-- .../@planx/components/Pay/Public/Confirm.tsx | 120 +++++++----- .../components/Pay/Public/Pay.stories.tsx | 14 ++ .../@planx/components/Pay/Public/Pay.test.tsx | 183 ++++++++++++++++-- .../src/@planx/components/Pay/Public/Pay.tsx | 50 +++-- .../src/@planx/components/Pay/model.ts | 2 + 6 files changed, 310 insertions(+), 98 deletions(-) diff --git a/editor.planx.uk/src/@planx/components/Pay/Editor.tsx b/editor.planx.uk/src/@planx/components/Pay/Editor.tsx index e37945e05b..b6fac2b43e 100644 --- a/editor.planx.uk/src/@planx/components/Pay/Editor.tsx +++ b/editor.planx.uk/src/@planx/components/Pay/Editor.tsx @@ -31,6 +31,7 @@ function Component(props: any) { `

You can pay for your application by using GOV.UK Pay.

\

Your application will be sent after you have paid the fee. \ Wait until you see an application sent message before closing your browser.

`, + hidePay: props.node?.data?.hidePay || false, allowInviteToPay: props.node?.data?.allowInviteToPay ?? true, secondaryPageTitle: props.node?.data?.secondaryPageTitle || @@ -113,20 +114,30 @@ function Component(props: any) { /> - - - { - formik.setFieldValue( - "allowInviteToPay", - !formik.values.allowInviteToPay, - ); - }} - > - Allow applicants to invite someone else to pay - - + { + formik.setFieldValue("hidePay", !formik.values.hidePay); + }} + style={{ width: "100%" }} + > + Hide the pay buttons and show fee for information only + + + + + { + formik.setFieldValue( + "allowInviteToPay", + !formik.values.allowInviteToPay, + ); + }} + style={{ width: "100%" }} + > + Allow applicants to invite someone else to pay + {formik.values.allowInviteToPay ? ( <> diff --git a/editor.planx.uk/src/@planx/components/Pay/Public/Confirm.tsx b/editor.planx.uk/src/@planx/components/Pay/Public/Confirm.tsx index a482eaa863..2b5c6efa93 100644 --- a/editor.planx.uk/src/@planx/components/Pay/Public/Confirm.tsx +++ b/editor.planx.uk/src/@planx/components/Pay/Public/Confirm.tsx @@ -15,6 +15,7 @@ import ReactMarkdownOrHtml from "ui/ReactMarkdownOrHtml"; import { formattedPriceWithCurrencySymbol } from "../model"; import InviteToPayForm, { InviteToPayFormProps } from "./InviteToPayForm"; +import { PAY_API_ERROR_UNSUPPORTED_TEAM } from "./Pay"; export interface Props { title?: string; @@ -35,6 +36,7 @@ export interface Props { onConfirm: () => void; error?: string; hideFeeBanner?: boolean; + hidePay?: boolean; } interface PayBodyProps extends Props { @@ -60,53 +62,9 @@ const PayBody: React.FC = (props) => { const path = useStore((state) => state.path); const isSaveReturn = path === ApplicationPath.SaveAndReturn; - return ( - <> - {!props.error ? ( - - - - {props.instructionsTitle || "How to pay"} - - You can pay for your application by using GOV.UK Pay.

\ -

Your application will be sent after you have paid the fee. \ - Wait until you see an application sent message before closing your browser.

` - } - openLinksOnNewTab - /> - - {props.showInviteToPay && ( - <> - - - )} - {isSaveReturn && } -
-
- ) : ( + if (props.error) { + if (props.error.startsWith(PAY_API_ERROR_UNSUPPORTED_TEAM)) { + return ( @@ -118,8 +76,68 @@ const PayBody: React.FC = (props) => { - )} - + ); + } else { + return ( + + + + {props.error} + + + This error has been logged and our team will see it soon. You can + safely close this tab and try resuming again soon by returning to + this URL. + + + + ); + } + } + + return ( + + + + {props.instructionsTitle || "How to pay"} + + You can pay for your application by using GOV.UK Pay.

\ +

Your application will be sent after you have paid the fee. \ + Wait until you see an application sent message before closing your browser.

` + } + openLinksOnNewTab + /> + + {!props.hidePay && props.showInviteToPay && ( + <> + + + )} + {isSaveReturn && } +
+
); }; @@ -176,7 +194,9 @@ export default function Confirm(props: Props) { className="marginBottom" component="span" > - {formattedPriceWithCurrencySymbol(props.fee)} + {isNaN(props.fee) + ? "Unknown" + : formattedPriceWithCurrencySymbol(props.fee)} { - const handleSubmit = jest.fn(); +const flowWithUndefinedFee: Store.flow = { + _root: { + edges: ["setValue", "pay"], + }, + setValue: { + type: TYPES.SetValue, + edges: ["pay"], + data: { + fn: "application.fee.payable", + val: "0", + }, + }, + pay: { + type: TYPES.Pay, + data: { + fn: "application.fee.typo", + }, + }, +}; - // if no props.fn, then fee defaults to 0 - setup(); +const flowWithZeroFee: Store.flow = { + _root: { + edges: ["setValue", "pay"], + }, + setValue: { + type: TYPES.SetValue, + edges: ["pay"], + data: { + fn: "application.fee.payable", + val: "0", + }, + }, + pay: { + type: TYPES.Pay, + data: { + fn: "application.fee.payable", + }, + }, +}; - // handleSubmit is still called to set auto = true so Pay isn't seen in card sequence - expect(handleSubmit).toHaveBeenCalled(); -}); +// Mimic having passed setValue to reach Pay +const breadcrumbs: Breadcrumbs = { + setValue: { + auto: true, + data: { + "application.fee.payable": ["0"], + }, + }, +}; + +describe("Pay component when fee is undefined or £0", () => { + beforeEach(() => { + getState().resetPreview(); + }); + + it("Shows an error if fee is undefined", () => { + const handleSubmit = jest.fn(); + + setState({ flow: flowWithUndefinedFee, breadcrumbs: breadcrumbs }); + expect(getState().computePassport()).toEqual({ + data: { "application.fee.payable": ["0"] }, + }); + + setup(); + + // handleSubmit has NOT been called (not skipped), Pay shows error instead + expect(handleSubmit).not.toHaveBeenCalled(); + expect( + screen.getByText("We are unable to calculate your fee right now"), + ).toBeInTheDocument(); + expect(screen.queryByText("Continue")).not.toBeInTheDocument(); + }); + + it("Skips pay if fee = 0", () => { + const handleSubmit = jest.fn(); + + setState({ flow: flowWithZeroFee, breadcrumbs: breadcrumbs }); + expect(getState().computePassport()).toEqual({ + data: { "application.fee.payable": ["0"] }, + }); -it("should not have any accessibility violations", async () => { - const handleSubmit = jest.fn(); - const { container } = setup(); - const results = await axe(container); - expect(results).toHaveNoViolations(); + setup(); + + // handleSubmit is called to auto-answer Pay (aka "skip" in card sequence) + expect(handleSubmit).toHaveBeenCalled(); + }); }); const defaultProps = { @@ -89,7 +161,8 @@ describe("Confirm component without inviteToPay", () => { it("displays an error and continue-with-testing button if Pay is not enabled for this team", async () => { const handleSubmit = jest.fn(); - const errorMessage = "No pay token found for this team!"; + const errorMessage = + "GOV.UK Pay is not enabled for this local authority (testing)"; const { user } = setup( { expect(handleSubmit).toHaveBeenCalled(); }); - it("should not have any accessibility violations", async () => { - const { container } = setup(); - const results = await axe(container); - expect(results).toHaveNoViolations(); - }); - it("displays the Save/Resume option if the application path requires it", () => { act(() => setState({ @@ -141,6 +208,12 @@ describe("Confirm component without inviteToPay", () => { expect(screen.queryByText(saveButtonText)).not.toBeInTheDocument(); expect(screen.queryByText(resumeButtonText)).not.toBeInTheDocument(); }); + + it("should not have any accessibility violations", async () => { + const { container } = setup(); + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); }); describe("Confirm component with inviteToPay", () => { @@ -293,3 +366,71 @@ describe("Confirm component with inviteToPay", () => { expect(results).toHaveNoViolations(); }); }); + +describe("Confirm component in information-only mode", () => { + beforeAll(() => (initialState = getState())); + afterEach(() => act(() => setState(initialState))); + + it("renders correctly", async () => { + const handleSubmit = jest.fn(); + const { user } = setup( + , + ); + + expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent( + "Pay for your application", + ); + expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent( + "The fee is", + ); + expect(screen.getByRole("heading", { level: 3 })).toHaveTextContent( + "How to pay", + ); + + expect(screen.getByRole("button")).toHaveTextContent("Continue"); + expect(screen.getByRole("button")).not.toHaveTextContent("Pay"); + + await user.click(screen.getByText("Continue")); + expect(handleSubmit).toHaveBeenCalled(); + }); + + it("renders correctly when inviteToPay is also toggled on by an editor", async () => { + const handleSubmit = jest.fn(); + const { user } = setup( + , + ); + + expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent( + "Pay for your application", + ); + expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent( + "The fee is", + ); + expect(screen.getByRole("heading", { level: 3 })).toHaveTextContent( + "How to pay", + ); + + expect(screen.getByRole("button")).toHaveTextContent("Continue"); + expect(screen.getByRole("button")).not.toHaveTextContent("Pay"); + expect(screen.getByRole("button")).not.toHaveTextContent( + "Invite someone else to pay for this application", + ); + + await user.click(screen.getByText("Continue")); + expect(handleSubmit).toHaveBeenCalled(); + }); + + it("should not have any accessibility violations", async () => { + const handleSubmit = jest.fn(); + const { container } = setup( + , + ); + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/editor.planx.uk/src/@planx/components/Pay/Public/Pay.tsx b/editor.planx.uk/src/@planx/components/Pay/Public/Pay.tsx index c843b3d5dc..6925de00fd 100644 --- a/editor.planx.uk/src/@planx/components/Pay/Public/Pay.tsx +++ b/editor.planx.uk/src/@planx/components/Pay/Public/Pay.tsx @@ -30,9 +30,11 @@ type ComponentState = | { status: "fetching_payment"; displayText?: string } | { status: "retry" } | { status: "success"; displayText?: string } - | { status: "unsupported_team" }; + | { status: "unsupported_team" } + | { status: "undefined_fee" }; enum Action { + NoFeeFound, NoPaymentFound, IncompletePaymentFound, IncompletePaymentConfirmed, @@ -42,6 +44,9 @@ enum Action { Success, } +export const PAY_API_ERROR_UNSUPPORTED_TEAM = + "GOV.UK Pay is not enabled for this local authority"; + function Component(props: Props) { const [ flowId, @@ -67,6 +72,8 @@ function Component(props: Props) { // Handles UI states const reducer = (_state: ComponentState, action: Action): ComponentState => { switch (action) { + case Action.NoFeeFound: + return { status: "undefined_fee" }; case Action.NoPaymentFound: return { status: "init" }; case Action.IncompletePaymentFound: @@ -101,11 +108,18 @@ function Component(props: Props) { const handleError = useErrorHandler(); useEffect(() => { - if (isNaN(fee) || fee <= 0) { - // skip the pay component because there's no fee to charge + // Auto-skip component when fee=0 + if (fee <= 0) { return props.handleSubmit({ auto: true }); } + // If props.fn is undefined, display & log an error + if (isNaN(fee)) { + dispatch(Action.NoFeeFound); + logger.notify(`Unable to calculate fee for session ${sessionId}`); + return; + } + if (!govUkPayment) { dispatch(Action.NoPaymentFound); return; @@ -231,7 +245,11 @@ function Component(props: Props) { window.location.replace(payment._links.next_url.href); }) .catch((error) => { - if (error.response?.data?.error?.endsWith("local authority")) { + if ( + error.response?.data?.error?.startsWith( + PAY_API_ERROR_UNSUPPORTED_TEAM, + ) + ) { // Show a custom message if this team isn't set up to use Pay yet dispatch(Action.StartNewPaymentError); } else { @@ -247,18 +265,19 @@ function Component(props: Props) { <> {state.status === "init" || state.status === "retry" || - state.status === "unsupported_team" ? ( + state.status === "unsupported_team" || + state.status === "undefined_fee" ? ( { - if (state.status === "init") { + if (props.hidePay || state.status === "unsupported_team") { + // Show "Continue" button to proceed + props.handleSubmit({ auto: false }); + } else if (state.status === "init") { startNewPayment(); } else if (state.status === "retry") { resumeExistingPayment(); - } else if (state.status === "unsupported_team") { - // Allow "Continue" button to skip Pay - props.handleSubmit({ auto: true }); } }} buttonTitle={ @@ -267,14 +286,19 @@ function Component(props: Props) { : "Retry payment" } error={ - state.status === "unsupported_team" - ? "GOV.UK Pay is not enabled for this local authority" - : undefined + (state.status === "unsupported_team" && + "GOV.UK Pay is not enabled for this local authority") || + (state.status === "undefined_fee" && + "We are unable to calculate your fee right now") || + undefined } showInviteToPay={ - props.allowInviteToPay && state.status !== "unsupported_team" + props.allowInviteToPay && + !props.hidePay && + state.status !== "unsupported_team" } paymentStatus={govUkPayment?.state?.status} + hidePay={props.hidePay} /> ) : ( diff --git a/editor.planx.uk/src/@planx/components/Pay/model.ts b/editor.planx.uk/src/@planx/components/Pay/model.ts index 7ff2c6d0e7..9d0fe202d3 100644 --- a/editor.planx.uk/src/@planx/components/Pay/model.ts +++ b/editor.planx.uk/src/@planx/components/Pay/model.ts @@ -12,6 +12,7 @@ export interface Pay extends MoreInformation { fn?: string; instructionsTitle?: string; instructionsDescription?: string; + hidePay?: boolean; allowInviteToPay?: boolean; secondaryPageTitle?: string; nomineeTitle?: string; @@ -87,6 +88,7 @@ export const validationSchema = object({ fn: string().trim().required("Data field is required"), instructionsTitle: string().trim().required(), instructionsDescription: string().trim().required(), + hidePay: boolean(), allowInviteToPay: boolean(), nomineeTitle: string().trim().when("allowInviteToPay", { is: true,