Function bodies 169 total
POST function · javascript · L6-L21 (16 LOC)src/app/api/articles/summarize/route.js
export async function POST(req) {
try {
const { content } = await req.json();
// Use the utility function to summarize the article
const summary = await summarizeArticle(content);
return NextResponse.json({ summary });
} catch (error) {
console.error("Error calling OpenAI API:", error);
return NextResponse.json(
{ message: "Failed to summarize article" },
{ status: 500 }
);
}
}POST function · javascript · L6-L28 (23 LOC)src/app/api/articles/unassign/route.js
export async function POST(request) {
const adminCheck = requireAdmin(request);
if (adminCheck instanceof NextResponse) return adminCheck;
try {
const { articleId, editorId } = await request.json();
await query(
`DELETE FROM article_assignments WHERE article_id = $1 AND editor_id = $2`,
[articleId, editorId]
);
return NextResponse.json({
message: "Article unassigned successfully",
});
} catch (error) {
console.error("Error unassigning article:", error);
return NextResponse.json(
{ message: "Error unassigning article" },
{ status: 500 }
);
}
}POST function · javascript · L7-L41 (35 LOC)src/app/api/auth/create-editor/route.js
export async function POST(request) {
// 1) block non‑admins
const guard = requireAdmin(request);
if (guard instanceof NextResponse) return guard;
// 2) pull the new user data + role
const { firstName, lastName, email, password } = await request.json();
if (!firstName || !lastName || !email || !password ) {
return NextResponse.json({ message: "Missing fields" }, { status: 400 });
}
// 3) hash & insert
const passwordHash = await bcrypt.hash(password, 10);
const insertUser = await query(
`INSERT INTO email_credentials
(first_name, last_name, email, password_hash, role)
VALUES ($1,$2,$3,$4,$5)
RETURNING id, email`,
[firstName, lastName, email.toLowerCase(), passwordHash, "editor"]
);
const userId = insertUser.rows[0].id;
// 4) profile table
await query(
`INSERT INTO profile (user_id, name, email, photo, bio)
VALUES ($1,$2,$3,$4,$5)`,
[userId, `${firstName} ${lastName}`, email.toLowerCase(), null, null]
);
// 5) POST function · javascript · L8-L92 (85 LOC)src/app/api/auth/login/route.js
export async function POST(req) {
try {
let { email, password } = await req.json();
// Get user from the database
email = email.toLowerCase(); // ← normalize input
// Now compare against a lower‑cased column value
const userResult = await query(
`SELECT *
FROM email_credentials
WHERE LOWER(email) = $1`,
[email]
);
if (userResult.rows.length === 0) {
return NextResponse.json(
{ message: "Invalid credentials" },
{ status: 401 }
);
}
const user = userResult.rows[0];
const passwordMatch = await bcrypt.compare(
password,
user.password_hash
);
if (!passwordMatch) {
return NextResponse.json(
{ message: "Invalid credentials" },
{ status: 401 }
);
}
// Check if user is admin
POST function · javascript · L4-L23 (20 LOC)src/app/api/auth/logout/route.js
export async function POST() {
try {
// Clear the auth cookie by setting it with an expired date
const response = NextResponse.json({ message: "Logout successful" });
// Set the cookie to expire immediately
response.headers.set(
"Set-Cookie",
"auth=; Max-Age=0; path=/; HttpOnly; secure; SameSite=strict"
);
return response;
} catch (error) {
console.error("Logout error:", error);
return NextResponse.json(
{ message: "Error during logout" },
{ status: 500 }
);
}
}GET function · javascript · L5-L33 (29 LOC)src/app/api/auth/session/route.js
export async function GET(req) {
const token = req.cookies.get("auth")?.value; // Get the token from cookies
if (!token) {
return NextResponse.json({ isLoggedIn: false }, { status: 200 });
}
try {
const decoded = verify(
token,
process.env.JWT_SECRET || "your-secret-key"
); // Verify the token
return NextResponse.json(
{
isLoggedIn: true,
email: decoded.email,
userId: decoded.id,
name: decoded.name,
isAdmin: decoded.isAdmin,
role: decoded.role,
},
{ status: 200 }
);
} catch (error) {
console.error("Session verification error:", error);
return NextResponse.json({ isLoggedIn: false }, { status: 200 });
}
}POST function · javascript · L8-L76 (69 LOC)src/app/api/auth/signup/route.js
export async function POST(req) {
try {
const { firstName, lastName, email, password } = await req.json();
// Hash password
const passwordHash = await bcrypt.hash(password, 10);
// Insert user into the email_credentials table
const result = await query(
`INSERT INTO email_credentials (first_name, last_name, email, password_hash)
VALUES ($1, $2, $3, $4)
RETURNING id, email`,
[firstName, lastName, email, passwordHash]
);
const userId = result.rows[0].id; // Get the ID of the newly created user
const userEmail = result.rows[0].email; // Get the email of the newly created user
// Create a blank profile for the user
await query(
`INSERT INTO profile (user_id, name, email, photo, bio)
VALUES ($1, $2, $3, $4, $5)`,
[userId, `${firstName} ${lastName}`, userEmail, null, null] // Include email
);
Repobility · open methodology · https://repobility.com/research/
POST function · javascript · L5-L52 (48 LOC)src/app/api/auth/update-password/route.js
export async function POST(req) {
try {
const { currentPassword, newPassword, email } = await req.json();
// Get user from the database (replace with session user info)
const userResult = await query(
"SELECT * FROM email_credentials WHERE email = $1",
[email]
);
if (userResult.rows.length === 0) {
return NextResponse.json(
{ message: "User not found" },
{ status: 404 }
);
}
const user = userResult.rows[0];
const passwordMatch = await bcrypt.compare(
currentPassword,
user.password_hash
);
if (!passwordMatch) {
return NextResponse.json(
{ message: "Current password is incorrect" },
{ status: 401 }
);
}
// Hash new password
const newPasswordHash = await bcrypt.hash(newPassword, 10);
// Update password in the daGET function · javascript · L4-L62 (59 LOC)src/app/api/clinical-trials/active/route.js
export async function GET() {
try {
const tenant = process.env.NEXT_PUBLIC_SITE_KEY;
if (!tenant) {
return NextResponse.json(
{ success: false, error: "Missing site tenant" },
{ status: 400 },
);
}
const trials = await sql`
SELECT
nct_id,
COALESCE(short_title_manual, short_title) AS short_title,
COALESCE(ai_summary_manual, ai_summary) AS ai_summary,
tenant,
overall_status,
start_date,
primary_completion_date,
conditions,
raw_data->'protocolSection'->'contactsLocationsModule'->'locations'->0->>'city'
AS location_city,
raw_data->'protocolSection'->'contactsLocationsModule'->'locations'->0->>'state'
AS location_state,
raw_data->'protocolSection'->'contactsLocationsModule'->'locations'->0->>'country'
AS location_country,
raw_data->'protocolSection'->'contactsLocationsModule'->'locations'
AS locations,
GET function · javascript · L1-L40 (40 LOC)src/app/api/clinical-trials/list/route.js
export async function GET(req) {
const { searchParams } = new URL(req.url);
const page = Number(searchParams.get("page") || 1);
const limit = 6;
const offset = (page - 1) * limit;
const q = searchParams.get("q") || "";
const tenant = process.env.NEXT_PUBLIC_SITE_KEY;
if (!tenant) {
return NextResponse.json(
{ success: false, error: "Missing tenant" },
{ status: 400 }
);
}
const trials = await sql`
SELECT
nct_id,
overall_status,
COALESCE(ai_summary_manual, ai_summary) AS ai_summary
FROM clinical_trials
WHERE is_active = true
AND tenant = ${tenant}
AND (
nct_id ILIKE ${"%" + q + "%"}
OR COALESCE(ai_summary_manual, ai_summary) ILIKE ${"%" + q + "%"}
)
ORDER BY last_synced_at DESC
LIMIT ${limit}
OFFSET ${offset}
`;
return NextResponse.json({
success: true,
trials,
total: Number(total[0].count),
page,
});
}GET function · javascript · L4-L65 (62 LOC)src/app/api/clinical-trials/[nctId]/route.js
export async function GET(req, { params }) {
const { nctId } = params;
const tenant = process.env.NEXT_PUBLIC_SITE_KEY;
if (!tenant) {
return NextResponse.json(
{ success: false, error: "Missing site tenant" },
{ status: 400 }
);
}
try {
const result = await sql`
SELECT
nct_id,
tenant,
overall_status,
start_date,
primary_completion_date,
completion_date,
last_update_date,
conditions,
keywords,
COALESCE(short_title_manual, short_title) AS short_title,
COALESCE(ai_summary_manual, ai_summary) AS ai_summary,
COALESCE(ai_purpose_manual, ai_purpose) AS ai_purpose,
COALESCE(ai_treatments_manual, ai_treatments) AS ai_treatments,
COALESCE(ai_design_manual, ai_design) AS ai_design,
COALESCE(ai_eligibility_manual, ai_eligibility) AS ai_eligibility,
generatePatientSummary function · javascript · L10-L33 (24 LOC)src/app/api/clinical-trials/regenerate/[nctid]/route.js
async function generatePatientSummary(trial) {
const protocol = trial.protocolSection;
const prompt = `
Summarize this clinical trial for patients in simple language.
Title: ${protocol.identificationModule?.briefTitle}
Conditions: ${protocol.conditionsModule?.conditions?.join(", ")}
Eligibility: ${protocol.eligibilityModule?.eligibilityCriteria}
Locations: ${protocol.contactsLocationsModule?.locations
?.map((l) => `${l.city}, ${l.country}`)
.join("; ")}
Summary:
`;
const response = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: prompt }],
temperature: 0.3,
});
return response.choices[0].message.content.trim();
}POST function · javascript · L35-L67 (33 LOC)src/app/api/clinical-trials/regenerate/[nctid]/route.js
export async function POST(req, { params }) {
const { nctId } = params;
const rows = await sql`
SELECT raw_data
FROM clinical_trials
WHERE nct_id = ${nctId}
LIMIT 1
`;
if (!rows.length) {
return NextResponse.json({ error: "Trial not found" }, { status: 404 });
}
const trial = rows[0].raw_data;
const aiSummary = await generatePatientSummary(trial);
const sourceHash = crypto
.createHash("sha256")
.update(JSON.stringify(trial))
.digest("hex");
await sql`
UPDATE clinical_trials
SET
ai_summary = ${aiSummary},
source_hash = ${sourceHash},
ai_summary_updated_at = NOW()
WHERE nct_id = ${nctId}
`;
return NextResponse.json({ success: true });
}normalizeDate function · javascript · L49-L54 (6 LOC)src/app/api/clinical-trials/sync/route.js
function normalizeDate(dateStr) {
if (!dateStr) return null;
if (/^\d{4}-\d{2}$/.test(dateStr)) return `${dateStr}-01`;
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return dateStr;
return null;
}trialMatchesTenant function · javascript · L56-L77 (22 LOC)src/app/api/clinical-trials/sync/route.js
function trialMatchesTenant(trial, tenantKey) {
const config = TENANT_CONFIG[tenantKey];
if (!config) return false;
const conditions = trial.protocolSection?.conditionsModule?.conditions || [];
const haystack = conditions.join(" ").toLowerCase();
const matchesRequired = config.required.some((term) =>
haystack.includes(term.toLowerCase())
);
if (!matchesRequired) return false;
if (config.exclude?.length) {
const hasExcluded = config.exclude.some((term) =>
haystack.includes(term.toLowerCase())
);
if (hasExcluded) return false;
}
return true;
}Same scanner, your repo: https://repobility.com — Repobility
buildSourceHash function · javascript · L79-L92 (14 LOC)src/app/api/clinical-trials/sync/route.js
function buildSourceHash(trial) {
const p = trial.protocolSection;
return crypto
.createHash("sha256")
.update(
JSON.stringify({
title: p.identificationModule?.briefTitle ?? "",
conditions: p.conditionsModule?.conditions ?? [],
eligibility: p.eligibilityModule?.eligibilityCriteria ?? "",
status: p.statusModule?.overallStatus ?? "",
})
)
.digest("hex");
}aiCall function · javascript · L94-L105 (12 LOC)src/app/api/clinical-trials/sync/route.js
async function aiCall(prompt) {
const res = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: prompt },
],
temperature: 0.3,
});
return res.choices[0].message.content.trim();
}generateShortTitle function · javascript · L109-L168 (60 LOC)src/app/api/clinical-trials/sync/route.js
async function generateShortTitle(trial) {
const p = trial.protocolSection;
const studyType = p.designModule?.studyType?.toLowerCase() || "";
const conditions = p.conditionsModule?.conditions?.join(", ") || "";
const drugs = p.armsInterventionsModule?.interventions
?.map((i) => i.name)
.filter(Boolean)
.join(", ");
if (studyType === "interventional" && drugs) {
const drugsText = drugs || "Not specified";
const conditionsText = conditions || "Not specified";
const prompt = `
Create a short, patient-friendly study title.
Rules (must follow ALL):
- Format EXACTLY: [Drug or Drugs] for [Specific disease or tumor]
- Use the SPECIFIC tumor or condition being treated
- If the condition occurs within a larger disorder, name only the tumor
- Use real drug name(s)
- 3–6 words total
- No punctuation
- No phase numbers
- Do NOT use the words "study", "trial", or "treatment"
Drug(s): ${drugsText}
Condition being treated: ${conditionsText}
Return ONLY the title.generatePatientSummary function · javascript · L170-L203 (34 LOC)src/app/api/clinical-trials/sync/route.js
async function generatePatientSummary(trial) {
const p = trial.protocolSection;
const conditions = p.conditionsModule?.conditions?.join(", ") || "the condition being studied";
const studyType = p.designModule?.studyType?.toLowerCase() || "observational";
const interventions = p.armsInterventionsModule?.interventions?.map((i) => i.name).filter(Boolean) || [];
return aiCall(`
Write ONE clear, patient-friendly summary paragraph (5–7 sentences).
Explain:
1. The specific disease or tumor being treated (be precise)
2. What type of study this is (interventional or observational)
3. What drug, device, or approach is being tested
4. What type of drug or approach this is and how it works in simple terms
5. What participants are asked to do
6. How the information from the study will be used
STRICT RULES (must follow ALL):
- Use plain text only
- DO NOT use markdown, asterisks (*), bold, italics, or symbols
- Always name the SPECIFIC disease or tumor being treated
- If the condition igeneratePurpose function · javascript · L205-L244 (40 LOC)src/app/api/clinical-trials/sync/route.js
async function generatePurpose(trial) {
const p = trial.protocolSection;
const indication = p.conditionsModule?.conditions?.join(", ") || "";
const drugs = p.armsInterventionsModule?.interventions?.map((i) => i.name).filter(Boolean).join(", ");
const source = [
p.identificationModule?.briefSummary,
p.descriptionModule?.briefSummary,
p.descriptionModule?.detailedDescription,
].filter(Boolean).join("\n\n");
const result = await aiCall(`
Answer this question for a patient:
"What is the purpose of this study?"
STRICT RULES (must follow ALL):
- Clearly state WHAT drug, therapy, or approach is being tested
- Clearly state the SPECIFIC disease, tumor, or condition being treated
- Explain WHAT the study is measuring (for example: tumor shrinkage, safety, side effects, symptom control)
- Use concrete, plain language
- Do NOT describe disease biology
- Do NOT use vague phrases like "to better understand", "researchers want to learn more", "being evaluated", "for futuregenerateTreatments function · javascript · L246-L270 (25 LOC)src/app/api/clinical-trials/sync/route.js
async function generateTreatments(trial) {
const interventions = trial.protocolSection?.armsInterventionsModule?.interventions?.map((i) => i.name).filter(Boolean) || [];
if (!interventions.length) return "This study does not test a specific drug or device.";
const result = await aiCall(`
Answer this question for a patient: "What treatments are being tested?"
Rules:
- Clearly name the treatment(s)
- If the study does not explain how they work, say so briefly and neutrally
- Do NOT mention missing text
- Do NOT ask for more information
- Do NOT speculate
- 1–2 sentences
- Plain language
Treatments: ${interventions.join(", ")}
`);
const forbidden = ["paste", "provide more", "not enough information", "cannot determine", "i need more"];
if (!result || forbidden.some((f) => result.toLowerCase().includes(f))) {
return `The study is testing ${interventions.join(", ")}.`;
}
return result.trim();
}generateDesign function · javascript · L272-L292 (21 LOC)src/app/api/clinical-trials/sync/route.js
async function generateDesign(trial) {
const p = trial.protocolSection;
const studyType = p.designModule?.studyType?.toLowerCase();
if (studyType === "observational") {
const text = p.detailedDescription || p.designModule?.description || "";
if (!text) return "This is an observational study where researchers collect information over time without assigning treatments or interventions.";
return aiCall(`Explain how this observational study works. Rules: Describe what information is collected and how participants are followed. Mention surveys, interviews, medical record review, imaging, or follow-ups if listed. Do NOT describe treatments or assignments. Do NOT ask for more information. Do NOT mention missing text. 2–4 sentences. Plain language. Text: ${text}`);
}
const source = [
p.designModule?.studyType,
p.designModule?.allocation,
p.designModule?.interventionModel,
p.designModule?.masking,
p.armsInterventionsModule?.armGroups?.map((a) => a.labgenerateEligibility function · javascript · L294-L296 (3 LOC)src/app/api/clinical-trials/sync/route.js
async function generateEligibility(trial) {
return aiCall(`Create two bullet lists: Who may be able to join, Who may not be able to join. Rules: Use only the eligibility text. Plain language. No extra explanations. Text: ${trial.protocolSection?.eligibilityModule?.eligibilityCriteria || ""}`);
}Want fix-PRs on findings? Install Repobility's GitHub App · github.com/apps/repobility-bot
generateParticipation function · javascript · L298-L309 (12 LOC)src/app/api/clinical-trials/sync/route.js
async function generateParticipation(trial) {
const text = trial.protocolSection?.detailedDescription || trial.protocolSection?.descriptionModule?.briefSummary || "";
if (!text || text.trim().length < 40) return "The study team will explain what participation involves.";
const result = await aiCall(`You are writing for patients. Answer the question: "What is participation like?" STRICT RULES: Describe what participants actually DO. Mention visits, procedures, surveys, imaging, medications, or follow-ups if listed. If randomized, say participants may be assigned to a group. Do NOT mention missing information. Do NOT say text was not provided. Do NOT hedge or apologize. Do NOT ask questions. 2–3 sentences MAX. Plain, neutral language. Use ONLY the text provided. Text: ${text}`);
const badPhrases = ["not provided", "wasn't provided", "missing", "no information", "cannot summarize", "can't summarize", "please", "paste", "looks like", "based on the text provided"];
if (!result |generateLeadership function · javascript · L311-L319 (9 LOC)src/app/api/clinical-trials/sync/route.js
async function generateLeadership(trial) {
const sponsor = trial.protocolSection?.sponsorCollaboratorsModule?.leadSponsor?.name;
const contacts = trial.protocolSection?.contactsLocationsModule?.centralContacts || [];
if (!sponsor && contacts.length === 0) return "This study is being run by the study team listed on ClinicalTrials.gov.";
if (contacts.length === 0) return `This study is being run by ${sponsor}.`;
return aiCall(`Explain who is running the study. Rules: Use plain text only. Do NOT ask the reader for information. Do NOT say "share it and I can add it". Do NOT mention missing information. Name the sponsor. List contact details exactly as provided. Short and factual. Sponsor: ${sponsor || ""} Contacts: ${contacts.map((c) => `${c.name} (${c.phone || "no phone listed"}, ${c.email || "no email listed"})`).join("\n")}`);
}generatePriorResearch function · javascript · L321-L326 (6 LOC)src/app/api/clinical-trials/sync/route.js
async function generatePriorResearch(trial) {
const condition = trial.protocolSection?.conditionsModule?.conditions?.join(", ");
const interventions = trial.protocolSection?.armsInterventionsModule?.interventions?.map((i) => i.name).join(", ");
return aiCall(`You are writing a short "Prior Research" section for a patient-facing clinical trial summary. This section should summarize general background research related to the condition and treatment. Important rules: Do NOT claim that this trial itself produced these results. Do NOT invent study names, years, authors, or citations. Speak generally (e.g. "previous studies have shown...", "earlier research suggests..."). If little is known, say so clearly. 1–2 short paragraphs, plain language. Condition: ${condition || "Not specified"} Treatment / Intervention: ${interventions || "Not specified"}`);
}generateLocations function · javascript · L328-L332 (5 LOC)src/app/api/clinical-trials/sync/route.js
function generateLocations(trial) {
const locations = trial.protocolSection?.contactsLocationsModule?.locations || [];
if (!locations.length) return "This is a decentralized study, which means it can be done remotely.";
return locations.map((l) => `${l.city}, ${l.state}, ${l.country}`).filter(Boolean).join("; ");
}fetchAllStudies function · javascript · L336-L363 (28 LOC)src/app/api/clinical-trials/sync/route.js
async function fetchAllStudies(tenant) {
const SEARCH_TERMS = TENANT_CONFIG[tenant].required;
const BASE_URL = "https://clinicaltrials.gov/api/v2/studies";
const PAGE_SIZE = 50;
const MAX_STUDIES = 500;
let allStudies = [];
let pageToken = null;
do {
const url = new URL(BASE_URL);
url.searchParams.set("query.cond", SEARCH_TERMS.join(" OR "));
url.searchParams.set("pageSize", PAGE_SIZE.toString());
if (pageToken) {
url.searchParams.set("pageToken", pageToken);
}
const res = await fetch(url.toString());
const data = await res.json();
const studies = data.studies || [];
allStudies.push(...studies);
pageToken = data.nextPageToken || null;
} while (pageToken && allStudies.length < MAX_STUDIES);
return allStudies.slice(0, MAX_STUDIES);
}checkShouldSkip function · javascript · L367-L404 (38 LOC)src/app/api/clinical-trials/sync/route.js
async function checkShouldSkip(nctId, study) {
const newHash = buildSourceHash(study);
const existing = await sql`
SELECT
source_hash,
last_synced_at,
ai_purpose,
ai_treatments,
ai_design,
ai_eligibility,
ai_participation,
ai_leadership,
ai_prior_research,
ai_locations
FROM clinical_trials
WHERE nct_id = ${nctId}
LIMIT 1
`;
const hasAllAI =
existing[0]?.ai_purpose &&
existing[0]?.ai_treatments &&
existing[0]?.ai_design &&
existing[0]?.ai_eligibility &&
existing[0]?.ai_participation &&
existing[0]?.ai_leadership &&
existing[0]?.ai_prior_research &&
existing[0]?.ai_locations;
const shouldSkip =
existing.length &&
existing[0].source_hash === newHash &&
new Date(existing[0].last_synced_at).getTime() > Date.now() - WEEK_MS &&
hasAllAI;
return shouldSkip;
}processStudy function · javascript · L408-L486 (79 LOC)src/app/api/clinical-trials/sync/route.js
async function processStudy(study, tenant) {
const p = study.protocolSection;
const nctId = p?.identificationModule?.nctId;
const newHash = buildSourceHash(study);
const aiPayload = {
shortTitle: await generateShortTitle(study),
summary: await generatePatientSummary(study),
purpose: await generatePurpose(study),
treatments: await generateTreatments(study),
design: await generateDesign(study),
eligibility: await generateEligibility(study),
participation: await generateParticipation(study),
leadership: await generateLeadership(study),
priorResearch: await generatePriorResearch(study),
locations: generateLocations(study),
};
if (
!aiPayload.purpose ||
!aiPayload.design ||
!aiPayload.eligibility ||
!aiPayload.participation ||
!aiPayload.leadership ||
!aiPayload.priorResearch ||
!aiPayload.locations
) {
throw new Error("Incomplete AI payload");
}
await sql`
INSERT INTO clinical_trials (
ncGET function · javascript · L490-L611 (122 LOC)src/app/api/clinical-trials/sync/route.js
export async function GET(req) {
const TENANT = defaultTenant.shortName?.toUpperCase();
if (!TENANT || !TENANT_CONFIG[TENANT]) {
return new Response(
JSON.stringify({ success: false, error: "Invalid tenant" }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const send = (data) => {
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
};
try {
// Step 1: Fetch all studies
send({ type: "status", message: "Fetching studies from ClinicalTrials.gov...", tenant: TENANT });
const allStudies = await fetchAllStudies(TENANT);
// Filter to matching studies
const matchingStudies = allStudies.filter((s) => trialMatchesTenant(s, TENANT));
send({
type: "status",
message: `Found ${allStudies.length} studies, ${matchingStudies.lengtRepobility · severity-and-effort ranking · https://repobility.com
normalizeDate function · javascript · L55-L60 (6 LOC)src/app/api/clinical-trials/sync/[tenant]/route.js
function normalizeDate(dateStr) {
if (!dateStr) return null;
if (/^\d{4}-\d{2}$/.test(dateStr)) return `${dateStr}-01`;
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return dateStr;
return null;
}trialMatchesTenant function · javascript · L62-L86 (25 LOC)src/app/api/clinical-trials/sync/[tenant]/route.js
function trialMatchesTenant(trial, tenantKey) {
const config = TENANT_CONFIG[tenantKey];
if (!config) return false;
const conditions = trial.protocolSection?.conditionsModule?.conditions || [];
const haystack = conditions.join(" ").toLowerCase();
// MUST match at least one required term
const matchesRequired = config.required.some((term) =>
haystack.includes(term.toLowerCase())
);
if (!matchesRequired) return false;
// MUST NOT contain excluded terms
if (config.exclude?.length) {
const hasExcluded = config.exclude.some((term) =>
haystack.includes(term.toLowerCase())
);
if (hasExcluded) return false;
}
return true;
}buildSourceHash function · javascript · L88-L101 (14 LOC)src/app/api/clinical-trials/sync/[tenant]/route.js
function buildSourceHash(trial) {
const p = trial.protocolSection;
return crypto
.createHash("sha256")
.update(
JSON.stringify({
title: p.identificationModule?.briefTitle ?? "",
conditions: p.conditionsModule?.conditions ?? [],
eligibility: p.eligibilityModule?.eligibilityCriteria ?? "",
status: p.statusModule?.overallStatus ?? "",
})
)
.digest("hex");
}aiCall function · javascript · L103-L114 (12 LOC)src/app/api/clinical-trials/sync/[tenant]/route.js
async function aiCall(prompt) {
const res = await openai.chat.completions.create({
model: "gpt-5.2",
messages: [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: prompt },
],
temperature: 0.3,
});
return res.choices[0].message.content.trim();
}generateShortTitle function · javascript · L117-L176 (60 LOC)src/app/api/clinical-trials/sync/[tenant]/route.js
async function generateShortTitle(trial) {
const p = trial.protocolSection;
const studyType = p.designModule?.studyType?.toLowerCase() || "";
const conditions = p.conditionsModule?.conditions?.join(", ") || "";
const drugs = p.armsInterventionsModule?.interventions
?.map((i) => i.name)
.filter(Boolean)
.join(", ");
if (studyType === "interventional" && drugs) {
const drugsText = drugs || "Not specified";
const conditionsText = conditions || "Not specified";
const prompt = `
Create a short, patient-friendly study title.
Rules (must follow ALL):
- Format EXACTLY: [Drug or Drugs] for [Specific disease or tumor]
- Use the SPECIFIC tumor or condition being treated
- If the condition occurs within a larger disorder, name only the tumor
- Use real drug name(s)
- 3–6 words total
- No punctuation
- No phase numbers
- Do NOT use the words “study”, “trial”, or “treatment”
Drug(s): ${drugsText}
Condition being treated: ${conditionsText}
Return ONLY the title.generatePatientSummary function · javascript · L179-L224 (46 LOC)src/app/api/clinical-trials/sync/[tenant]/route.js
async function generatePatientSummary(trial) {
const p = trial.protocolSection;
const conditions =
p.conditionsModule?.conditions?.join(", ") || "the condition being studied";
const studyType = p.designModule?.studyType?.toLowerCase() || "observational";
const interventions =
p.armsInterventionsModule?.interventions
?.map((i) => i.name)
.filter(Boolean) || [];
return aiCall(`
Write ONE clear, patient-friendly summary paragraph (5–7 sentences).
Explain:
1. The specific disease or tumor being treated (be precise)
2. What type of study this is (interventional or observational)
3. What drug, device, or approach is being tested
4. What type of drug or approach this is and how it works in simple terms
5. What participants are asked to do
6. How the information from the study will be used
STRICT RULES (must follow ALL):
- Use plain text only
- DO NOT use markdown, asterisks (*), bold, italics, or symbols
- Always name the SPECIFIC disease or tumor being tregeneratePurpose function · javascript · L228-L299 (72 LOC)src/app/api/clinical-trials/sync/[tenant]/route.js
async function generatePurpose(trial) {
const p = trial.protocolSection;
const indication = p.conditionsModule?.conditions?.join(", ") || "";
const drugs = p.armsInterventionsModule?.interventions
?.map((i) => i.name)
.filter(Boolean)
.join(", ");
const source = [
p.identificationModule?.briefSummary,
p.descriptionModule?.briefSummary,
p.descriptionModule?.detailedDescription,
]
.filter(Boolean)
.join("\n\n");
const result = await aiCall(`
Answer this question for a patient:
"What is the purpose of this study?"
STRICT RULES (must follow ALL):
- Clearly state WHAT drug, therapy, or approach is being tested
- Clearly state the SPECIFIC disease, tumor, or condition being treated
(not just the broader diagnosis)
- Explain WHAT the study is measuring (for example: tumor shrinkage, safety, side effects, symptom control)
- Use concrete, plain language
- Do NOT describe disease biology
- Do NOT use vague phrases like:
"to better understand"generateTreatments function · javascript · L301-L343 (43 LOC)src/app/api/clinical-trials/sync/[tenant]/route.js
async function generateTreatments(trial) {
const interventions =
trial.protocolSection?.armsInterventionsModule?.interventions
?.map((i) => i.name)
.filter(Boolean) || [];
// ✅ SAFE FALLBACK — NEVER null
if (!interventions.length) {
return "This study does not test a specific drug or device.";
}
const result = await aiCall(`
Answer this question for a patient:
"What treatments are being tested?"
Rules:
- Clearly name the treatment(s)
- If the study does not explain how they work, say so briefly and neutrally
- Do NOT mention missing text
- Do NOT ask for more information
- Do NOT speculate
- 1–2 sentences
- Plain language
Treatments:
${interventions.join(", ")}
`);
const forbidden = [
"paste",
"provide more",
"not enough information",
"cannot determine",
"i need more",
];
if (!result || forbidden.some((f) => result.toLowerCase().includes(f))) {
return `The study is testing ${interventions.join(", ")}.`;
}
return reRepobility · open methodology · https://repobility.com/research/
generateDesign function · javascript · L345-L403 (59 LOC)src/app/api/clinical-trials/sync/[tenant]/route.js
async function generateDesign(trial) {
const p = trial.protocolSection;
const studyType = p.designModule?.studyType?.toLowerCase();
// OBSERVATIONAL
if (studyType === "observational") {
const text = p.detailedDescription || p.designModule?.description || "";
if (!text) {
return "This is an observational study where researchers collect information over time without assigning treatments or interventions.";
}
return aiCall(`
Explain how this observational study works.
Rules:
- Describe what information is collected and how participants are followed
- Mention surveys, interviews, medical record review, imaging, or follow-ups if listed
- Do NOT describe treatments or assignments
- Do NOT ask for more information
- Do NOT mention missing text
- 2–4 sentences
- Plain language
Text:
${text}
`);
}
// INTERVENTIONAL
const source = [
p.designModule?.studyType,
p.designModule?.allocation,
p.designModule?.interventionModel,
p.designModule?.mgenerateEligibility function · javascript · L405-L419 (15 LOC)src/app/api/clinical-trials/sync/[tenant]/route.js
async function generateEligibility(trial) {
return aiCall(`
Create two bullet lists:
- Who may be able to join
- Who may not be able to join
Rules:
- Use only the eligibility text
- Plain language
- No extra explanations
Text:
${trial.protocolSection?.eligibilityModule?.eligibilityCriteria || ""}
`);
}generateParticipation function · javascript · L439-L494 (56 LOC)src/app/api/clinical-trials/sync/[tenant]/route.js
async function generateParticipation(trial) {
const text =
trial.protocolSection?.detailedDescription ||
trial.protocolSection?.descriptionModule?.briefSummary ||
"";
if (!text || text.trim().length < 40) {
return "The study team will explain what participation involves.";
}
const result = await aiCall(`
You are writing for patients.
Answer the question:
"What is participation like?"
STRICT RULES (must follow ALL):
- Describe what participants actually DO
- Mention visits, procedures, surveys, imaging, medications, or follow-ups if listed
- If randomized, say participants may be assigned to a group
- Do NOT mention missing information
- Do NOT say text was not provided
- Do NOT hedge or apologize
- Do NOT ask questions
- 2–3 sentences MAX
- Plain, neutral language
- Use ONLY the text provided
- If participation details are vague, give a general but neutral description
Text:
${text}
`);
const badPhrases = [
"not provided",
"wasn't provided",
"generateLeadership function · javascript · L506-L547 (42 LOC)src/app/api/clinical-trials/sync/[tenant]/route.js
async function generateLeadership(trial) {
const sponsor =
trial.protocolSection?.sponsorCollaboratorsModule?.leadSponsor?.name;
const contacts =
trial.protocolSection?.contactsLocationsModule?.centralContacts || [];
// Clean fallback when no contact info exists
if (!sponsor && contacts.length === 0) {
return "This study is being run by the study team listed on ClinicalTrials.gov.";
}
if (contacts.length === 0) {
return `This study is being run by ${sponsor}.`;
}
return aiCall(`
Explain who is running the study.
Rules (must follow ALL):
- Use plain text only
- Do NOT ask the reader for information
- Do NOT say "share it and I can add it"
- Do NOT mention missing information
- Name the sponsor
- List contact details exactly as provided
- Short and factual
Sponsor:
${sponsor || ""}
Contacts:
${contacts
.map(
(c) =>
`${c.name} (${c.phone || "no phone listed"}, ${
c.email || "no email listed"
})`
)
.join("\n")}
`);
}generatePriorResearch function · javascript · L549-L579 (31 LOC)src/app/api/clinical-trials/sync/[tenant]/route.js
async function generatePriorResearch(trial) {
const condition =
trial.protocolSection?.conditionsModule?.conditions?.join(", ");
const interventions =
trial.protocolSection?.armsInterventionsModule?.interventions
?.map((i) => i.name)
.join(", ");
return aiCall(`
You are writing a short "Prior Research" section for a patient-facing clinical trial summary.
This section should summarize **general background research** related to the condition and treatment,
and may include information from published literature (e.g. PubMed, prior clinical studies, or well-established research).
Important rules:
- Do NOT claim that this trial itself produced these results
- Do NOT invent study names, years, authors, or citations
- Speak generally (e.g. "previous studies have shown...", "earlier research suggests...")
- If little is known, say so clearly
- 1–2 short paragraphs, plain language
Condition:
${condition || "Not specified"}
Treatment / Intervention:
${interventionsgenerateLocations function · javascript · L581-L593 (13 LOC)src/app/api/clinical-trials/sync/[tenant]/route.js
function generateLocations(trial) {
const locations =
trial.protocolSection?.contactsLocationsModule?.locations || [];
if (!locations.length) {
return "This is a decentralized study, which means it can be done remotely.";
}
return locations
.map((l) => `${l.city}, ${l.state}, ${l.country}`)
.filter(Boolean)
.join("; ");
}GET function · javascript · L597-L845 (249 LOC)src/app/api/clinical-trials/sync/[tenant]/route.js
export async function GET(req, context) {
const { tenant } = await context.params;
const TENANT = tenant?.toUpperCase();
if (!TENANT || !TENANT_CONFIG[TENANT]) {
return NextResponse.json(
{ success: false, error: "Invalid tenant" },
{ status: 400 }
);
}
const SEARCH_TERMS = TENANT_CONFIG[TENANT].required;
// ---- NEW: pagination + safety cap ----
const BASE_URL = "https://clinicaltrials.gov/api/v2/studies";
const PAGE_SIZE = 50;
const MAX_STUDIES = 500;
let allStudies = [];
let pageToken = null;
try {
do {
const url = new URL(BASE_URL);
url.searchParams.set("query.cond", SEARCH_TERMS.join(" OR "));
url.searchParams.set("pageSize", PAGE_SIZE.toString());
if (pageToken) {
url.searchParams.set("pageToken", pageToken);
}
const res = await fetch(url.toString());
const data = await res.json();
const studies = data.studies || [];
allStudies.push(...studies);
pageToken = POST function · javascript · L6-L27 (22 LOC)src/app/api/contact/article-request/route.js
export async function POST(req) {
try {
const { article_link, submitted_by, anonymous } = await req.json();
// Insert article request; conditionally include 'requested_by' based on anonymity
const result = await query(
`INSERT INTO requested_articles (article_link${
anonymous ? "" : ", requested_by"
})
VALUES ($1${anonymous ? "" : ", $2"}) RETURNING *;`,
anonymous ? [article_link] : [article_link, submitted_by]
);
return NextResponse.json(result.rows[0]);
} catch (error) {
console.error("Error adding article request:", error);
return NextResponse.json(
{ error: "Error adding article request" },
{ status: 500 }
);
}
}Same scanner, your repo: https://repobility.com — Repobility
POST function · javascript · L6-L30 (25 LOC)src/app/api/contact/bug-report/route.js
export async function POST(req) {
try {
const { report, submitted_by, anonymous } = await req.json();
const submittedAt = new Date().toISOString();
// Insert bug report; conditionally include 'submitted_by' based on anonymity
const result = await query(
`INSERT INTO bug_reports (report, submitted_at${
anonymous ? "" : ", submitted_by"
})
VALUES ($1, $2${anonymous ? "" : ", $3"}) RETURNING *;`,
anonymous
? [JSON.stringify(report), submittedAt] // Save report as JSON if needed
: [JSON.stringify(report), submittedAt, submitted_by] // Use the submitted_by variable
);
return NextResponse.json(result.rows[0]);
} catch (error) {
console.error("Error adding bug report:", error);
return NextResponse.json(
{ error: "Error adding bug report" },
{ status: 500 }
);
}
}GET function · javascript · L5-L49 (45 LOC)src/app/api/cron/clinical-trials/route.js
export async function GET(req) {
const authHeader = req.headers.get("authorization");
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json(
{ success: false, error: "Unauthorized" },
{ status: 401 }
);
}
const baseUrl =
process.env.NEXT_PUBLIC_SITE_URL ||
`https://${req.headers.get("host")}`;
const results = [];
for (const tenant of TENANTS) {
try {
const res = await fetch(
`${baseUrl}/api/clinical-trials/sync/${tenant}`,
{ cache: "no-store" }
);
const data = await res.json();
results.push({
tenant,
success: res.ok,
response: data,
});
} catch (err) {
results.push({
tenant,
success: false,
error: String(err),
});
}
}
return NextResponse.json({
success: true,
ranAt: new Date().toISOString(),
results,
});
}GET function · javascript · L6-L36 (31 LOC)src/app/api/editors/assigned-articles/route.js
export async function GET(request) {
const editorId = request.nextUrl.searchParams.get("editorId");
if (!editorId) {
return NextResponse.json(
{ message: "Editor ID is required" },
{ status: 400 }
);
}
try {
const result = await query(
`
SELECT pa.*
FROM pending_article pa
JOIN article_assignments aa ON pa.id = aa.article_id
WHERE aa.editor_id = $1
ORDER BY pa.created_at DESC
`,
[editorId]
);
return NextResponse.json(result.rows);
} catch (error) {
console.error("Error fetching assigned articles:", error);
return NextResponse.json(
{ message: "Error fetching assigned articles" },
{ status: 500 }
);
}
}