From Compliance Data to GitHub PRs: Navigating Type-Safety and Robust Exports
Join us as we recount building a critical compliance report export feature, from assembling structured Markdown to creating GitHub Pull Requests, all while tackling tricky TypeScript challenges with Prisma's dynamic Json types.
Just wrapped up a particularly satisfying development sprint! The goal: baking a robust compliance report export feature directly into our workflow analysis tool. This wasn't just about dumping data; it was about transforming complex analysis results into human-readable Markdown, offering flexible export options (download or GitHub PR!), and ensuring everything played nicely with our existing stack.
Let's dive into the journey, the wins, and the crucial lessons learned along the way.
The Mission: Exporting Clarity
Our compliance analysis workflow generates a wealth of data – executive metrics, detailed step-by-step breakdowns, quality gate checks, and even sophisticated hallucination and consistency analyses. The challenge was to consolidate this into a single, comprehensive report that could be easily shared and version-controlled.
Our solution involved three key pieces:
-
The Data Alchemist:
compliance-report-formatter.tsThis new service acts as our data alchemist. It takes the raw, structured output from our compliance workflow steps and meticulously crafts it into a well-formatted Markdown document. Think of it as a custom templating engine. It assembles sections for executive metrics, individual workflow steps, quality gates, and the results of our hallucination and consistency analyses. The goal here was pure data assembly, ensuring the output was consistent, readable, and ready for consumption. -
The Backend Gateway:
exportComplianceReportMutation On the backend, we exposed a newexportComplianceReportmutation via ourtRPCrouter (src/server/trpc/routers/workflows.ts). This mutation is the brain of the operation, offering dual functionality:- Direct Download: For quick local review, users can simply download the generated Markdown file.
- GitHub PR Creation: This is where things get really powerful. We integrated with our existing
github-connector.tsto allow users to automatically create a new branch, commit the compliance report, and open a Pull Request in a linked GitHub repository. This is a game-changer for audit trails and collaborative review processes.
-
The User Interface: Compliance Export Panel No feature is complete without a user-friendly interface. We added an expandable card to our workflow details page (
src/app/(dashboard)/dashboard/workflows/[id]/page.tsx). Following our establishedBundleExportPaneldesign pattern, it provides a clean interface with a crucial toggle: "Create GitHub PR." This allows users to choose their preferred export method intuitively.
Hardening the Edges: Lessons from Code Review
A significant part of this sprint involved addressing critical and high-priority issues surfaced during code review. This wasn't just about fixing bugs; it was about making the feature more robust, secure, and maintainable.
- Markdown Sanitization with
escapeTableCell(): When generating Markdown tables, special characters like pipes (|) and newlines can wreak havoc. We implementedescapeTableCell()to properly sanitize cell content, preventing malformed tables and potential Markdown injection issues. A small detail, but crucial for data integrity. - Idempotent GitHub API Calls: Interacting with external APIs always requires careful consideration. For GitHub PR creation, we made the process idempotent. This means if a user tries to create a PR for the same report twice, our system won't error out or create duplicate content. We achieved this by:
- Handling
422(Unprocessable Entity) for existing branches: If the target branch already exists, we gracefully handle it. - Using
getFileShafor updates: Instead of always creating new files, we fetch the existing file's SHA (if it exists) to perform an update, ensuring changes are committed to the same file.
- Handling
- Graceful Error Handling with
TRPCError: We wrapped all internal GitHub API errors withinTRPCErrorinstances. This prevents sensitive internal API details from leaking to the client and allows us to present user-friendly error messages, improving the overall user experience and security. - Runtime Validation for Prisma Json Fields: Our Prisma schema includes
Jsonfields, which are notoriously flexible (and sometimes tricky with TypeScript). We added robust runtime validation for ourdocRefsfield, both on the server and client, ensuring that the dynamic JSON data conforms to our expected structure before we try to process it. - Safety First: Guarding Against
null/undefined: We replaced several non-null assertions (!) with explicit safe guards in our consistency and hallucination analysis sections. This makes the code more resilient to unexpectednullorundefinedvalues, reducing potential runtime errors. - Clarity in Conditional Rendering: For better readability and maintainability, we extracted the logic for determining if a workflow is a "compliance workflow" into a clear
isComplianceWorkflowvariable, simplifying complex JSX conditional rendering.
The "Aha!" Moment: Taming Prisma's Json Types
One of the most head-scratching moments came when dealing with Prisma's Json type and TypeScript's strictness. Our docRefs field in Prisma is defined as Json, which TypeScript interprets as Prisma.JsonValue.
The Problem:
Prisma.JsonValue is a union type: string | number | boolean | Prisma.JsonObject | Prisma.JsonArray | null. When you retrieve an element from a Prisma.JsonArray, TypeScript only knows it's a Prisma.JsonValue. We wanted to access properties like owner on these elements, assuming they were objects.
// This failed with TS2339: Property 'owner' does not exist on type 'JsonObject | JsonArray'
const docRef = docRefs[0]; // docRef is Prisma.JsonValue
if (typeof docRef === 'object' && docRef !== null && 'owner' in docRef) {
// ... still not happy, 'owner' might not be a property of all JsonObjects
// and direct property access on JsonObject is not allowed by default.
}
Directly casting docRef as { owner: string } felt too aggressive without proper checks.
The Solution (and recurring pattern):
The reliable workaround involves an intermediate cast to Record<string, unknown> before performing property access within a type guard. This tells TypeScript, "Hey, treat this as a generic object for a moment, so I can check its properties safely."
// The robust workaround
type DocRef = {
owner: string;
// ... other properties
};
function isDocRef(obj: unknown): obj is DocRef {
// First, safely cast to a generic object type
const potentialDocRef = obj as Record<string, unknown>;
return (
typeof potentialDocRef === 'object' &&
potentialDocRef !== null &&
typeof potentialDocRef.owner === 'string'
// ... add checks for other required properties
);
}
// Usage:
const docRefs = // ... fetched from Prisma, type Prisma.JsonArray
if (Array.isArray(docRefs)) {
const validDocRefs = docRefs.filter(isDocRef);
// Now, validDocRefs is an array of DocRef, and we can safely access .owner
}
This pattern has become our go-to for dealing with dynamic Json fields in Prisma and TypeScript. It ensures type safety without sacrificing the flexibility of JSON storage.
What's Next?
With the feature now complete and all critical code review feedback addressed, the immediate next steps are:
- Commit and Deploy: Get these changes into
main! - End-to-End Testing: Thoroughly test the workflow from completion to report download, and especially the GitHub PR creation.
- Refactoring for Maintainability: That 2164-line page file is screaming for attention! Extracting the compliance export panel into its own component (
src/components/workflow/compliance-export-panel.tsx) is high on the list. - Audit Logging: Consider adding audit logging for GitHub PR creation actions, providing a clear trail of who did what and when.
This sprint was a great example of how combining feature development with rigorous code review and a pragmatic approach to type-safety leads to a much stronger, more reliable product. Onwards!