codeflash-internal/js/cf-api/endpoints/code-context-hash.ts

233 lines
7.7 KiB
TypeScript
Raw Normal View History

2025-05-29 23:06:27 +00:00
import { prisma } from "@codeflash-ai/common"
2025-04-28 02:15:34 +00:00
import { Request, Response } from "express"
import { githubApp } from "../github/github-app.js"
import { getInstallationOctokitByOwner, isUserCollaborator } from "../github/github-utils.js"
import { userNickname } from "../auth0-mgmt.js"
2025-06-14 23:48:40 +00:00
import { posthog } from "../analytics.js"
import { logger } from "../utils/logger.js"
2026-01-19 17:33:57 +00:00
import {
missingRequiredFields,
unauthorized,
githubNotCollaborator,
validationFailure,
internalServerError,
conflict,
} from "../exceptions/index.js"
2025-04-28 02:15:34 +00:00
export async function is_code_being_optimized_again(req: Request, res: Response) {
2025-04-28 02:15:34 +00:00
try {
2025-06-02 23:05:17 +00:00
const { owner, repo, pr_number, code_contexts } = req.body
const userId = (req as any).userId
2025-04-28 02:15:34 +00:00
2025-06-02 23:05:17 +00:00
if (!repo || !owner || !pr_number || !code_contexts) {
2026-01-19 17:33:57 +00:00
throw missingRequiredFields("repo, owner, pr_number, code_contexts")
2025-04-28 02:15:34 +00:00
}
const nickname: string | null = await userNickname(userId)
if (nickname == null) {
2026-01-19 17:33:57 +00:00
throw unauthorized("")
}
const octokit = await getInstallationOctokitByOwner(githubApp, owner, repo, userId)
if (octokit instanceof Error) {
2026-01-19 17:33:57 +00:00
throw internalServerError(octokit.message)
}
// Check collaborator status with error handling
try {
2025-06-07 00:31:52 +00:00
const isCollaborator = await isUserCollaborator(octokit, owner, repo, nickname)
if (!isCollaborator) {
logger.warn("User is not a collaborator on repository", req, {
nickname,
repo: `${owner}/${repo}`,
})
2026-01-19 17:33:57 +00:00
throw githubNotCollaborator(`${owner}/${repo}`)
}
} catch (error) {
2026-01-19 17:33:57 +00:00
if (error && typeof error === "object" && "getHttpStatus" in error) {
throw error
}
logger.error("Error checking collaborator status", req, {}, error as Error)
2026-01-19 17:33:57 +00:00
throw internalServerError("Failed to verify collaborator status")
}
2025-04-28 02:15:34 +00:00
// Validate pr_number is an integer above 0
const pr_number_int = parseInt(pr_number, 10)
if (isNaN(pr_number_int) || pr_number_int <= 0) {
2026-01-19 17:33:57 +00:00
throw validationFailure("pr_number must be a positive integer")
2025-04-28 02:15:34 +00:00
}
2025-06-07 04:00:06 +00:00
// Validate code_contexts is an array
if (!Array.isArray(code_contexts)) {
2026-01-19 17:33:57 +00:00
throw validationFailure("code_contexts must be an array of objects")
2025-06-02 23:05:17 +00:00
}
2025-06-07 04:00:06 +00:00
if (code_contexts.length === 0) {
2026-01-19 17:33:57 +00:00
throw validationFailure("code_contexts cannot be empty")
2025-06-02 23:05:17 +00:00
}
2025-06-07 04:00:06 +00:00
// Validate each code context object has required fields
for (let i = 0; i < code_contexts.length; i++) {
const context = code_contexts[i]
if (typeof context !== "object" || context === null) {
2026-01-19 17:33:57 +00:00
throw validationFailure(`code_contexts[${i}] must be an object`)
2025-06-07 04:00:06 +00:00
}
const { file_path, function_name, code_hash } = context
if (typeof file_path !== "string" || file_path.trim().length === 0) {
2026-01-19 17:33:57 +00:00
throw validationFailure(`code_contexts[${i}].file_path must be a non-empty string`)
2025-06-07 04:00:06 +00:00
}
if (typeof function_name !== "string" || function_name.trim().length === 0) {
2026-01-19 17:33:57 +00:00
throw validationFailure(`code_contexts[${i}].function_name must be a non-empty string`)
2025-06-07 04:00:06 +00:00
}
if (typeof code_hash !== "string" || !/^[a-f0-9]{64}$/.test(code_hash)) {
2026-01-19 17:33:57 +00:00
throw validationFailure(`code_contexts[${i}].code_hash must be a valid SHA-256 hash`)
2025-06-07 04:00:06 +00:00
}
2025-04-28 02:15:34 +00:00
}
2025-06-02 23:05:17 +00:00
// Build the compound keys for batch querying
2025-06-07 04:00:06 +00:00
const compoundKeys = code_contexts.map(({ code_hash }) => ({
2025-06-02 23:05:17 +00:00
repo,
owner,
2025-06-07 04:00:06 +00:00
pr_number: pr_number_int,
context_hash: code_hash,
2025-06-02 23:05:17 +00:00
}))
const existingRecords = await prisma.pr_code_context_hash_cache.findMany({
2025-04-28 02:15:34 +00:00
where: {
2025-06-07 00:31:52 +00:00
OR: compoundKeys.map(key => ({
2025-06-02 23:05:17 +00:00
repo: key.repo,
owner: key.owner,
pr_number: key.pr_number,
context_hash: key.context_hash,
})),
2025-04-28 02:15:34 +00:00
},
2025-06-02 23:05:17 +00:00
select: {
context_hash: true,
},
})
// Create a Set of existing hashes for O(1) lookup
const existingHashes = new Set(existingRecords.map(record => record.context_hash))
2025-06-07 04:00:06 +00:00
// Identify code contexts that are already optimized (return as tuples)
const alreadyOptimizedTuples = code_contexts
.filter(({ code_hash }) => existingHashes.has(code_hash))
.map(({ file_path, function_name }) => [file_path, function_name])
posthog?.capture({
2025-06-14 23:48:40 +00:00
distinctId: userId,
event: `cfapi-github-pr-optimization`,
properties: {
2025-06-15 00:05:56 +00:00
repo_owner: owner,
repo_name: repo,
pr_number: pr_number,
2025-06-14 23:48:40 +00:00
},
})
2025-06-07 04:00:06 +00:00
logger.info("Code context check completed", req, {
repo: `${owner}/${repo}`,
pr_number,
total_contexts: code_contexts.length,
already_optimized: alreadyOptimizedTuples.length,
new_contexts: code_contexts.length - alreadyOptimizedTuples.length,
})
2025-06-07 04:00:06 +00:00
res.status(200).json({
already_optimized_tuples: alreadyOptimizedTuples,
total_contexts: code_contexts.length,
new_contexts: code_contexts.length - alreadyOptimizedTuples.length,
})
} catch (error) {
2026-01-19 17:33:57 +00:00
if (error && typeof error === "object" && "getHttpStatus" in error) {
throw error
}
logger.errorWithSentry("Error in is_code_being_optimized_again", req, {}, error as Error)
2026-01-19 17:33:57 +00:00
throw internalServerError("Error checking code optimization status")
2025-06-07 04:00:06 +00:00
}
}
export async function add_optimized_code_context(req: Request, res: Response) {
try {
const { owner, repo, pr_number, code_hash } = req.body
const userId = (req as any).userId
if (!repo || !owner || !pr_number || !code_hash) {
2026-01-19 17:33:57 +00:00
throw missingRequiredFields("repo, owner, pr_number, code_hash")
2025-06-07 04:00:06 +00:00
}
const nickname: string | null = await userNickname(userId)
if (nickname == null) {
2026-01-19 17:33:57 +00:00
throw unauthorized("")
2025-06-07 04:00:06 +00:00
}
const octokit = await getInstallationOctokitByOwner(githubApp, owner, repo, userId)
2025-06-07 04:00:06 +00:00
if (octokit instanceof Error) {
2026-01-19 17:33:57 +00:00
throw internalServerError(octokit.message)
2025-06-07 04:00:06 +00:00
}
// Check collaborator status with error handling
try {
const isCollaborator = await isUserCollaborator(octokit, owner, repo, nickname)
if (!isCollaborator) {
logger.warn("User is not a collaborator on repository", req, {
nickname,
repo: `${owner}/${repo}`,
})
2026-01-19 17:33:57 +00:00
throw githubNotCollaborator(`${owner}/${repo}`)
2025-06-07 04:00:06 +00:00
}
} catch (error) {
2026-01-19 17:33:57 +00:00
if (error && typeof error === "object" && "getHttpStatus" in error) {
throw error
}
logger.error("Error checking collaborator status", req, {}, error as Error)
2026-01-19 17:33:57 +00:00
throw internalServerError("Failed to verify collaborator status")
2025-06-07 04:00:06 +00:00
}
2025-06-07 00:31:52 +00:00
2025-06-07 04:00:06 +00:00
// Validate pr_number is an integer above 0
const pr_number_int = parseInt(pr_number, 10)
if (isNaN(pr_number_int) || pr_number_int <= 0) {
2026-01-19 17:33:57 +00:00
throw validationFailure("pr_number must be a positive integer")
2025-06-07 04:00:06 +00:00
}
// Validate code_hash is a valid SHA-256 hash
if (typeof code_hash !== "string" || !/^[a-f0-9]{64}$/.test(code_hash)) {
2026-01-19 17:33:57 +00:00
throw validationFailure("code_hash must be a valid SHA-256 hash")
2025-06-07 04:00:06 +00:00
}
2025-06-02 23:05:17 +00:00
2025-06-07 04:00:06 +00:00
// Create the new entry
await prisma.pr_code_context_hash_cache.create({
data: {
2025-04-28 02:15:34 +00:00
repo,
owner,
2025-06-07 04:00:06 +00:00
pr_number: pr_number_int,
context_hash: code_hash,
},
})
2025-06-02 23:05:17 +00:00
logger.info("Code context successfully added", req, {
repo: `${owner}/${repo}`,
pr_number,
code_hash,
})
2025-06-07 04:00:06 +00:00
res.status(201).json({
message: "Code context successfully added",
2025-06-02 23:05:17 +00:00
})
2026-01-19 17:33:57 +00:00
} catch (error: any) {
if (error && typeof error === "object" && "getHttpStatus" in error) {
throw error
}
logger.errorWithSentry("Error in add_optimized_code_context", req, {}, error as Error)
2025-06-07 04:00:06 +00:00
// Handle unique constraint violations gracefully
if (error.code === "P2002") {
logger.warn("Code context already exists for this PR", req, {})
2026-01-19 17:33:57 +00:00
throw conflict("Code context already exists for this PR")
2025-06-07 04:00:06 +00:00
}
2026-01-19 17:33:57 +00:00
throw internalServerError("Error adding code context")
2025-04-28 02:15:34 +00:00
}
}