Skip to content

Commit

Permalink
chore: Modularise /admin endpoints, improve Feedback endpoint docs (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
DafyddLlyr authored Nov 13, 2023
1 parent bf09f02 commit 2b0f6af
Show file tree
Hide file tree
Showing 22 changed files with 278 additions and 217 deletions.
161 changes: 0 additions & 161 deletions api.planx.uk/admin/feedback/downloadFeedbackCSV.ts

This file was deleted.

20 changes: 20 additions & 0 deletions api.planx.uk/modules/admin/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { generateFeedbackCSV } from "./service/feedback/downloadFeedbackCSV";
import { DownloadFeedbackCSVController } from "./service/feedback/types";

export const downloadFeedbackCSV: DownloadFeedbackCSVController = async (
req,
res,
next,
) => {
try {
const feedbackFishCookie = res.locals.parsedReq.query.cookie;
const csvStream = await generateFeedbackCSV(feedbackFishCookie);
res.header("Content-type", "text/csv");
return csvStream.pipe(res);
} catch (error) {
return next({
message:
"Failed to generate FeedbackFish CSV: " + (error as Error).message,
});
}
};
28 changes: 28 additions & 0 deletions api.planx.uk/modules/admin/docs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
openapi: 3.1.0
info:
title: Plan✕ API
version: 0.1.0
tags:
- name: admin
description: Admin only utility endpoints
paths:
/admin/feedback:
get:
tags: ["admin"]
security:
- bearerAuth: []
summary: Download FeedbackFish CSV
parameters:
- in: query
name: cookie
type: string
description: Cookie from FeedbackFish to authenticate request
required: true
responses:
"200":
content:
text/csv:
schema:
type: string
"500":
$ref: "#/components/responses/ErrorMessage"
37 changes: 37 additions & 0 deletions api.planx.uk/modules/admin/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Router } from "express";
import { usePlatformAdminAuth } from "../auth/middleware";
import { getOneAppXML } from "./session/oneAppXML";
import { getBOPSPayload } from "./session/bops";
import { getCSVData, getRedactedCSVData } from "./session/csv";
import { getHTMLExport, getRedactedHTMLExport } from "./session/html";
import { generateZip } from "./session/zip";
import { getSessionSummary } from "./session/summary";
import { getDigitalPlanningApplicationPayload } from "./session/digitalPlanningData";
import { validate } from "../../shared/middleware/validate";
import { downloadFeedbackCSVSchema } from "./service/feedback/types";
import { downloadFeedbackCSV } from "./controller";

const router = Router();

router.use(usePlatformAdminAuth);
router.get(
"/feedback",
validate(downloadFeedbackCSVSchema),
downloadFeedbackCSV,
);

// TODO: Split the routes below into controller and service components
router.get("/session/:sessionId/xml", getOneAppXML);
router.get("/session/:sessionId/bops", getBOPSPayload);
router.get("/session/:sessionId/csv", getCSVData);
router.get("/session/:sessionId/csv-redacted", getRedactedCSVData);
router.get("/session/:sessionId/html", getHTMLExport);
router.get("/session/:sessionId/html-redacted", getRedactedHTMLExport);
router.get("/session/:sessionId/zip", generateZip);
router.get("/session/:sessionId/summary", getSessionSummary);
router.get(
"/session/:sessionId/digital-planning-application",
getDigitalPlanningApplicationPayload,
);

export default router;
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Feedback, parseFeedback } from "./downloadFeedbackCSV";
import { parseFeedback } from "./downloadFeedbackCSV";
import supertest from "supertest";
import app from "../../server";
import { authHeader } from "../../tests/mockJWT";
import app from "../../../../server";
import { authHeader } from "../../../../tests/mockJWT";
import Axios from "axios";
import { Feedback } from "./types";

jest.mock("axios");
const mockAxios = Axios as jest.Mocked<typeof Axios>;
Expand Down Expand Up @@ -72,8 +73,11 @@ describe("Download feedback CSV endpoint", () => {
await supertest(app)
.get(ENDPOINT)
.set(auth)
.expect(401)
.then((res) => expect(res.body).toEqual({ error: "Missing cookie" }));
.expect(400)
.then((res) => {
expect(res.body).toHaveProperty("issues");
expect(res.body).toHaveProperty("name", "ZodError");
});
});

it("returns an error if request to FeedbackFish fails", async () => {
Expand Down
85 changes: 85 additions & 0 deletions api.planx.uk/modules/admin/service/feedback/downloadFeedbackCSV.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { gql } from "graphql-request";
import Axios from "axios";
import { stringify } from "csv-stringify";
import { Feedback, METADATA_KEYS, ParsedFeedback } from "./types";

export const generateFeedbackCSV = async (feedbackFishCookie: string) => {
const feedback = await fetchFeedback(feedbackFishCookie);
const parsedFeedback = parseFeedback(feedback);
const csvStream = stringify(parsedFeedback, {
header: true,
columns: [
"id",
"text",
"category",
"createdAt",
"location",
"screenshotUrl",
"device",
...METADATA_KEYS,
],
});
return csvStream;
};

const fetchFeedback = async (cookie: string): Promise<Feedback[]> => {
const feedbackFishGraphQLEndpoint = "https://graphcdn.api.feedback.fish/";
const body = {
query: gql`
query getFeedback($projectId: String!) {
feedback(projectId: $projectId) {
id
text
category
createdAt
location
screenshotUrl
metadata {
key
value
}
device {
client {
name
version
}
os {
name
version
}
}
}
}
`,
operationName: "getFeedback",
variables: {
projectId: "65f02de00b90d1",
},
};

try {
const result = await Axios.post(feedbackFishGraphQLEndpoint, body, {
headers: { cookie },
});
const feedback: Feedback[] = result.data.data.feedback;
return feedback;
} catch (error) {
throw Error(
"Failed to connect to FeedbackFish: " + (error as Error).message,
);
}
};

const generateMetadata = (feedback: ParsedFeedback): ParsedFeedback => {
// Transform metadata into kv pairs
feedback.metadata?.forEach(({ key, value }) => (feedback[key] = value));
// Drop redundant raw metadata
delete feedback.metadata;
return feedback;
};

export const parseFeedback = (feedback: Feedback[]): ParsedFeedback[] => {
const parsedFeedback: ParsedFeedback[] = [...feedback];
parsedFeedback.map(generateMetadata);
return parsedFeedback;
};
Loading

0 comments on commit 2b0f6af

Please sign in to comment.