Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
Warnings:

- You are about to drop the column `checkpoint_url` on the `test_flows` table. All the data in the column will be lost.
- You are about to drop the column `is_clipped` on the `test_flows` table. All the data in the column will be lost.
- You are about to drop the `test_flow_compositions` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `test_flow_steps` table. If the table is not empty, all the data it contains will be lost.

*/
-- DropForeignKey
ALTER TABLE "test_flow_compositions" DROP CONSTRAINT "test_flow_compositions_flow_id_fkey";

-- DropForeignKey
ALTER TABLE "test_flow_compositions" DROP CONSTRAINT "test_flow_compositions_step_id_fkey";

-- DropForeignKey
ALTER TABLE "test_flow_steps" DROP CONSTRAINT "test_flow_steps_crawl_session_id_fkey";

-- AlterTable
ALTER TABLE "test_flows" DROP COLUMN "checkpoint_url",
DROP COLUMN "is_clipped",
ADD COLUMN "transition_refs" TEXT[];

-- DropTable
DROP TABLE "test_flow_compositions";

-- DropTable
DROP TABLE "test_flow_steps";
2 changes: 1 addition & 1 deletion prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
37 changes: 3 additions & 34 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,6 @@ model CrawlSession {
regressionCodebase RegressionCodebase? @relation(fields: [regressionCodebaseId], references: [id], onDelete: SetNull)
schedule CrawlSchedule? @relation(fields: [scheduleId], references: [id], onDelete: SetNull)
testFlows TestFlow[]
testFlowSteps TestFlowStep[]

@@map("crawl_sessions")
}
Expand All @@ -224,46 +223,16 @@ model TestFlow {
appVersionId String @map("app_version_id") @db.Uuid
targetStateHash String @map("target_state_hash")
checkpointStateHash String @map("checkpoint_state_hash")
checkpointUrl String @map("checkpoint_url")
isClipped Boolean @map("is_clipped")

transitionRefs String[] @map("transition_refs")

stepCount Int @map("step_count")
createdAt DateTime @default(now()) @map("created_at")

crawlSession CrawlSession @relation(fields: [crawlSessionId], references: [id], onDelete: Cascade)
appVersion TargetApplicationVersion @relation(fields: [appVersionId], references: [id], onDelete: Cascade)
compositions TestFlowComposition[]

@@index([crawlSessionId])
@@index([appVersionId, targetStateHash])
@@map("test_flows")
}

model TestFlowStep {
id String @id @default(uuid()) @db.Uuid
crawlSessionId String @map("crawl_session_id") @db.Uuid
sourceStateHash String @map("source_state_hash")
targetStateHash String @map("target_state_hash")
actionType String @map("action_type")
actionFingerprint String @map("action_fingerprint")
transition Json

crawlSession CrawlSession @relation(fields: [crawlSessionId], references: [id], onDelete: Cascade)
compositions TestFlowComposition[]

@@unique([crawlSessionId, actionFingerprint])
@@map("test_flow_steps")
}

model TestFlowComposition {
id String @id @default(uuid()) @db.Uuid
flowId String @map("flow_id") @db.Uuid
stepId String @map("step_id") @db.Uuid
stepOrder Int @map("step_order")

flow TestFlow @relation(fields: [flowId], references: [id], onDelete: Cascade)
step TestFlowStep @relation(fields: [stepId], references: [id], onDelete: Cascade)

@@unique([flowId, stepOrder])
@@index([flowId])
@@map("test_flow_compositions")
}
8 changes: 1 addition & 7 deletions src/api/controllers/testFlow.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,9 @@ import { Request, Response, NextFunction } from "express";
import { z } from "@utils/zod";
import { saveAllFlows } from "@services/testFlow.service";

export const FlowStepSchema = z.object({
state_hash: z.string(),
transition: z.record(z.string(), z.unknown()).nullable(),
});

export const SerializedFlowSchema = z.object({

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Schemas should be a contract in the coverit-contracts and added to the models/ folder instead of creating them using zod.

checkpoint: z.string(),
is_clipped: z.boolean(),
path: z.array(FlowStepSchema),
transition_refs: z.array(z.string()),
});

