Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Modularise /admin endpoints, improve Feedback endpoint docs #2413

Merged
merged 3 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading