diff --git a/prisma/migrations/20260614065220_minimal_test_flow_setup/migration.sql b/prisma/migrations/20260614065220_minimal_test_flow_setup/migration.sql new file mode 100644 index 0000000..f44a463 --- /dev/null +++ b/prisma/migrations/20260614065220_minimal_test_flow_setup/migration.sql @@ -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"; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml index 99e4f20..044d57c 100644 --- a/prisma/migrations/migration_lock.toml +++ b/prisma/migrations/migration_lock.toml @@ -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" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 19474c7..6e4c2ce 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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") } @@ -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") -} diff --git a/src/api/controllers/testFlow.controller.ts b/src/api/controllers/testFlow.controller.ts index c45b1fd..cd3a061 100644 --- a/src/api/controllers/testFlow.controller.ts +++ b/src/api/controllers/testFlow.controller.ts @@ -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({ checkpoint: z.string(), - is_clipped: z.boolean(), - path: z.array(FlowStepSchema), + transition_refs: z.array(z.string()), }); export const SaveAllFlowsBodySchema = z.object({ diff --git a/src/services/testFlow.service.ts b/src/services/testFlow.service.ts index 62215e9..181a801 100644 --- a/src/services/testFlow.service.ts +++ b/src/services/testFlow.service.ts @@ -5,24 +5,13 @@ import prisma from "@lib/prisma"; import { logger } from "@services/logger.service"; -interface FlowStep { - state_hash: string; - transition: Record | null; -} - interface SerializedFlow { checkpoint: string; - is_clipped: boolean; - path: FlowStep[]; + transition_refs: string[]; } export type AllFlowsPayload = Record; -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, @@ -32,111 +21,27 @@ export async function saveAllFlows( select: { appVersionId: true }, }); - const stepsByFingerprint = new Map; - }>(); - - 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> = []; - - 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}`); + 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.`, ); } \ No newline at end of file