export const SaveAllFlowsBodySchema = z.object({
Expand Down
129 changes: 17 additions & 112 deletions src/services/testFlow.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,13 @@
import prisma from "@lib/prisma";
import { logger } from "@services/logger.service";

interface FlowStep {
state_hash: string;
transition: Record<string, unknown> | null;
}

interface SerializedFlow {
checkpoint: string;
is_clipped: boolean;
path: FlowStep[];
transition_refs: string[];
}

export type AllFlowsPayload = Record<string, SerializedFlow[]>;

function resolveCheckpointUrl(flow: SerializedFlow): string {
const firstAction = flow.path[1];
return (firstAction?.transition?.checkpoint_url as string | undefined) ?? "";
}

export async function saveAllFlows(
sessionId: string,
payload: AllFlowsPayload,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SerializedFlow and AllFlowsPayload may be redundant after adding the contracts and just using the same contract you will use for the schema as a type here.

Expand All @@ -32,111 +21,27 @@ export async function saveAllFlows(
select: { appVersionId: true },
});

const stepsByFingerprint = new Map<string, {
sourceStateHash: string;
targetStateHash: string;
actionType: string;
actionFingerprint: string;
transition: Record<string, unknown>;
}>();

for (const flows of Object.values(payload)) {
for (const flow of flows) {
for (let i = 1; i < flow.path.length; i++) {
const step = flow.path[i];
const prev = flow.path[i - 1];
const t = step.transition;

if (!t) continue;

const fingerprint = t.action_fingerprint as string | undefined;
if (!fingerprint || stepsByFingerprint.has(fingerprint)) continue;

stepsByFingerprint.set(fingerprint, {
sourceStateHash: prev.state_hash,
targetStateHash: step.state_hash,
actionType: (t.action_type as string) ?? "",
actionFingerprint: fingerprint,
transition: t,
});
}
}
}

const stepsToInsert = Array.from(stepsByFingerprint.values()).map((s) => ({
crawlSessionId: sessionId,
sourceStateHash: s.sourceStateHash,
targetStateHash: s.targetStateHash,
actionType: s.actionType,
actionFingerprint: s.actionFingerprint,
transition: s.transition as any,
}));

if (stepsToInsert.length > 0) {
await prisma.testFlowStep.createMany({
data: stepsToInsert,
skipDuplicates: true,
});
}

const persistedSteps = await prisma.testFlowStep.findMany({
where: { crawlSessionId: sessionId },
select: { id: true, actionFingerprint: true },
const flowsToInsert = Object.entries(payload).flatMap(([targetStateHash, flows]) => {
return flows.map((flow) => ({
crawlSessionId: sessionId,
appVersionId: session.appVersionId,
targetStateHash,
checkpointStateHash: flow.checkpoint,
transitionRefs: flow.transition_refs,
stepCount: flow.transition_refs.length,
}));
});

const stepIdByFingerprint = new Map(
persistedSteps.map((s) => [s.actionFingerprint, s.id]),
);

const allDbOperations: Array<ReturnType<typeof prisma.testFlow.create>> = [];

for (const [targetStateHash, flows] of Object.entries(payload)) {
if (flows.length === 0) continue;

for (const flow of flows) {
const compositionSteps: { stepId: string; stepOrder: number }[] = [];

for (let i = 1; i < flow.path.length; i++) {
const step = flow.path[i];
const fingerprint = step.transition?.action_fingerprint as string | undefined;

if (fingerprint) {
const stepId = stepIdByFingerprint.get(fingerprint);
if (stepId) {
compositionSteps.push({
stepId,
stepOrder: i,
});
}
}
}

allDbOperations.push(
prisma.testFlow.create({
data: {
crawlSessionId: sessionId,
appVersionId: session.appVersionId,
targetStateHash,
checkpointStateHash: flow.checkpoint,
checkpointUrl: resolveCheckpointUrl(flow),
isClipped: flow.is_clipped,
stepCount: flow.path.length,
compositions: {
create: compositionSteps,
},
},
})
);
}
if (flowsToInsert.length === 0) {
logger.info(`No flows to save for session ${sessionId}`);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be an error and add a constants file for the errors/messages.

return;
}

if (allDbOperations.length > 0) {
await prisma.$transaction(allDbOperations);
}
await prisma.testFlow.createMany({
data: flowsToInsert,
});

logger.info(
`Saved flows for session ${sessionId}: ` +
`${stepsByFingerprint.size} unique steps, ` +
`${Object.values(payload).flat().length} total flows`,
`Saved flows for session ${sessionId}: ${flowsToInsert.length} total flows saved in bulk.`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return a Promise<MessageResponse> and add types to the functions declaration.

);
}
Loading