ClinicalTrials.gov: Searches and retrieves clinical trial records from the public ClinicalTrials.gov API v2, no key required.
| 1 | // How a ClinicalTrials.gov v2 study is read into flat rows. |
| 2 | |
| 3 | /** The parts of a study record this plugin reads, as the API answers them. */ |
| 4 | export interface Study { |
| 5 | protocolSection?: { |
| 6 | identificationModule?: { nctId?: string; briefTitle?: string; officialTitle?: string } |
| 7 | statusModule?: { |
| 8 | overallStatus?: string |
| 9 | startDateStruct?: { date?: string; type?: string } |
| 10 | primaryCompletionDateStruct?: { date?: string; type?: string } |
| 11 | completionDateStruct?: { date?: string; type?: string } |
| 12 | studyFirstPostDateStruct?: { date?: string } |
| 13 | resultsFirstPostDateStruct?: { date?: string } |
| 14 | lastUpdatePostDateStruct?: { date?: string } |
| 15 | } |
| 16 | sponsorCollaboratorsModule?: { |
| 17 | leadSponsor?: { name?: string; class?: string } |
| 18 | collaborators?: { name?: string }[] |
| 19 | } |
| 20 | descriptionModule?: { briefSummary?: string } |
| 21 | conditionsModule?: { conditions?: string[] } |
| 22 | designModule?: { |
| 23 | studyType?: string |
| 24 | phases?: string[] |
| 25 | designInfo?: { allocation?: string; primaryPurpose?: string; maskingInfo?: { masking?: string } } |
| 26 | enrollmentInfo?: { count?: number; type?: string } |
| 27 | } |
| 28 | armsInterventionsModule?: { |
| 29 | armGroups?: { label?: string; type?: string; interventionNames?: string[] }[] |
| 30 | interventions?: { type?: string; name?: string }[] |
| 31 | } |
| 32 | outcomesModule?: { primaryOutcomes?: { measure?: string; timeFrame?: string }[] } |
| 33 | eligibilityModule?: { |
| 34 | eligibilityCriteria?: string |
| 35 | sex?: string |
| 36 | minimumAge?: string |
| 37 | maximumAge?: string |
| 38 | healthyVolunteers?: boolean |
| 39 | } |
| 40 | contactsLocationsModule?: { locations?: unknown[] } |
| 41 | } |
| 42 | hasResults?: boolean |
| 43 | } |
| 44 | |
| 45 | export type Row = Record<string, string | number | boolean | null> |
| 46 | |
| 47 | /** One flat row for a search result. */ |
| 48 | export function summaryRow(study: Study): Row { |
| 49 | const p = study.protocolSection ?? {} |
| 50 | const id = p.identificationModule ?? {} |
| 51 | const status = p.statusModule ?? {} |
| 52 | const sponsors = p.sponsorCollaboratorsModule ?? {} |
| 53 | const design = p.designModule ?? {} |
| 54 | const nctId = id.nctId ?? '' |
| 55 | return { |
| 56 | nctId, |
| 57 | briefTitle: id.briefTitle ?? null, |
| 58 | leadSponsor: sponsors.leadSponsor?.name ?? null, |
| 59 | sponsorClass: sponsors.leadSponsor?.class ?? null, |
| 60 | collaborators: join(sponsors.collaborators?.map((c) => c.name)), |
| 61 | phases: join(design.phases), |
| 62 | overallStatus: status.overallStatus ?? null, |
| 63 | startDate: status.startDateStruct?.date ?? null, |
| 64 | primaryCompletionDate: status.primaryCompletionDateStruct?.date ?? null, |
| 65 | primaryCompletionType: status.primaryCompletionDateStruct?.type ?? null, |
| 66 | completionDate: status.completionDateStruct?.date ?? null, |
| 67 | conditions: join(p.conditionsModule?.conditions), |
| 68 | interventions: join(p.armsInterventionsModule?.interventions?.map((i) => (i.type ? `${i.type}: ${i.name}` : i.name))), |
| 69 | enrollment: design.enrollmentInfo?.count ?? null, |
| 70 | studyType: design.studyType ?? null, |
| 71 | hasResults: study.hasResults ?? false, |
| 72 | lastUpdatePostDate: status.lastUpdatePostDateStruct?.date ?? null, |
| 73 | url: `https://clinicaltrials.gov/study/${nctId}`, |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | /** The summary row plus the key details of one study. */ |
| 78 | export function detailRow(study: Study): Row { |
| 79 | const p = study.protocolSection ?? {} |
| 80 | const status = p.statusModule ?? {} |
| 81 | const design = p.designModule ?? {} |
| 82 | const elig = p.eligibilityModule ?? {} |
| 83 | return { |
| 84 | ...summaryRow(study), |
| 85 | officialTitle: p.identificationModule?.officialTitle ?? null, |
| 86 | briefSummary: p.descriptionModule?.briefSummary ?? null, |
| 87 | primaryOutcomes: join( |
| 88 | p.outcomesModule?.primaryOutcomes?.map((o) => (o.timeFrame ? `${o.measure} [${o.timeFrame}]` : o.measure)), |
| 89 | ' | ', |
| 90 | ), |
| 91 | arms: join( |
| 92 | p.armsInterventionsModule?.armGroups?.map( |
| 93 | (a) => `${a.label}${a.type ? ` (${a.type})` : ''}${a.interventionNames?.length ? `: ${a.interventionNames.join(', ')}` : ''}`, |
| 94 | ), |
| 95 | ' | ', |
| 96 | ), |
| 97 | allocation: design.designInfo?.allocation ?? null, |
| 98 | masking: design.designInfo?.maskingInfo?.masking ?? null, |
| 99 | primaryPurpose: design.designInfo?.primaryPurpose ?? null, |
| 100 | enrollmentType: design.enrollmentInfo?.type ?? null, |
| 101 | sex: elig.sex ?? null, |
| 102 | minimumAge: elig.minimumAge ?? null, |
| 103 | maximumAge: elig.maximumAge ?? null, |
| 104 | healthyVolunteers: elig.healthyVolunteers ?? null, |
| 105 | eligibilityCriteria: elig.eligibilityCriteria ? elig.eligibilityCriteria.slice(0, 4000) : null, |
| 106 | locationsCount: p.contactsLocationsModule?.locations?.length ?? 0, |
| 107 | startDateType: status.startDateStruct?.type ?? null, |
| 108 | completionDateType: status.completionDateStruct?.type ?? null, |
| 109 | studyFirstPostDate: status.studyFirstPostDateStruct?.date ?? null, |
| 110 | resultsFirstPostDate: status.resultsFirstPostDateStruct?.date ?? null, |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | /** A list as one string, or null when empty. */ |
| 115 | function join(items: (string | undefined)[] | undefined, separator = '; '): string | null { |
| 116 | const kept = (items ?? []).filter((s): s is string => Boolean(s)) |
| 117 | return kept.length ? kept.join(separator) : null |
| 118 | } |