Skip to content

Commit

Permalink
chore: Fix Run API test type warnings
Browse files Browse the repository at this point in the history
  • Loading branch information
DafyddLlyr committed Oct 26, 2023
1 parent caf3fb9 commit e080d0e
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 48 deletions.
17 changes: 9 additions & 8 deletions api.planx.uk/editor/publish.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import supertest from "supertest";
import { queryMock } from "../tests/graphqlQueryMock";
import { authHeader } from "../tests/mockJWT";
import app from "../server";
import { Flow } from "../types";
import { flowWithInviteToPay } from "../tests/mocks/inviteToPayData";
import { FlowGraph } from "@opensystemslab/planx-core/types";

beforeEach(() => {
queryMock.mockQuery({
Expand Down Expand Up @@ -168,7 +168,7 @@ describe("sections validation on diff", () => {
});

it("does not update if there are sections, but there is not a section in the first position", async () => {
const flowWithSections: Flow["data"] = {
const flowWithSections: FlowGraph = {
_root: {
edges: ["questionNode", "sectionNode"],
},
Expand Down Expand Up @@ -205,7 +205,7 @@ describe("sections validation on diff", () => {

describe("invite to pay validation on diff", () => {
it("does not update if invite to pay is enabled, but there is not a Send component", async () => {
const { Send, ...invalidatedFlow } = flowWithInviteToPay;
const { Send: _Send, ...invalidatedFlow } = flowWithInviteToPay;
invalidatedFlow["_root"].edges?.splice(
invalidatedFlow["_root"].edges?.indexOf("Send"),
);
Expand Down Expand Up @@ -266,7 +266,8 @@ describe("invite to pay validation on diff", () => {
});

it("does not update if invite to pay is enabled, but there is not a FindProperty (`_address`) component", async () => {
const { FindProperty, ...invalidatedFlow } = flowWithInviteToPay;
const { FindProperty: _FindProperty, ...invalidatedFlow } =
flowWithInviteToPay;
invalidatedFlow["_root"].edges?.splice(
invalidatedFlow["_root"].edges?.indexOf("FindProperty"),
);
Expand Down Expand Up @@ -326,9 +327,9 @@ describe("invite to pay validation on diff", () => {

it("does not update if invite to pay is enabled, but there is not a Checklist that sets `proposal.projectType`", async () => {
const {
Checklist,
ChecklistOptionOne,
ChecklistOptionTwo,
Checklist: _Checklist,
ChecklistOptionOne: _ChecklistOptionOne,
ChecklistOptionTwo: _ChecklistOptionTwo,
...invalidatedFlow
} = flowWithInviteToPay;
invalidatedFlow["_root"].edges?.splice(
Expand Down Expand Up @@ -358,7 +359,7 @@ describe("invite to pay validation on diff", () => {
});
});

const mockFlowData: Flow["data"] = {
const mockFlowData: FlowGraph = {
_root: {
edges: [
"SectionOne",
Expand Down
91 changes: 57 additions & 34 deletions api.planx.uk/editor/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import { adminGraphQLClient as adminClient } from "../hasura";
import { dataMerged, getMostRecentPublishedFlow } from "../helpers";
import { gql } from "graphql-request";
import intersection from "lodash/intersection";
import { ComponentType } from "@opensystemslab/planx-core/types";
import {
ComponentType,
FlowGraph,
Node,
} from "@opensystemslab/planx-core/types";
import { userContext } from "../modules/auth/middleware";
import type { Entry } from "type-fest";

const validateAndDiffFlow = async (
req: Request,
Expand Down Expand Up @@ -139,17 +144,17 @@ type ValidationResponse = {
description?: string;
};

const validateSections = (flow: Record<string, any>): ValidationResponse => {
if (getSectionNodeIds(flow)?.length > 0) {
if (!sectionIsInFirstPosition(flow)) {
const validateSections = (flowGraph: FlowGraph): ValidationResponse => {
if (getSectionNodeIds(flowGraph)?.length > 0) {
if (!sectionIsInFirstPosition(flowGraph)) {
return {
isValid: false,
message: "Cannot publish an invalid flow",
description: "When using Sections, your flow must start with a Section",
};
}

if (!allSectionsOnRoot(flow)) {
if (!allSectionsOnRoot(flowGraph)) {
return {
isValid: false,
message: "Cannot publish an invalid flow",
Expand All @@ -165,57 +170,58 @@ const validateSections = (flow: Record<string, any>): ValidationResponse => {
};
};

const getSectionNodeIds = (flow: Record<string, any>): string[] => {
return Object.entries(flow)
.filter(([_nodeId, nodeData]) => nodeData?.type === ComponentType.Section)
?.map(([nodeId, _nodeData]) => nodeId);
const getSectionNodeIds = (flowGraph: FlowGraph): string[] => {
const sectionNodes = Object.entries(flowGraph).filter((entry) =>
isComponentType(entry, ComponentType.Section),
);
return sectionNodes.map(([nodeId, _nodeData]) => nodeId);
};

const sectionIsInFirstPosition = (flow: Record<string, any>): boolean => {
const firstNodeId = flow["_root"].edges[0];
return flow[firstNodeId].type === ComponentType.Section;
const sectionIsInFirstPosition = (flowGraph: FlowGraph): boolean => {
const firstNodeId = flowGraph["_root"].edges[0];
return flowGraph[firstNodeId].type === ComponentType.Section;
};

const allSectionsOnRoot = (flow: Record<string, any>): boolean => {
const sectionTypeNodeIds = getSectionNodeIds(flow);
const allSectionsOnRoot = (flowData: FlowGraph): boolean => {
const sectionTypeNodeIds = getSectionNodeIds(flowData);
const intersectingNodeIds = intersection(
flow["_root"].edges,
flowData["_root"].edges,
sectionTypeNodeIds,
);
return intersectingNodeIds.length === sectionTypeNodeIds.length;
};

const validateInviteToPay = (flow: Record<string, any>): ValidationResponse => {
const validateInviteToPay = (flowGraph: FlowGraph): ValidationResponse => {
const invalidResponseTemplate = {
isValid: false,
message: "Cannot publish an invalid flow",
};

if (inviteToPayEnabled(flow)) {
if (numberOfComponentType(flow, ComponentType.Pay) > 1) {
if (inviteToPayEnabled(flowGraph)) {
if (numberOfComponentType(flowGraph, ComponentType.Pay) > 1) {
return {
...invalidResponseTemplate,
description:
"When using Invite to Pay, your flow must have exactly ONE Pay",
};
}

if (!hasComponentType(flow, ComponentType.Send)) {
if (!hasComponentType(flowGraph, ComponentType.Send)) {
return {
...invalidResponseTemplate,
description: "When using Invite to Pay, your flow must have a Send",
};
}

if (numberOfComponentType(flow, ComponentType.Send) > 1) {
if (numberOfComponentType(flowGraph, ComponentType.Send) > 1) {
return {
...invalidResponseTemplate,
description:
"When using Invite to Pay, your flow must have exactly ONE Send. It can select many destinations",
};
}

if (!hasComponentType(flow, ComponentType.FindProperty)) {
if (!hasComponentType(flowGraph, ComponentType.FindProperty)) {
return {
...invalidResponseTemplate,
description:
Expand All @@ -224,7 +230,11 @@ const validateInviteToPay = (flow: Record<string, any>): ValidationResponse => {
}

if (
!hasComponentType(flow, ComponentType.Checklist, "proposal.projectType")
!hasComponentType(
flowGraph,
ComponentType.Checklist,
"proposal.projectType",
)
) {
return {
...invalidResponseTemplate,
Expand All @@ -241,27 +251,40 @@ const validateInviteToPay = (flow: Record<string, any>): ValidationResponse => {
};
};

const inviteToPayEnabled = (flow: Record<string, any>): boolean => {
const payNodeStatuses = Object.entries(flow)
.filter(([_nodeId, nodeData]) => nodeData?.type === ComponentType.Pay)
?.map(([_nodeId, nodeData]) => nodeData?.data?.allowInviteToPay);
const inviteToPayEnabled = (flowGraph: FlowGraph): boolean => {
const payNodes = Object.entries(flowGraph).filter(
(entry): entry is [string, Node] =>
isComponentType(entry, ComponentType.Pay),
);
const payNodeStatuses = payNodes.map(
([_nodeId, node]) => node?.data?.allowInviteToPay,
);
return (
payNodeStatuses.length > 0 &&
payNodeStatuses.every((status) => status === true)
);
};

const isComponentType = (
entry: Entry<FlowGraph>,
type: ComponentType,
): entry is [string, Node] => {
const [nodeId, node] = entry;
if (nodeId === "_root") return false;
return Boolean(node?.type === type);
};

const hasComponentType = (
flow: Record<string, any>,
flowGraph: FlowGraph,
type: ComponentType,
fn?: string,
): boolean => {
const nodeIds = Object.entries(flow).filter(
([_nodeId, nodeData]) => nodeData?.type === type,
const nodeIds = Object.entries(flowGraph).filter(
(entry): entry is [string, Node] => isComponentType(entry, type),
);
if (fn) {
nodeIds
?.filter(([_nodeId, nodeData]) => nodeData?.data.fn === fn)
?.filter(([_nodeId, nodeData]) => nodeData?.data?.fn === fn)
?.map(([nodeId, _nodeData]) => nodeId);
} else {
nodeIds?.map(([nodeId, _nodeData]) => nodeId);
Expand All @@ -270,16 +293,16 @@ const hasComponentType = (
};

const numberOfComponentType = (
flow: Record<string, any>,
flowGraph: FlowGraph,
type: ComponentType,
fn?: string,
): number => {
const nodeIds = Object.entries(flow).filter(
([_nodeId, nodeData]) => nodeData?.type === type,
const nodeIds = Object.entries(flowGraph).filter(
(entry): entry is [string, Node] => isComponentType(entry, type),
);
if (fn) {
nodeIds
?.filter(([_nodeId, nodeData]) => nodeData?.data.fn === fn)
?.filter(([_nodeId, nodeData]) => nodeData?.data?.fn === fn)
?.map(([nodeId, _nodeData]) => nodeId);
} else {
nodeIds?.map(([nodeId, _nodeData]) => nodeId);
Expand Down
4 changes: 3 additions & 1 deletion api.planx.uk/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,9 @@ describe("dataMerged() function", () => {
});
it("handles multiple external portal nodes", async () => {
const result = await dataMerged("parent-id");
const nodeTypes = Object.values(result).map((node) => node.type);
const nodeTypes = Object.values(result).map((node) =>
"type" in node ? node.type : undefined,
);
const areAllPortalsFlattened = !nodeTypes.includes(
ComponentType.ExternalPortal,
);
Expand Down
7 changes: 5 additions & 2 deletions api.planx.uk/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { gql } from "graphql-request";
import { capitalize } from "lodash";
import { adminGraphQLClient as adminClient } from "./hasura";
import { Flow, Node } from "./types";
import { ComponentType } from "@opensystemslab/planx-core/types";
import { ComponentType, FlowGraph } from "@opensystemslab/planx-core/types";

// Get a flow's data (unflattened, without external portal nodes)
const getFlowData = async (id: string): Promise<Flow> => {
Expand Down Expand Up @@ -135,7 +135,10 @@ const getPublishedFlowByDate = async (id: string, created_at: string) => {
// Flatten a flow's data to include main content & portals in a single JSON representation
// XXX: getFlowData & dataMerged are currently repeated in ../editor.planx.uk/src/lib/dataMergedHotfix.ts
// in order to load frontend /preview routes for flows that are not published
const dataMerged = async (id: string, ob: Record<string, any> = {}) => {
const dataMerged = async (
id: string,
ob: FlowGraph = { _root: { edges: [] } },
): Promise<FlowGraph> => {
// get the primary flow data
const { slug, data } = await getFlowData(id);

Expand Down
1 change: 1 addition & 0 deletions api.planx.uk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"string-to-stream": "^3.0.1",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0",
"type-fest": "^4.6.0",
"zod": "^3.22.3"
},
"scripts": {
Expand Down
9 changes: 6 additions & 3 deletions api.planx.uk/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions api.planx.uk/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ export interface Node {
type?: number;
}

/**
* @deprecated Migrating to Flow and FlowGraph from planx-core
*/
export interface Flow {
id: string;
slug: string;
Expand Down

0 comments on commit e080d0e

Please sign in to comment.