Skip to content

Commit

Permalink
new flows/ routes
Browse files Browse the repository at this point in the history
  • Loading branch information
jessicamcinchak committed Feb 5, 2025
1 parent 9670291 commit eebd986
Show file tree
Hide file tree
Showing 9 changed files with 512 additions and 57 deletions.
27 changes: 16 additions & 11 deletions api.planx.uk/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { userContext } from "./modules/auth/middleware.js";
import { publishFlow } from "./modules/flows/publish/service.js";
import type { Flow, Node } from "./types.js";

export interface FlowData {
export interface GetFlowDataResponse {
slug: string;
name: string;
data: Flow["data"];
Expand All @@ -25,9 +25,11 @@ export interface FlowData {
| [];
}

// Get a flow's data (unflattened, without external portal nodes)
const getFlowData = async (id: string): Promise<FlowData> => {
const { flow } = await $public.client.request<{ flow: FlowData | null }>(
// Get a flow's data (unflattened)
const getFlowData = async (id: string): Promise<GetFlowDataResponse> => {
const { flow } = await $public.client.request<{
flow: GetFlowDataResponse | null;
}>(
gql`
query GetFlowData($id: uuid!) {
flow: flows_by_pk(id: $id) {
Expand Down Expand Up @@ -58,9 +60,9 @@ const getFlowData = async (id: string): Promise<FlowData> => {
return flow;
};

interface InsertFlow {
interface CreateFlowResponse {
flow: {
id: string;
id: Flow["id"];
};
}

Expand All @@ -71,21 +73,23 @@ const createFlow = async (
name: string,
flowData: Flow["data"],
copiedFrom?: Flow["id"],
templatedFrom?: Flow["id"],
) => {
const { client: $client } = getClient();
const userId = userContext.getStore()?.user?.sub;

try {
const {
flow: { id },
} = await $client.request<InsertFlow>(
} = await $client.request<CreateFlowResponse>(
gql`
mutation InsertFlow(
$team_id: Int!
$slug: String!
$name: String!
$data: jsonb = {}
$copied_from: uuid
$templated_from: uuid
) {
flow: insert_flows_one(
object: {
Expand All @@ -95,6 +99,7 @@ const createFlow = async (
data: $data
version: 1
copied_from: $copied_from
templated_from: $templated_from
}
) {
id
Expand All @@ -107,6 +112,7 @@ const createFlow = async (
name: name,
data: flowData,
copied_from: copiedFrom,
templated_from: templatedFrom,
},
);

Expand Down Expand Up @@ -142,10 +148,9 @@ const createAssociatedOperation = async (flowId: Flow["id"]) => {
return data?.operation;
};

interface PublishedFlows {
interface PublishedFlowsResponse {
flow: {
publishedFlows: {
// TODO: use FlowGraph from planx-core here
data: Flow["data"];
id: number;
}[];
Expand All @@ -156,7 +161,7 @@ interface PublishedFlows {
const getMostRecentPublishedFlow = async (
id: string,
): Promise<Flow["data"] | undefined> => {
const { flow } = await $public.client.request<PublishedFlows>(
const { flow } = await $public.client.request<PublishedFlowsResponse>(
gql`
query GetMostRecentPublishedFlow($id: uuid!) {
flow: flows_by_pk(id: $id) {
Expand All @@ -179,7 +184,7 @@ const getMostRecentPublishedFlow = async (
const getMostRecentPublishedFlowVersion = async (
id: string,
): Promise<number | undefined> => {
const { flow } = await $public.client.request<PublishedFlows>(
const { flow } = await $public.client.request<PublishedFlowsResponse>(
gql`
query GetMostRecentPublishedFlowVersion($id: uuid!) {
flow: flows_by_pk(id: $id) {
Expand Down
48 changes: 48 additions & 0 deletions api.planx.uk/modules/flows/createFlow/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { z } from "zod";
import type { ValidatedRequestHandler } from "../../../shared/middleware/validate.js";
import { ServerError } from "../../../errors/index.js";
import { createFlow } from "../../../helpers.js";
import type { Flow } from "../../../types.js";

interface CreateFlowResponse {
message: string;
id: Flow["id"];
slug: Flow["slug"];
}

export const createFlowSchema = z.object({
body: z.object({
teamId: z.number(),
slug: z.string(),
name: z.string(),
data: z.any(), // FlowGraph
}),
});

export type CreateFlowController = ValidatedRequestHandler<
typeof createFlowSchema,
CreateFlowResponse
>;

export const createFlowController: CreateFlowController = async (
_req,
res,
next,
) => {
try {
const { teamId, slug, name, data } = res.locals.parsedReq.body;

// createFlow automatically handles the associated operation and initial publish
const { id } = await createFlow(teamId, slug, name, data);

res.status(200).send({
message: `Successfully created flow ${slug}`,
id,
slug,
});
} catch (error) {
return next(
new ServerError({ message: `Failed to create flow. Error: ${error}` }),
);
}
};
157 changes: 157 additions & 0 deletions api.planx.uk/modules/flows/createFlow/createFlow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import supertest from "supertest";

import { queryMock } from "../../../tests/graphqlQueryMock.js";
import { authHeader } from "../../../tests/mockJWT.js";
import app from "../../../server.js";
import type { Flow } from "../../../types.js";

const mockNewFlowData: Flow["data"] = {
_root: {
edges: [],
},
};

const validBody = {
teamId: 1,
slug: "my-new-flow",
name: "My new flow",
data: mockNewFlowData,
};

const invalidBody = {
slug: "my-new-flow",
data: mockNewFlowData,
};

beforeEach(() => {
queryMock.mockQuery({
name: "GetFlowData",
matchOnVariables: false,
data: {
flow: {
data: mockNewFlowData,
},
},
});

queryMock.mockQuery({
name: "InsertFlow",
matchOnVariables: false,
data: {
flow: {
id: "2",
},
},
});

queryMock.mockQuery({
name: "InsertOperation",
matchOnVariables: false,
data: {
operation: {
id: 1,
},
},
});

queryMock.mockQuery({
name: "PublishFlow",
matchOnVariables: false,
data: {
publishedFlow: {
data: mockNewFlowData,
},
},
});

queryMock.mockQuery({
name: "GetMostRecentPublishedFlow",
matchOnVariables: false,
data: {
flow: {
publishedFlows: [
{
data: mockNewFlowData,
},
],
},
},
});
});

const auth = authHeader({ role: "teamEditor" });

it("returns an error if authorization headers are not set", async () => {
await supertest(app)
.post("/flows/create")
.send(validBody)
.expect(401)
.then((res) => {
expect(res.body).toEqual({
error: "No authorization token was found",
});
});
});

it("returns an error if the user does not have the correct role", async () => {
await supertest(app)
.post("/flows/create")
.send(validBody)
.set(authHeader({ role: "teamViewer" }))
.expect(403);
});

it("returns an error if required properties are missing in the request body", async () => {
await supertest(app)
.post("/flows/create")
.send(invalidBody)
.set(auth)
.expect(400)
.then((res) => {
expect(res.body).toHaveProperty("issues");
expect(res.body).toHaveProperty("name", "ZodError");
});
});

it("returns an error when the service errors", async () => {
queryMock.reset();
queryMock.mockQuery({
name: "InsertFlow",
variables: {
team_id: 1,
slug: "my-new-flow",
name: "My new flow",
data: mockNewFlowData,
},
data: {},
graphqlErrors: [
{
message: "Something went wrong",
},
],
});

await supertest(app)
.post("/flows/create")
.send(validBody)
.set(auth)
.expect(500)
.then((res) => {
expect(res.body.error).toMatch(/Failed to create flow/);
});
});

it("successfully creates a new flow", async () => {
await supertest(app)
.post("/flows/create")
.send(validBody)
.set(auth)
.expect(200)
.then((res) => {
expect(res.body).toEqual({
message: `Successfully created flow ${validBody.slug}`,
id: "2",
slug: validBody.slug,
});
});
});
47 changes: 47 additions & 0 deletions api.planx.uk/modules/flows/createFlowFromTemplate/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { z } from "zod";
import type { ValidatedRequestHandler } from "../../../shared/middleware/validate.js";
import { ServerError } from "../../../errors/index.js";
import type { Flow } from "../../../types.js";
import { createFlowFromTemplate } from "./service.js";

interface CreateFlowFromTemplateResponse {
message: string;
id: Flow["id"];
slug: Flow["slug"];
}

export const createFlowFromTemplateSchema = z.object({
params: z.object({
templateId: z.string(),
}),
body: z.object({
teamId: z.number(),
}),
});

export type CreateFlowFromTemplateController = ValidatedRequestHandler<
typeof createFlowFromTemplateSchema,
CreateFlowFromTemplateResponse
>;

export const createFlowFromTemplateController: CreateFlowFromTemplateController =
async (_req, res, next) => {
try {
const { templateId } = res.locals.parsedReq.params;
const { teamId } = res.locals.parsedReq.body;

const { id, slug } = await createFlowFromTemplate(templateId, teamId);

res.status(200).send({
message: `Successfully created flow from template ${slug}`,
id,
slug,
});
} catch (error) {
return next(
new ServerError({
message: `Failed to create flow from template. Error: ${error}`,
}),
);
}
};
Loading

0 comments on commit eebd986

Please sign in to comment.